Введение
Этот пост - начало серии постов о внутреннем устройстве и интересных деталях различных компонентов технологии Inter-Process-Communication (IPC) на базе Windows. Первоначально в этой серии будут рассмотрены следующие темы:
Именованные 'pipes':
Хотя название может звучать немного странно, 'pipes' - это базовая и простая технология для обеспечения связи и обмена данными между двумя процессами, где термин 'pipes' просто описывает раздел общей памяти, используемый этими двумя процессами.
Чтобы с самого начала ,было правильное понимание, технология IPC, о которой мы говорим, называется " pipes ", и существует два типа pipe:
Прежде чем мы погрузимся во внутреннее устройство Named Pipes пожалуйста, примите к сведению, что далее будет приведено несколько фрагментов кода, взятых из моего публичного примера реализации Named Pipes. Если вы почувствуете, что вам нужно больше контекста, перейдите в репозиторий кода и просмотрите общую картину.
Обмен сообщениями по именованным трубам
Итак, давайте разберемся с внутренним устройством Named Pipe. Если вы никогда раньше не слышали об Named Pipes представьте себе эту коммуникационную технологию как настоящую стальную трубу - у вас есть полый прут с двумя концами, и если вы крикнете что-то в один конец, слушатель услышит ваши слова на другом конце. Это все, что делает Named Pipe - она передает информацию с одного конца на другой.
Если вы являетесь пользователем Unix, вы наверняка уже использовали pipes (поскольку это не чисто Windows-технология), используя что-то вроде этого: cat file.txt | wc -l. Команда, которая выводит содержимое файла file.txt, но вместо вывода на STDOUT (который может быть окном вашего терминала) вывод перенаправляется ("передается") на вход второй команды wc -l, которая таким образом подсчитывает строки вашего файла. Это пример Anonymous Pipes.
Name Pipes в Windows так же легко понять, как и в приведенном выше примере. Чтобы мы могли использовать весь набор возможностей pipe мы отойдем от Anonymous Pipes и создадим сервер и клиент, которые будут общаться друг с другом.
Name Pipe - это просто объект, точнее, FILE_OBJECT, который управляется специальной файловой системой, Named Pipe File System (NPFS):
Когда вы создаете Named Pipe, назовем его 'fpipe', под капотом вы создаете FILE_OBJECT с заданным именем 'fpipe' (отсюда: named pipe) на специальном диске устройства под названием 'pipe'.
Немного практики
Named Pipe создается путем вызова функции WinAPI CreateNamedPipe, как показано ниже [источник]:
На данный момент наиболее интересной частью этого вызова является \\\\.\\\pipe\\fpipe.
C++ требует экранирования слэшей, поэтому независимо от языка \\\.\pipe\fpipe. Ведущее '\.' относится к глобальному корневому каталогу вашей машины, где термин 'pipe' является символической ссылкой на устройство NamedPipe.
Поскольку объект Named Pipe является FILE_OBJECT, доступ к Named Pipe, которую мы только что создали, равносилен доступу к "обычному" файлу.
Поэтому подключиться к Named Pipe с клиента так же просто, как вызвать CreateFile [источник]:
После подключения чтение из pipe требует только вызова ReadFile [источник]
Прежде чем вы сможете прочитать какие-то данные из pipe вы хотите, чтобы ваш сервер записал в нее какие-то данные (которые вы сможете прочитать). Это делается вызовом - кто бы мог подумать - WriteFile [источник]
Но что на самом деле происходит, когда вы "пишете" в pipes?
Как только клиент подключается к серверному каналу, созданная вами 'pipe' больше не находится в состоянии прослушивания, и данные могут быть записаны в нее.
Пользовательский вызов WriteFile передается ядру, где вызывается NtWriteFile, который определяет все детали операции записи, например, какой объект устройства связан с данным файлом, должна ли операция записи быть синхронной, устанавливается пакет запроса ввода/вывода (IRP) и, в конечном итоге, NtWriteFile заботится о том, чтобы ваши данные были записаны в файл. В нашем случае указанные данные записываются не в реальный файл на диске, а в раздел общей памяти, на который ссылается файловый хэндл, возвращаемый из CreateNamedPipe.
Наконец - как уже упоминалось во введении - Named Pipe также можно использовать через сетевое соединение, пересекающее границы системы.
Для вызова удаленного сервера Named Pipe не требуется никаких дополнительных реализаций, просто убедитесь, что в вызове CreateFile указан IP или имя хоста (как в примере выше).
Давайте угадаем: какой сетевой протокол будет использоваться при вызове удаленного сервера? .... барабанная дробь ... абсолютно неудивительно, что это SMB.
С удаленным сервером устанавливается SMB-соединение, которое по умолчанию инициализируется переговорным запросом для определения протокола сетевой аутентификации. В отличие от других механизмов IPC, таких как RPC, вы, как разработчик сервера, не можете контролировать протокол сетевой аутентификации, поскольку он всегда согласовывается через SMB. Поскольку Kerberos является предпочтительной схемой аутентификации, начиная с Windows 2000, Kerberos будет согласован, если это возможно.
Примечание: С точки зрения клиента вы можете эффективно выбирать протокол аутентификации, выбирая подключение к имени хоста или IP. Из-за особенностей конструкции Kerberos он не очень хорошо работает с IP-адресами, поэтому если вы решили подключиться к IP-адресу, результатом согласования всегда будет NTLM(v2). В то время как при подключении к имени хоста вы, скорее всего, всегда будете использовать Kerberos.
Когда аутентификация завершена, действия, которые клиент и сервер хотят выполнить, снова являются классическими файловыми действиями, которые обрабатываются SMB так же, как и любые другие файловые операции, например, путем запуска запроса 'Create Request File', как показано ниже:
Режимы передачи данных
Named Pipe предлагают два основных режима передачи данных: режим байтов и режим сообщений.
В режиме байтов сообщения передаются в виде непрерывного потока байтов между клиентом и сервером. Это означает, что клиентское приложение и серверное приложение не знают точно, сколько байт считывается из pipe или записывается в нее в каждый момент времени. Поэтому запись на одной стороне не всегда приводит к чтению того же размера на другой. Это позволяет клиенту и серверу передавать данные, не заботясь об их размере.
В режиме сообщений клиент и сервер отправляют и получают данные дискретными блоками. Каждый раз, когда сообщение отправляется по pipe, оно должно быть прочитано как полное сообщение. Если вы считываете данные с pipe сервера в режиме сообщений, но ваш буфер чтения слишком мал, чтобы вместить все данные, то часть данных, которая помещается в ваш буфер, будет скопирована в него, остальные данные останутся в разделе общей памяти сервера, и вы получите ошибку 234 (0xEA, ERROR_MORE_DATA), указывающую на то, что необходимо получить больше данных.
Визуальное сравнение режимов сообщений показано ниже, взято из книги "Сетевое программирование для Microsoft Windows" (1999):
Перекрывающийся ввод-вывод, режим блокировки и буферы ввода-вывода
Перекрывающийся ввод-вывод, режим блокировки и буферы ввода-вывода не являются удивительно важными с точки зрения безопасности, но знание о том, что они существуют и что они означают, может помочь в понимании, общении, построении и отладке именованных труб. Поэтому я вкратце расскажу об этих понятиях.
Перекрывающийся ввод-вывод
Некоторые функции, связанные с Named Pipe, такие как ReadFile, WriteFile, TransactNamedPipe и ConnectNamedPipe, могут выполнять операции с трубами либо синхронно, то есть выполняющий поток ждет завершения операции, прежде чем продолжить, либо асинхронно, то есть выполняющий поток запускает действие и продолжает, не дожидаясь его завершения. Важно отметить, что асинхронные операции с pipe могут быть выполнены только на pipe (сервере), который допускает перекрывающийся ввод-вывод, путем установки FILE_FLAG_OVERLAPPED в вызове CreateNamedPipe.
Асинхронные вызовы могут быть выполнены либо путем указания структуры OVERLAPPED в качестве последнего параметра для каждого из вышеупомянутых "стандартных" действий с pipe таких как ReadFile, либо путем указания COMPLETION_ROUTINE в качестве последнего параметра для "расширенных" действий с pipe, таких как ReadFileEx. Первый метод, метод OVERLAPPED structure, основан на событиях, то есть должен быть создан объект события, который сигнализируется по завершении операции, в то время как метод COMPLETION_ROUTINE основан на обратном вызове, то есть исполняющему потоку передается процедура обратного вызова, которая ставится в очередь и выполняется по сигналу. Подробнее об этом можно узнать здесь с примером реализации от Microsoft здесь .
Режим блокировки
Поведение в режиме блокировки определяется при настройке сервера NamedPipe с помощью CreateNamedPipe путем использования (или отсутствия) флага в параметре dwPipeMode. Следующие два флага dwPipeMode определяют режим блокировки сервера:
PIPE_WAIT (по умолчанию): Режим блокировки включен. При использовании операций с NamedPipe таких как ReadFile на Pipe для которой включен режим блокировки, операция ожидает завершения. Это означает, что операция чтения на таком Pipe будет ждать, пока не появятся данные для чтения, а операция записи будет ждать, пока все данные не будут записаны. Это, конечно, может привести к тому, что в некоторых ситуациях операция будет ждать бесконечно долго.
PIPE_NOWAIT: Режим блокировки отключен. Именованные операции pipe, такие как ReadFile, возвращаются немедленно. Чтобы убедиться в том, что все данные прочитаны или записаны, нужны такие процедуры, как Overlapping I/O.
Буферы ввода-вывода
Под буферами ввода-вывода я имею в виду входные и выходные буферы сервера NamedPipe, которые вы создаете при вызове CreateNamedPipe, а точнее, размеры этих буферов в параметрах nInBufferSize и nOutBufferSize.
При выполнении операций чтения и записи ваш сервер NamedPipe использует нестраничную память (имеется в виду физическая память) для временного хранения данных, подлежащих чтению или записи. Злоумышленник, которому разрешено влиять на эти значения для созданного сервера, может злоупотребить ими, чтобы потенциально вызвать крах системы, выбрав большие буферы или задержать операции с Pipe, выбрав маленький буфер (например, 0):
Большие буферы: Поскольку буферы In-/Out являются нестраничными, сервер исчерпает память, если выбрать их слишком большими. Однако параметры nInBufferSize и nOutBufferSize не принимаются системой "вслепую". Верхний предел определяется константой, зависящей от системы; я не смог найти точной информации об этой константе (и не стал копаться в заголовках); в этом сообщении указано, что это ~4GB для системы x64 Windows7.
Маленькие буферы: Размер буфера 0 абсолютно допустим для nInBufferSize и nOutBufferSize. Если бы система строго выполняла то, что ей было сказано, вы бы не смогли ничего записать в pipe, потому что буфер размера 0 - это ... ну, несуществующий буфер. К счастью, система достаточно умна, чтобы понять, что вы просите минимальный размер буфера, и поэтому увеличит фактический размер буфера до размера, который она получит, но это имеет последствия для производительности. Размер буфера 0 означает, что каждый байт должен быть прочитан процессом на другой стороне pipe (и тем самым очищен буфер), прежде чем новые данные могут быть записаны в буфер. Это справедливо для обоих значений, nInBufferSize и nOutBufferSize. Буфер размером 0 может стать причиной задержек сервера.
Безопасность NamedPipe
И снова мы можем сделать эту главу о том, как установить и контролировать безопасность именованного трубопровода, довольно короткой, но важно знать, как это делается.
Единственное средство, которое вы можете включить, когда хотите защитить NamePipe, - это установка дескриптора безопасности для сервера NamePipe в качестве последнего параметра (lpSecurityAttributes) в вызове CreateNamedPipe. Установка этого дескриптора безопасности необязательна; дескриптор безопасности по умолчанию можно установить, указав NULL в параметре lpSecurityAttributes.
В документации Windows определено, что делает дескриптор безопасности по умолчанию для сервера NamedPipe:
ACLs в дескрипторе безопасности по умолчанию для NamedPipe предоставляют полный контроль учетной записи LocalSystem, администраторам и владельцу-создателю. Они также предоставляют доступ на чтение членам группы Everyone и учетной записи anonymous.
CreateNamedPipe > Paremter > lpSecurityAttributes [источник]
Таким образом, по умолчанию все могут читать с вашего сервера NamedPipe если вы не указали дескриптор безопасности, независимо от того, находится ли читающий клиент на той же машине или нет. Если вы подключаетесь к NamedPipe серверу без заданного дескриптора безопасности, но все равно получаете ошибку Access Denied Error (код ошибки: 5), убедитесь, что вы указали только доступ READ (обратите внимание, что в примере выше указан доступ READ и WRITE с GENERIC_READ | GENERIC_WRITE).
Для удаленных подключений еще раз обратите внимание - как описано в конце главы Named Pipe Messaging - что протокол сетевой аутентификации согласовывается между клиентом и сервером через протокол SMB. Не существует способа программно принудительно использовать более сильный протокол Kerberos (вы можете только отключить NTLM на хосте сервера).
Имперсонация
Имперсонация - это простая концепция, которая понадобится нам в следующем разделе для обсуждения векторов атак с использованием NamedPipe.
Если вы знакомы с имперсонацией, можете пропустить этот раздел; имперсонация не относится к NamedPipe.
Если вы еще не сталкивались с имперсонацией в среде Windows, позвольте мне вкратце рассказать вам об этой концепции:
Имперсонация - это способность потока выполняться в контексте безопасности, отличном от контекста безопасности процесса, которому принадлежит поток. Имперсонация обычно применяется в архитектуре клиент-сервер, где клиент подключается к серверу, а сервер может (при необходимости) выдать себя за клиента. Имперсонация позволяет серверу (потоку) выполнять действия от имени клиента, но в рамках прав доступа клиента.
Типичный сценарий - сервер хочет получить доступ к некоторым записям (например, в базе данных), но только клиент имеет право доступа к своим собственным записям. Сервер может ответить клиенту, попросив получить записи самостоятельно и передать их серверу, или сервер может использовать протокол авторизации, чтобы доказать, что клиент разрешил серверу доступ к записям, или - и это то, чем является имперсонация - клиент посылает серверу некоторую идентификационную информацию и позволяет серверу переключиться на роль клиента. Это похоже на то, как если бы клиент передал свои водительские права серверу вместе с разрешением использовать эти права для идентификации других сторон, например, привратника (или, более технически, сервера базы данных).
Идентификационная информация, такая как информация, указывающая, кем является клиент (например, SID), упаковывается в структуру, называемую контекстом безопасности. Эта структура глубоко встроена во внутреннее устройство операционной системы и является необходимым элементом информации для межпроцессного взаимодействия. Поэтому клиент не может выполнить IPC вызов без контекста безопасности, но ему нужен способ указать, что он позволяет серверу знать и делать с его идентификатором. Для контроля этого Microsoft создала так называемые уровни имперсонации.
Структура перечисления SECURITY_IMPERSONATION_LEVEL определяет четыре уровня имперсонации, которые определяют операции, которые сервер может выполнять в контексте клиента.
Для получения дополнительной справочной информации об имперсонации ознакомьтесь с документацией Microsoft по клиентской имперсонации.
Для получения некоторого контекста вокруг имперсонации ознакомьтесь с разделом Токены доступа и следующим разделом об имперсонации в моем посте об авторизации Windows.
Имперсонификация клиента NamedPipe
Итак, раз уж мы затронули эту тему, и если вам еще не совсем скучно. Давайте вкратце расскажем о том, что на самом деле происходит под капотом, если сервер выдает себя за клиента.
Если вас больше интересует, как это реализовать, вы найдете ответ в моем примере реализации
Шаг 1: Сервер ожидает входящего соединения от клиента и после этого вызывает функцию ImpersonateNamedPipeClient.
Шаг 2: Этот вызов приводит к вызову NtCreateEvent (для создания события обратного вызова) и NtFsControlFile, которая является функцией, выполняющей имперсонацию.
Шаг 3: NtFsControlFile - это функция общего назначения, действие которой задается аргументом, в данном случае FSCTL_PIPE_Impersonate.
Приведенное ниже описание основано на открытом исходном коде ReactOS, но я думаю, что оно справедливо как
Шаг 4: Далее по стеку вызовов вызывается NpCommonFileSystemControl, где FSCTL_PIPE_IMPERSONATE передается в качестве аргумента и используется в инструкции switch-case для определения того, что делать.
Шаг 5: NpCommonFileSystemControl вызывает NbAcquireExeclusiveVcb для блокировки объекта, а NpImpersonate вызывается, учитывая объект трубы сервера и IRP (I/O Request Object), выданный клиентом.
Шаг 6: NpImpersonate затем в свою очередь вызывает SeImpersonateClientEx с контекстом безопасности клиента, который был получен из IRP клиента, в качестве параметра.
Шаг 7: SeImpersonateClientEx в свою очередь вызывает PsImpersonateClient с объектом потока сервера и маркером безопасности клиента, который извлекается из контекста безопасности клиента.
Шаг 8: Контекст потока сервера затем изменяется на контекст безопасности клиента.
Шаг 9: Любое действие сервера и любая функция, которую сервер вызывает, находясь в контексте безопасности клиента, выполняются с идентификатором клиента и тем самым выдают себя за клиента.
Шаг 10: Если сервер закончил то, что собирался сделать, будучи клиентом, он вызывает команду RevertToSelf, чтобы вернуться в свой собственный, исходный контекст потока.
Поверхность атаки
Имперсонация клиента
Наконец-то мы заговорили о поверхности атаки. Самый важный вектор атаки, основанный на NamedPipe - это имперсонация.
К счастью, мы уже представили и поняли концепцию имперсонации в предыдущем разделе, поэтому мы можем сразу приступить к работе.
Сценарий атаки
Имперсонацией с помощью NamedPipe лучше всего злоупотреблять, когда у вас есть служба, программа или процедура, которая позволяет вам указать или контролировать доступ к файлу (неважно, разрешает ли она вам доступ на чтение или запись или и то, и другое). Благодаря тому, что NamedPipe по сути являются FILE_OBJECTs и работают с теми же функциями доступа, что и обычные файлы (ReadFile, WriteFile, CreateFile, ...), вы можете указать NamedPipe вместо обычного имени файла и заставить процесс вашей жертвы подключиться к NamedPipe под вашим контролем.
Предварительные условия
Есть два важных аспекта, которые необходимо проверить при попытке выдать себя за клиента.
Во-первых, необходимо проверить, как клиент реализует доступ к файлам, а точнее, указывает ли клиент флаг SECURITY_SQOS_PRESENT при вызове CreateFile?
Уязвимый вызов CreateFile выглядит следующим образом:
В то время как безопасный вызов CreateFile выглядит следующим образом:
По умолчанию вызов без явного указания SECURITY_IMPERSONATION_LEVEL (как в приведенном выше примере) выполняется с уровнем обезличивания SecurityAnonymous.
Если флаг SECURITY_SQOS_PRESENT установлен без дополнительного уровня имперсонации (IL) или с IL, установленным на SECURITY_IDENTIFICATION или SECURITY_ANONYMOUS, вы не сможете выдать себя за клиента.
Второй важный аспект, который необходимо проверить, это имя файла, он же параметр lpFileName, передаваемый CreateFile. Существует важное различие между вызовом localNamedPipe и вызовом удаленных NamedPipe.
Вызов localNamedPipe определяется расположением файла \\\.\pipe\<SomeName>.
Вызовы к localNamedPipe могут быть имперсонифицированы, только если явно установлен флаг SECURITY_SQOS_PRESENT с уровнем имперсонации выше SECURITY_IDENTIFICATION. Поэтому уязвимый вызов выглядит следующим образом:
Для ясности. Безопасный вызов localNamedPipe будет выглядеть следующим образом:
Этот последующий вызов безопасен даже без SECURITY_SQOS_PRESENT, поскольку вызывается localNamedPipe.
Удаленная NamedPipe с другой стороны, определяется именем lpFileName, начинающимся с имени хоста или IP, например: \\\ServerA.domain.local\pipe\<SomeName>.
Теперь наступает важный момент:
Когда флаг SECURITY_SQOS_PRESENT отсутствует и вызывается удаленная NamedPipe уровень имперсонации определяется привилегиями пользователя, управляющего сервером NamedPipe.
Это означает, что когда вы вызываете удаленный NamedPipe без флага SECURITY_SQOS_PRESENT, ваш атакующий пользователь, запускающий канал, должен обладать привилегией SeImpersonatePrivilege (SE_IMPERSONATE_NAME), чтобы выдать себя за клиента.
Если ваш пользователь не обладает этой привилегией, уровень имперсонации будет установлен на SecurityIdentification (что позволяет вам идентифицировать, но не выдавать себя за пользователя).
Но это также означает, что если ваш пользователь обладает привилегией SeEnableDelegationPrivilege (SE_ENABLE_DELEGATION_NAME), уровень имперсонации будет установлен на SecurityDelegation, и вы даже сможете аутентифицировать пользователя-жертву в других сетевых службах.
Важным выводом здесь является следующее:
Вы можете сделать удаленный вызов NamedPipe запущенной на той же машине, указав \\\127.0.0.1\pipe\<SomeName>.
Чтобы окончательно собрать все части воедино:
Если параметр SECURITY_SQOS_PRESENT не установлен, вы можете выдать себя за клиента, если у вас есть пользователь с привилегиями не ниже SE_IMPERSONATE_NAME, но для NamedPipe, работающих на той же машине, вам нужно вызвать их через \\\127.0.0.1\pipe\....
Если установлен SECURITY_SQOS_PRESENT, вы можете выдать себя за клиента, только если вместе с ним установлен уровень имперсонации выше SECURITY_IDENTIFICATION (независимо от того, локально или удаленно вы вызываете NamedPipe)
В документации Microsoft об уровнях авторизации говорится следующее:
Если namedPipe RPC или DDE-соединение является удаленным, флаги, переданные CreateFile для установки уровня обезличивания, игнорируются. В этом случае уровень обезличивания клиента определяется уровнями обезличивания, включенными сервером, которые задаются флагом учетной записи сервера в службе каталогов. Например, если на сервере разрешено делегирование, уровень обезличивания клиента также будет установлен на делегирование, даже если флаги, переданные CreateFile, указывают уровень обезличивания идентификации. [источник]
Имейте в виду, что технически это верно, но несколько вводит в заблуждение...
Точная версия такова: если при вызове удаленного namedPipe вы указываете CreateFile только флаги уровня имперсонации (и ничего больше), то они будут проигнорированы, но если вы указываете флаги имперсонации вместе с флагом SECURITY_SQOS_PRESENT, то они будут соблюдены.
Примеры:
Реализация
А вот такой результат:
При самостоятельной реализации этой функции возникают некоторые трудности:
Когда вы создаете процесс с помощью CreateProcessWithTokenW, вам необходимо RevertToSelf перед вызовом CreateProcessWithTokenW, иначе вы получите ошибку.
Когда вы хотите создать оконный процесс (что-то с всплывающим окном, например, calc.exe или cmd.exe), вам нужно предоставить клиенту доступ к вашему окну и рабочему столу. Пример реализации, позволяющей всем пользователям получить доступ к вашему Window и Desktop, можно найти здесь .
Условие гонки при создании экземпляра
Экземпляры именованных труб создаются и живут в глобальном "пространстве имен" (на самом деле технически пространства имен нет, но это помогает понять, что все NamedPipe живут под одной крышей) на диске устройства Name Pipe File System (NPFS). Более того, несколько NamedPipe с одинаковыми именами могут существовать под одной крышей. Что же произойдет, если приложение создаст NamedPipe, который уже существует? Если вы не установите правильные флаги, ничего не произойдет, то есть вы не получите ошибку и, что еще хуже, не получите клиентских соединений, поскольку экземпляры NamedPipe организованы в стеке FIFO (First In First Out).
Такая конструкция делает Named Pipes уязвимыми для уязвимостей состояния гонки при создании экземпляров.
Сценарий атаки
Сценарий атаки для использования такого состояния гонки выглядит следующим образом: Вы определили службу, программу или процедуру, которая создает именованный канал, используемый клиентскими приложениями, работающими в другом контексте безопасности (допустим, они работают под пользователем NT Service). Сервер создает NamePipe для связи с клиентским приложением (приложениями). Время от времени клиент подключается к NamePipe сервера - не редкость, если серверное приложение вызывает подключение клиентов после создания pipe-сервера. Вы выяснили, когда и как запускается сервер, а также имя создаваемой им pipe.
Теперь вы пишете программу, которая создает namedpipe с тем же именем в сценарии, где ваш экземпляр NamedPipe создается раньше, чем NamedPipe целевого сервера. Если NamedPipe сервера создана небезопасно, она не заметит, что NamedPipe с таким же именем уже существует, и вызовет подключение клиентов. Благодаря стеку FIFO клиенты подключатся к вам, и вы сможете читать или записывать их данные или попытаться выдать себя за клиента.
Необходимые условия
Для того чтобы эта атака сработала, вам нужен целевой сервер, который не проверяет, существует ли уже NamedPipe с таким же именем. Обычно сервер не имеет дополнительного кода для проверки вручную, существует ли уже NamedPipe с таким же именем - думая об этом, вы ожидаете получить ошибку, если имя вашей NamedPipe уже существует, верно? Но этого не происходит, потому что два экземпляра NamedPipe с одинаковым именем абсолютно валидны... по любой причине.
Но для борьбы с этой атакой Microsoft добавила флаг FILE_FLAG_FIRST_PIPE_INSTANCE, который можно указать при создании NamedPipe трубы через CreateNamedPipe. Когда этот флаг установлен, вызов create вернет значение INVALID_HANDLE_VALUE, что приведет к ошибке при последующем вызове ConnectNamedPipe.
Если ваш целевой сервер не указывает флаг FILE_FLAG_FIRST_PIPE_INSTANCE, то он, скорее всего, уязвим, однако есть еще один момент, о котором следует знать атакующей стороне. При создании NamedPipe через CreateNamedPipe существует параметр nMaxInstances, который определяет...:
Максимальное количество экземпляров, которые могут быть созданы для этой pipe. Первый экземпляр трубы может задавать это значение; CreateNamedPipe [источник]
Таким образом, если вы установите это значение в '1' (как в примере кода выше), вы уничтожите свой собственный вектор атаки. Чтобы использовать уязвимость состояния гонки при создании экземпляра, установите значение PIPE_UNLIMITED_INSTANCES.
Реализация
Все, что вам нужно сделать для эксплуатации, это создать NamedPipe с нужным именем в нужное время.
Мой пример реализации здесь может быть использован в качестве шаблона реализации. Закиньте его в вашу любимую IDE, задайте имя NamedPipe убедитесь, что NamedPipe создана с флагом PIPE_UNLIMITED_INSTANCES, и начинайте работать.
Соединения с pipe без ответа - это попытки соединения, предпринимаемые клиентами, которые - кто бы мог подумать - не увенчались успехом, следовательно, остались без ответа, потому что pipe, которую запрашивает клиент, не существует.
Потенциал эксплуатации здесь довольно ясен и прост: Если клиент хочет подключиться к несуществующей pipe, мы создаем pipe, к которой клиент может подключиться, и пытаемся манипулировать клиентом с помощью вредоносной связи или выдать себя за клиента, чтобы получить дополнительные привилегии.
Эту уязвимость иногда также называют избыточными соединениями pipe (но, на мой взгляд, это не самая лучшая терминология).
Главный вопрос здесь заключается в следующем: Как найти таких клиентов?
Моим первоначальным немедленным ответом было бы: Запустить Procmon и поискать неудачные системные вызовы CreateFile.
Но я проверил это и оказалось, что Procmon не выдает список таких вызовов для труб... возможно, это потому, что инструмент проверяет/слушает только файловые операции через драйвер NTFS, но я не изучал этот вопрос глубже (возможно, есть трюк/переключатель, о котором я не знал) - я сообщу, если наткнусь на ответ...
Другой вариант - Pipe Monitor из набора инструментов IO Ninja. Этот инструмент требует лицензии, но предлагает бесплатный пробный период, чтобы поиграть с ним. Pipe Monitor предлагает функциональность для проверки активности pipe в системе и поставляется с несколькими основными фильтрами для процессов, имен файлов и т.п. Поскольку вы хотите найти все процессы и все имена файлов, я отфильтровал их по '*', запустил программу и использовал функцию поиска для поиска "Cannot open":
Если вы знаете какой-либо другой способ сделать это, используя инструментарий с открытым исходным кодом, дайте мне знать (/ 0xcsandker)
Убийство pipe-серверов
Если вы не можете найти неотвеченные попытки соединения по pipe, но обнаружили интересного клиента, с которым вы хотели бы поговорить или выдать себя за него, другой вариант получить соединение клиента - убить его текущий сервер.
В разделе Условия гонки при создании экземпляра я описал, что вы можете иметь несколько NamedPipe с одинаковыми именами в одном и том же "пространстве имен".
Если ваш целевой сервер не установил параметр nMaxInstances в '1', вы можете создать NamedPipe-cервер с тем же именем и поставить себя в очередь на обслуживание клиентов. Вы не получите ни одного клиентского вызова, пока оригинальный NamedPipe-cервер обслуживает клиентов, поэтому идея этой атаки заключается в том, чтобы нарушить работу или убить оригинальный NamedPipe-cервер чтобы на его место встал ваш вредоносный сервер.
Когда дело доходит до уничтожения или разрушения оригинального NamedPipe-cервер я не могу помочь с какими-либо общими предпосылками или реализациями, потому что это всегда зависит от того, кто управляет целевым сервером, а также от ваших прав доступа и привилегий пользователя.
При анализе целевого сервера на предмет применения техники kill старайтесь мыслить нестандартно, здесь есть нечто большее, чем просто отправка сигнала выключения процессу, например, могут быть условия ошибки, которые заставляют сервер выключиться или перезапуститься (помните, что вы номер 2 в очереди - перезапуска может быть достаточно, чтобы занять место).
Также обратите внимание, что pipe-сервер - это просто экземпляр, работающий на виртуальном FILE_OBJECT, поэтому все именованные pipe-серверы будут завершены, как только количество ссылок на их handle достигнет 0. Например, handle открывается клиентом, подключающимся к нему. Поэтому сервер также можно убить, уничтожив все его хэндлы (конечно, вы что-то получите, только если клиенты вернутся к вам после потери соединения).
PeekNamedPipe
Могут быть сценарии, в которых вас интересуют данные, которыми обмениваются, а не манипуляции или выдача себя за клиентов pipe.
Благодаря тому, что все экземпляры именованных труб живут под одной крышей, ака. в одном глобальном "пространстве имен" ака. на одном виртуальном диске устройства NPFS (как уже кратко упоминалось ранее), нет системного барьера, который помешает вам подключиться к любому произвольному (СИСТЕМНОМУ или не СИСТЕМНОМУ) экземпляру именованной трубы и посмотреть на данные в трубе (технически "в трубе" означает в разделе общей памяти, выделенной сервером труб).
Предварительные условия
Как упоминалось в разделе Безопасность NamedPipe единственное средство, которое вы можете использовать для защиты NamedPipe - это использование дескриптора безопасности в качестве последнего параметра (lpSecurityAttributes) вызова CreateNamedPipe. И это все, что может помешать вам получить доступ к произвольному экземпляру именованной трубы. Поэтому все, что вам нужно проверить при поиске цели, это установлен ли этот параметр и защищен ли он для предотвращения несанкционированного доступа.
Реализация
Когда вы нашли подходящую цель, нужно помнить еще об одном моменте: Если вы читаете из NamedPipe с помощью ReadFile, вы удаляете данные из общей памяти сервера, и следующий, потенциально легитимный клиент, который попытается прочитать из pipe, не найдет никаких данных и, возможно, выдаст ошибку.
Но вы можете использовать функцию PeekNamedPipe для просмотра данных, не удаляя их из общей памяти.
Фрагмент реализации, основанный на моем примере кода, может выглядеть следующим образом:
Вот и все, если вы хотите продолжить копаться в именованных каналах, вот несколько хороших ссылок для начала:
www.blakewatts.com
github.com
Перевёл эту статью
Этот пост - начало серии постов о внутреннем устройстве и интересных деталях различных компонентов технологии Inter-Process-Communication (IPC) на базе Windows. Первоначально в этой серии будут рассмотрены следующие темы:
Именованные 'pipes':
- LPC
- ALPC
- RPC
- Window Messages
- DDE (который основан на Window Messages)
- Сокеты Windows
- Почтовые слоты
Хотя название может звучать немного странно, 'pipes' - это базовая и простая технология для обеспечения связи и обмена данными между двумя процессами, где термин 'pipes' просто описывает раздел общей памяти, используемый этими двумя процессами.
Чтобы с самого начала ,было правильное понимание, технология IPC, о которой мы говорим, называется " pipes ", и существует два типа pipe:
- Named Pipes
- Anonymous Pipes
Прежде чем мы погрузимся во внутреннее устройство Named Pipes пожалуйста, примите к сведению, что далее будет приведено несколько фрагментов кода, взятых из моего публичного примера реализации Named Pipes. Если вы почувствуете, что вам нужно больше контекста, перейдите в репозиторий кода и просмотрите общую картину.
Обмен сообщениями по именованным трубам
Итак, давайте разберемся с внутренним устройством Named Pipe. Если вы никогда раньше не слышали об Named Pipes представьте себе эту коммуникационную технологию как настоящую стальную трубу - у вас есть полый прут с двумя концами, и если вы крикнете что-то в один конец, слушатель услышит ваши слова на другом конце. Это все, что делает Named Pipe - она передает информацию с одного конца на другой.
Если вы являетесь пользователем Unix, вы наверняка уже использовали pipes (поскольку это не чисто Windows-технология), используя что-то вроде этого: cat file.txt | wc -l. Команда, которая выводит содержимое файла file.txt, но вместо вывода на STDOUT (который может быть окном вашего терминала) вывод перенаправляется ("передается") на вход второй команды wc -l, которая таким образом подсчитывает строки вашего файла. Это пример Anonymous Pipes.
Name Pipes в Windows так же легко понять, как и в приведенном выше примере. Чтобы мы могли использовать весь набор возможностей pipe мы отойдем от Anonymous Pipes и создадим сервер и клиент, которые будут общаться друг с другом.
Name Pipe - это просто объект, точнее, FILE_OBJECT, который управляется специальной файловой системой, Named Pipe File System (NPFS):
Когда вы создаете Named Pipe, назовем его 'fpipe', под капотом вы создаете FILE_OBJECT с заданным именем 'fpipe' (отсюда: named pipe) на специальном диске устройства под названием 'pipe'.
Немного практики
Named Pipe создается путем вызова функции WinAPI CreateNamedPipe, как показано ниже [источник]:
C++:
HANDLE serverPipe = CreateNamedPipe(
L"\\\\.\\pipe\\fpipe", // name of our pipe, must be in the form of \\.\pipe\<NAME>
PIPE_ACCESS_DUPLEX, // open mode, specifying a duplex pipe so server and client can send and receive data
PIPE_TYPE_MESSAGE, // MESSAGE mode to send/receive messages in discrete units (instead of a byte stream)
1, // number of instanced for this pipe, 1 is enough for our use case
2048, // output buffer size
2048, // input buffer size
0, // default timeout value, equal to 50 milliseconds
NULL // use default security attributes
);
На данный момент наиболее интересной частью этого вызова является \\\\.\\\pipe\\fpipe.
C++ требует экранирования слэшей, поэтому независимо от языка \\\.\pipe\fpipe. Ведущее '\.' относится к глобальному корневому каталогу вашей машины, где термин 'pipe' является символической ссылкой на устройство NamedPipe.
Поскольку объект Named Pipe является FILE_OBJECT, доступ к Named Pipe, которую мы только что создали, равносилен доступу к "обычному" файлу.
Поэтому подключиться к Named Pipe с клиента так же просто, как вызвать CreateFile [источник]:
C++:
HANDLE hPipeFile = CreateFile(L"\\\\127.0.0.1\\pipe\\fpipe", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
После подключения чтение из pipe требует только вызова ReadFile [источник]
C++:
ReadFile(hPipeFile, pReadBuf, MESSAGE_SIZE, pdwBytesRead, NULL);
Прежде чем вы сможете прочитать какие-то данные из pipe вы хотите, чтобы ваш сервер записал в нее какие-то данные (которые вы сможете прочитать). Это делается вызовом - кто бы мог подумать - WriteFile [источник]
C++:
WriteFile(serverPipe, message, messageLenght, &bytesWritten, NULL);
Но что на самом деле происходит, когда вы "пишете" в pipes?
Как только клиент подключается к серверному каналу, созданная вами 'pipe' больше не находится в состоянии прослушивания, и данные могут быть записаны в нее.
Пользовательский вызов WriteFile передается ядру, где вызывается NtWriteFile, который определяет все детали операции записи, например, какой объект устройства связан с данным файлом, должна ли операция записи быть синхронной, устанавливается пакет запроса ввода/вывода (IRP) и, в конечном итоге, NtWriteFile заботится о том, чтобы ваши данные были записаны в файл. В нашем случае указанные данные записываются не в реальный файл на диске, а в раздел общей памяти, на который ссылается файловый хэндл, возвращаемый из CreateNamedPipe.
Наконец - как уже упоминалось во введении - Named Pipe также можно использовать через сетевое соединение, пересекающее границы системы.
Для вызова удаленного сервера Named Pipe не требуется никаких дополнительных реализаций, просто убедитесь, что в вызове CreateFile указан IP или имя хоста (как в примере выше).
Давайте угадаем: какой сетевой протокол будет использоваться при вызове удаленного сервера? .... барабанная дробь ... абсолютно неудивительно, что это SMB.
С удаленным сервером устанавливается SMB-соединение, которое по умолчанию инициализируется переговорным запросом для определения протокола сетевой аутентификации. В отличие от других механизмов IPC, таких как RPC, вы, как разработчик сервера, не можете контролировать протокол сетевой аутентификации, поскольку он всегда согласовывается через SMB. Поскольку Kerberos является предпочтительной схемой аутентификации, начиная с Windows 2000, Kerberos будет согласован, если это возможно.
Примечание: С точки зрения клиента вы можете эффективно выбирать протокол аутентификации, выбирая подключение к имени хоста или IP. Из-за особенностей конструкции Kerberos он не очень хорошо работает с IP-адресами, поэтому если вы решили подключиться к IP-адресу, результатом согласования всегда будет NTLM(v2). В то время как при подключении к имени хоста вы, скорее всего, всегда будете использовать Kerberos.
Когда аутентификация завершена, действия, которые клиент и сервер хотят выполнить, снова являются классическими файловыми действиями, которые обрабатываются SMB так же, как и любые другие файловые операции, например, путем запуска запроса 'Create Request File', как показано ниже:
Режимы передачи данных
Named Pipe предлагают два основных режима передачи данных: режим байтов и режим сообщений.
В режиме байтов сообщения передаются в виде непрерывного потока байтов между клиентом и сервером. Это означает, что клиентское приложение и серверное приложение не знают точно, сколько байт считывается из pipe или записывается в нее в каждый момент времени. Поэтому запись на одной стороне не всегда приводит к чтению того же размера на другой. Это позволяет клиенту и серверу передавать данные, не заботясь об их размере.
В режиме сообщений клиент и сервер отправляют и получают данные дискретными блоками. Каждый раз, когда сообщение отправляется по pipe, оно должно быть прочитано как полное сообщение. Если вы считываете данные с pipe сервера в режиме сообщений, но ваш буфер чтения слишком мал, чтобы вместить все данные, то часть данных, которая помещается в ваш буфер, будет скопирована в него, остальные данные останутся в разделе общей памяти сервера, и вы получите ошибку 234 (0xEA, ERROR_MORE_DATA), указывающую на то, что необходимо получить больше данных.
Визуальное сравнение режимов сообщений показано ниже, взято из книги "Сетевое программирование для Microsoft Windows" (1999):
Перекрывающийся ввод-вывод, режим блокировки и буферы ввода-вывода
Перекрывающийся ввод-вывод, режим блокировки и буферы ввода-вывода не являются удивительно важными с точки зрения безопасности, но знание о том, что они существуют и что они означают, может помочь в понимании, общении, построении и отладке именованных труб. Поэтому я вкратце расскажу об этих понятиях.
Перекрывающийся ввод-вывод
Некоторые функции, связанные с Named Pipe, такие как ReadFile, WriteFile, TransactNamedPipe и ConnectNamedPipe, могут выполнять операции с трубами либо синхронно, то есть выполняющий поток ждет завершения операции, прежде чем продолжить, либо асинхронно, то есть выполняющий поток запускает действие и продолжает, не дожидаясь его завершения. Важно отметить, что асинхронные операции с pipe могут быть выполнены только на pipe (сервере), который допускает перекрывающийся ввод-вывод, путем установки FILE_FLAG_OVERLAPPED в вызове CreateNamedPipe.
Асинхронные вызовы могут быть выполнены либо путем указания структуры OVERLAPPED в качестве последнего параметра для каждого из вышеупомянутых "стандартных" действий с pipe таких как ReadFile, либо путем указания COMPLETION_ROUTINE в качестве последнего параметра для "расширенных" действий с pipe, таких как ReadFileEx. Первый метод, метод OVERLAPPED structure, основан на событиях, то есть должен быть создан объект события, который сигнализируется по завершении операции, в то время как метод COMPLETION_ROUTINE основан на обратном вызове, то есть исполняющему потоку передается процедура обратного вызова, которая ставится в очередь и выполняется по сигналу. Подробнее об этом можно узнать здесь с примером реализации от Microsoft здесь .
Режим блокировки
Поведение в режиме блокировки определяется при настройке сервера NamedPipe с помощью CreateNamedPipe путем использования (или отсутствия) флага в параметре dwPipeMode. Следующие два флага dwPipeMode определяют режим блокировки сервера:
PIPE_WAIT (по умолчанию): Режим блокировки включен. При использовании операций с NamedPipe таких как ReadFile на Pipe для которой включен режим блокировки, операция ожидает завершения. Это означает, что операция чтения на таком Pipe будет ждать, пока не появятся данные для чтения, а операция записи будет ждать, пока все данные не будут записаны. Это, конечно, может привести к тому, что в некоторых ситуациях операция будет ждать бесконечно долго.
PIPE_NOWAIT: Режим блокировки отключен. Именованные операции pipe, такие как ReadFile, возвращаются немедленно. Чтобы убедиться в том, что все данные прочитаны или записаны, нужны такие процедуры, как Overlapping I/O.
Буферы ввода-вывода
Под буферами ввода-вывода я имею в виду входные и выходные буферы сервера NamedPipe, которые вы создаете при вызове CreateNamedPipe, а точнее, размеры этих буферов в параметрах nInBufferSize и nOutBufferSize.
При выполнении операций чтения и записи ваш сервер NamedPipe использует нестраничную память (имеется в виду физическая память) для временного хранения данных, подлежащих чтению или записи. Злоумышленник, которому разрешено влиять на эти значения для созданного сервера, может злоупотребить ими, чтобы потенциально вызвать крах системы, выбрав большие буферы или задержать операции с Pipe, выбрав маленький буфер (например, 0):
Большие буферы: Поскольку буферы In-/Out являются нестраничными, сервер исчерпает память, если выбрать их слишком большими. Однако параметры nInBufferSize и nOutBufferSize не принимаются системой "вслепую". Верхний предел определяется константой, зависящей от системы; я не смог найти точной информации об этой константе (и не стал копаться в заголовках); в этом сообщении указано, что это ~4GB для системы x64 Windows7.
Маленькие буферы: Размер буфера 0 абсолютно допустим для nInBufferSize и nOutBufferSize. Если бы система строго выполняла то, что ей было сказано, вы бы не смогли ничего записать в pipe, потому что буфер размера 0 - это ... ну, несуществующий буфер. К счастью, система достаточно умна, чтобы понять, что вы просите минимальный размер буфера, и поэтому увеличит фактический размер буфера до размера, который она получит, но это имеет последствия для производительности. Размер буфера 0 означает, что каждый байт должен быть прочитан процессом на другой стороне pipe (и тем самым очищен буфер), прежде чем новые данные могут быть записаны в буфер. Это справедливо для обоих значений, nInBufferSize и nOutBufferSize. Буфер размером 0 может стать причиной задержек сервера.
Безопасность NamedPipe
И снова мы можем сделать эту главу о том, как установить и контролировать безопасность именованного трубопровода, довольно короткой, но важно знать, как это делается.
Единственное средство, которое вы можете включить, когда хотите защитить NamePipe, - это установка дескриптора безопасности для сервера NamePipe в качестве последнего параметра (lpSecurityAttributes) в вызове CreateNamedPipe. Установка этого дескриптора безопасности необязательна; дескриптор безопасности по умолчанию можно установить, указав NULL в параметре lpSecurityAttributes.
В документации Windows определено, что делает дескриптор безопасности по умолчанию для сервера NamedPipe:
ACLs в дескрипторе безопасности по умолчанию для NamedPipe предоставляют полный контроль учетной записи LocalSystem, администраторам и владельцу-создателю. Они также предоставляют доступ на чтение членам группы Everyone и учетной записи anonymous.
CreateNamedPipe > Paremter > lpSecurityAttributes [источник]
Таким образом, по умолчанию все могут читать с вашего сервера NamedPipe если вы не указали дескриптор безопасности, независимо от того, находится ли читающий клиент на той же машине или нет. Если вы подключаетесь к NamedPipe серверу без заданного дескриптора безопасности, но все равно получаете ошибку Access Denied Error (код ошибки: 5), убедитесь, что вы указали только доступ READ (обратите внимание, что в примере выше указан доступ READ и WRITE с GENERIC_READ | GENERIC_WRITE).
Для удаленных подключений еще раз обратите внимание - как описано в конце главы Named Pipe Messaging - что протокол сетевой аутентификации согласовывается между клиентом и сервером через протокол SMB. Не существует способа программно принудительно использовать более сильный протокол Kerberos (вы можете только отключить NTLM на хосте сервера).
Имперсонация
Имперсонация - это простая концепция, которая понадобится нам в следующем разделе для обсуждения векторов атак с использованием NamedPipe.
Если вы знакомы с имперсонацией, можете пропустить этот раздел; имперсонация не относится к NamedPipe.
Если вы еще не сталкивались с имперсонацией в среде Windows, позвольте мне вкратце рассказать вам об этой концепции:
Имперсонация - это способность потока выполняться в контексте безопасности, отличном от контекста безопасности процесса, которому принадлежит поток. Имперсонация обычно применяется в архитектуре клиент-сервер, где клиент подключается к серверу, а сервер может (при необходимости) выдать себя за клиента. Имперсонация позволяет серверу (потоку) выполнять действия от имени клиента, но в рамках прав доступа клиента.
Типичный сценарий - сервер хочет получить доступ к некоторым записям (например, в базе данных), но только клиент имеет право доступа к своим собственным записям. Сервер может ответить клиенту, попросив получить записи самостоятельно и передать их серверу, или сервер может использовать протокол авторизации, чтобы доказать, что клиент разрешил серверу доступ к записям, или - и это то, чем является имперсонация - клиент посылает серверу некоторую идентификационную информацию и позволяет серверу переключиться на роль клиента. Это похоже на то, как если бы клиент передал свои водительские права серверу вместе с разрешением использовать эти права для идентификации других сторон, например, привратника (или, более технически, сервера базы данных).
Идентификационная информация, такая как информация, указывающая, кем является клиент (например, SID), упаковывается в структуру, называемую контекстом безопасности. Эта структура глубоко встроена во внутреннее устройство операционной системы и является необходимым элементом информации для межпроцессного взаимодействия. Поэтому клиент не может выполнить IPC вызов без контекста безопасности, но ему нужен способ указать, что он позволяет серверу знать и делать с его идентификатором. Для контроля этого Microsoft создала так называемые уровни имперсонации.
Структура перечисления SECURITY_IMPERSONATION_LEVEL определяет четыре уровня имперсонации, которые определяют операции, которые сервер может выполнять в контексте клиента.
SECURITY_IMPERSONATION_LEVEL
SecurityAnonymous Сервер не может выдавать себя за клиента или идентифицировать его.
SecurityIdentification Сервер может получить идентификационные данные и привилегии клиента, но не может выдавать себя за него.
SecurityImpersonation Сервер может выдавать себя за контекст безопасности клиента в локальной системе.
SecurityDelegation Сервер может выдавать за клиента контекст безопасности на удаленных системах.
Для получения дополнительной справочной информации об имперсонации ознакомьтесь с документацией Microsoft по клиентской имперсонации.
Для получения некоторого контекста вокруг имперсонации ознакомьтесь с разделом Токены доступа и следующим разделом об имперсонации в моем посте об авторизации Windows.
Имперсонификация клиента NamedPipe
Итак, раз уж мы затронули эту тему, и если вам еще не совсем скучно. Давайте вкратце расскажем о том, что на самом деле происходит под капотом, если сервер выдает себя за клиента.
Если вас больше интересует, как это реализовать, вы найдете ответ в моем примере реализации
Шаг 1: Сервер ожидает входящего соединения от клиента и после этого вызывает функцию ImpersonateNamedPipeClient.
Шаг 2: Этот вызов приводит к вызову NtCreateEvent (для создания события обратного вызова) и NtFsControlFile, которая является функцией, выполняющей имперсонацию.
Шаг 3: NtFsControlFile - это функция общего назначения, действие которой задается аргументом, в данном случае FSCTL_PIPE_Impersonate.
Приведенное ниже описание основано на открытом исходном коде ReactOS, но я думаю, что оно справедливо как
Шаг 4: Далее по стеку вызовов вызывается NpCommonFileSystemControl, где FSCTL_PIPE_IMPERSONATE передается в качестве аргумента и используется в инструкции switch-case для определения того, что делать.
Шаг 5: NpCommonFileSystemControl вызывает NbAcquireExeclusiveVcb для блокировки объекта, а NpImpersonate вызывается, учитывая объект трубы сервера и IRP (I/O Request Object), выданный клиентом.
Шаг 6: NpImpersonate затем в свою очередь вызывает SeImpersonateClientEx с контекстом безопасности клиента, который был получен из IRP клиента, в качестве параметра.
Шаг 7: SeImpersonateClientEx в свою очередь вызывает PsImpersonateClient с объектом потока сервера и маркером безопасности клиента, который извлекается из контекста безопасности клиента.
Шаг 8: Контекст потока сервера затем изменяется на контекст безопасности клиента.
Шаг 9: Любое действие сервера и любая функция, которую сервер вызывает, находясь в контексте безопасности клиента, выполняются с идентификатором клиента и тем самым выдают себя за клиента.
Шаг 10: Если сервер закончил то, что собирался сделать, будучи клиентом, он вызывает команду RevertToSelf, чтобы вернуться в свой собственный, исходный контекст потока.
Поверхность атаки
Имперсонация клиента
Наконец-то мы заговорили о поверхности атаки. Самый важный вектор атаки, основанный на NamedPipe - это имперсонация.
К счастью, мы уже представили и поняли концепцию имперсонации в предыдущем разделе, поэтому мы можем сразу приступить к работе.
Сценарий атаки
Имперсонацией с помощью NamedPipe лучше всего злоупотреблять, когда у вас есть служба, программа или процедура, которая позволяет вам указать или контролировать доступ к файлу (неважно, разрешает ли она вам доступ на чтение или запись или и то, и другое). Благодаря тому, что NamedPipe по сути являются FILE_OBJECTs и работают с теми же функциями доступа, что и обычные файлы (ReadFile, WriteFile, CreateFile, ...), вы можете указать NamedPipe вместо обычного имени файла и заставить процесс вашей жертвы подключиться к NamedPipe под вашим контролем.
Предварительные условия
Есть два важных аспекта, которые необходимо проверить при попытке выдать себя за клиента.
Во-первых, необходимо проверить, как клиент реализует доступ к файлам, а точнее, указывает ли клиент флаг SECURITY_SQOS_PRESENT при вызове CreateFile?
Уязвимый вызов CreateFile выглядит следующим образом:
C++:
hFile = CreateFile(pipeName, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
В то время как безопасный вызов CreateFile выглядит следующим образом:
C++:
// calling with explicit SECURITY_IMPERSONATION_LEVEL
hFile = CreateFile(pipeName, GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION , NULL);
// calling without explicit SECURITY_IMPERSONATION_LEVEL
hFile = CreateFile(pipeName, GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT, NULL);
По умолчанию вызов без явного указания SECURITY_IMPERSONATION_LEVEL (как в приведенном выше примере) выполняется с уровнем обезличивания SecurityAnonymous.
Если флаг SECURITY_SQOS_PRESENT установлен без дополнительного уровня имперсонации (IL) или с IL, установленным на SECURITY_IDENTIFICATION или SECURITY_ANONYMOUS, вы не сможете выдать себя за клиента.
Второй важный аспект, который необходимо проверить, это имя файла, он же параметр lpFileName, передаваемый CreateFile. Существует важное различие между вызовом localNamedPipe и вызовом удаленных NamedPipe.
Вызов localNamedPipe определяется расположением файла \\\.\pipe\<SomeName>.
Вызовы к localNamedPipe могут быть имперсонифицированы, только если явно установлен флаг SECURITY_SQOS_PRESENT с уровнем имперсонации выше SECURITY_IDENTIFICATION. Поэтому уязвимый вызов выглядит следующим образом:
C++:
hFile = CreateFile(L"\\.\pipe\fpipe", GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT | SECURITY_IMPERSONATION, NULL);
Для ясности. Безопасный вызов localNamedPipe будет выглядеть следующим образом:
C++:
hFile = CreateFile(L"\\.\pipe\fpipe", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
Этот последующий вызов безопасен даже без SECURITY_SQOS_PRESENT, поскольку вызывается localNamedPipe.
Удаленная NamedPipe с другой стороны, определяется именем lpFileName, начинающимся с имени хоста или IP, например: \\\ServerA.domain.local\pipe\<SomeName>.
Теперь наступает важный момент:
Когда флаг SECURITY_SQOS_PRESENT отсутствует и вызывается удаленная NamedPipe уровень имперсонации определяется привилегиями пользователя, управляющего сервером NamedPipe.
Это означает, что когда вы вызываете удаленный NamedPipe без флага SECURITY_SQOS_PRESENT, ваш атакующий пользователь, запускающий канал, должен обладать привилегией SeImpersonatePrivilege (SE_IMPERSONATE_NAME), чтобы выдать себя за клиента.
Если ваш пользователь не обладает этой привилегией, уровень имперсонации будет установлен на SecurityIdentification (что позволяет вам идентифицировать, но не выдавать себя за пользователя).
Но это также означает, что если ваш пользователь обладает привилегией SeEnableDelegationPrivilege (SE_ENABLE_DELEGATION_NAME), уровень имперсонации будет установлен на SecurityDelegation, и вы даже сможете аутентифицировать пользователя-жертву в других сетевых службах.
Важным выводом здесь является следующее:
Вы можете сделать удаленный вызов NamedPipe запущенной на той же машине, указав \\\127.0.0.1\pipe\<SomeName>.
Чтобы окончательно собрать все части воедино:
Если параметр SECURITY_SQOS_PRESENT не установлен, вы можете выдать себя за клиента, если у вас есть пользователь с привилегиями не ниже SE_IMPERSONATE_NAME, но для NamedPipe, работающих на той же машине, вам нужно вызвать их через \\\127.0.0.1\pipe\....
Если установлен SECURITY_SQOS_PRESENT, вы можете выдать себя за клиента, только если вместе с ним установлен уровень имперсонации выше SECURITY_IDENTIFICATION (независимо от того, локально или удаленно вы вызываете NamedPipe)
В документации Microsoft об уровнях авторизации говорится следующее:
Если namedPipe RPC или DDE-соединение является удаленным, флаги, переданные CreateFile для установки уровня обезличивания, игнорируются. В этом случае уровень обезличивания клиента определяется уровнями обезличивания, включенными сервером, которые задаются флагом учетной записи сервера в службе каталогов. Например, если на сервере разрешено делегирование, уровень обезличивания клиента также будет установлен на делегирование, даже если флаги, переданные CreateFile, указывают уровень обезличивания идентификации. [источник]
Имейте в виду, что технически это верно, но несколько вводит в заблуждение...
Точная версия такова: если при вызове удаленного namedPipe вы указываете CreateFile только флаги уровня имперсонации (и ничего больше), то они будут проигнорированы, но если вы указываете флаги имперсонации вместе с флагом SECURITY_SQOS_PRESENT, то они будут соблюдены.
Примеры:
C++:
/ In the below call the SECURITY_IDENTIFICATION flag will be respected by the remote server
hFile = CreateFile(L"\\ServerA.domain.local", GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION, NULL);
/* --> The server will obtain a SECURITY_IDENTIFICATION token */
// In this call the SECURITY_IDENTIFICATION flag will be ignored
hFile = CreateFile(L"\\ServerA.domain.local", GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_IDENTIFICATION, NULL);
/* --> The server will obtain a token based on the privileges of the user running the server.
A user holding SeImpersonatePrivilege will get an SECURITY_IMPERSONATION token */
// In this call the Impersonation Level will default to SECURITY_ANONYMOUS and will be respected
hFile = CreateFile(L"\\ServerA.domain.local", GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT, NULL);
/* --> The server will obtain a SECURITY_ANONYMOUS token. A call to OpenThreadToken will result in error 1347 (0x543, ERROR_CANT_OPEN_ANONYMOUS)*/
Реализация
C++:
// Create a server named pipe
serverPipe = CreateNamedPipe(
pipeName, // name of our pipe, must be in the form of \\.\pipe\<NAME>
PIPE_ACCESS_DUPLEX, // The rest of the parameters don't really matter
PIPE_TYPE_MESSAGE, // as all you want is impersonate the client...
1, //
2048, //
2048, //
0, //
NULL // This should ne NULL so every client can connect
);
// wait for pipe connections
BOOL bPipeConnected = ConnectNamedPipe(serverPipe, NULL);
// Impersonate client
BOOL bImpersonated = ImpersonateNamedPipeClient(serverPipe);
// if successful open Thread token - your current thread token is now the client's token
BOOL bSuccess = OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &hToken);
// now you got the client token saved in hToken and you can safeyl revert back to self
bSuccess = RevertToSelf();
// Now duplicate the client's token to get a Primary token
bSuccess = DuplicateTokenEx(hToken,
TOKEN_ALL_ACCESS,
NULL,
SecurityImpersonation,
TokenPrimary,
&hDuppedToken
);
// If that succeeds you got a Primary token as hDuppedToken and you can create a proccess with that token
CreateProcessWithTokenW(hDuppedToken, LOGON_WITH_PROFILE, command, NULL, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
А вот такой результат:
При самостоятельной реализации этой функции возникают некоторые трудности:
Когда вы создаете процесс с помощью CreateProcessWithTokenW, вам необходимо RevertToSelf перед вызовом CreateProcessWithTokenW, иначе вы получите ошибку.
Когда вы хотите создать оконный процесс (что-то с всплывающим окном, например, calc.exe или cmd.exe), вам нужно предоставить клиенту доступ к вашему окну и рабочему столу. Пример реализации, позволяющей всем пользователям получить доступ к вашему Window и Desktop, можно найти здесь .
Условие гонки при создании экземпляра
Экземпляры именованных труб создаются и живут в глобальном "пространстве имен" (на самом деле технически пространства имен нет, но это помогает понять, что все NamedPipe живут под одной крышей) на диске устройства Name Pipe File System (NPFS). Более того, несколько NamedPipe с одинаковыми именами могут существовать под одной крышей. Что же произойдет, если приложение создаст NamedPipe, который уже существует? Если вы не установите правильные флаги, ничего не произойдет, то есть вы не получите ошибку и, что еще хуже, не получите клиентских соединений, поскольку экземпляры NamedPipe организованы в стеке FIFO (First In First Out).
Такая конструкция делает Named Pipes уязвимыми для уязвимостей состояния гонки при создании экземпляров.
Сценарий атаки
Сценарий атаки для использования такого состояния гонки выглядит следующим образом: Вы определили службу, программу или процедуру, которая создает именованный канал, используемый клиентскими приложениями, работающими в другом контексте безопасности (допустим, они работают под пользователем NT Service). Сервер создает NamePipe для связи с клиентским приложением (приложениями). Время от времени клиент подключается к NamePipe сервера - не редкость, если серверное приложение вызывает подключение клиентов после создания pipe-сервера. Вы выяснили, когда и как запускается сервер, а также имя создаваемой им pipe.
Теперь вы пишете программу, которая создает namedpipe с тем же именем в сценарии, где ваш экземпляр NamedPipe создается раньше, чем NamedPipe целевого сервера. Если NamedPipe сервера создана небезопасно, она не заметит, что NamedPipe с таким же именем уже существует, и вызовет подключение клиентов. Благодаря стеку FIFO клиенты подключатся к вам, и вы сможете читать или записывать их данные или попытаться выдать себя за клиента.
Необходимые условия
Для того чтобы эта атака сработала, вам нужен целевой сервер, который не проверяет, существует ли уже NamedPipe с таким же именем. Обычно сервер не имеет дополнительного кода для проверки вручную, существует ли уже NamedPipe с таким же именем - думая об этом, вы ожидаете получить ошибку, если имя вашей NamedPipe уже существует, верно? Но этого не происходит, потому что два экземпляра NamedPipe с одинаковым именем абсолютно валидны... по любой причине.
Но для борьбы с этой атакой Microsoft добавила флаг FILE_FLAG_FIRST_PIPE_INSTANCE, который можно указать при создании NamedPipe трубы через CreateNamedPipe. Когда этот флаг установлен, вызов create вернет значение INVALID_HANDLE_VALUE, что приведет к ошибке при последующем вызове ConnectNamedPipe.
Если ваш целевой сервер не указывает флаг FILE_FLAG_FIRST_PIPE_INSTANCE, то он, скорее всего, уязвим, однако есть еще один момент, о котором следует знать атакующей стороне. При создании NamedPipe через CreateNamedPipe существует параметр nMaxInstances, который определяет...:
Максимальное количество экземпляров, которые могут быть созданы для этой pipe. Первый экземпляр трубы может задавать это значение; CreateNamedPipe [источник]
Таким образом, если вы установите это значение в '1' (как в примере кода выше), вы уничтожите свой собственный вектор атаки. Чтобы использовать уязвимость состояния гонки при создании экземпляра, установите значение PIPE_UNLIMITED_INSTANCES.
Реализация
Все, что вам нужно сделать для эксплуатации, это создать NamedPipe с нужным именем в нужное время.
Мой пример реализации здесь может быть использован в качестве шаблона реализации. Закиньте его в вашу любимую IDE, задайте имя NamedPipe убедитесь, что NamedPipe создана с флагом PIPE_UNLIMITED_INSTANCES, и начинайте работать.
Instance Creation Special Flavors
Unanswered Pipe Connections
Соединения с pipe без ответа - это попытки соединения, предпринимаемые клиентами, которые - кто бы мог подумать - не увенчались успехом, следовательно, остались без ответа, потому что pipe, которую запрашивает клиент, не существует.
Потенциал эксплуатации здесь довольно ясен и прост: Если клиент хочет подключиться к несуществующей pipe, мы создаем pipe, к которой клиент может подключиться, и пытаемся манипулировать клиентом с помощью вредоносной связи или выдать себя за клиента, чтобы получить дополнительные привилегии.
Эту уязвимость иногда также называют избыточными соединениями pipe (но, на мой взгляд, это не самая лучшая терминология).
Главный вопрос здесь заключается в следующем: Как найти таких клиентов?
Моим первоначальным немедленным ответом было бы: Запустить Procmon и поискать неудачные системные вызовы CreateFile.
Но я проверил это и оказалось, что Procmon не выдает список таких вызовов для труб... возможно, это потому, что инструмент проверяет/слушает только файловые операции через драйвер NTFS, но я не изучал этот вопрос глубже (возможно, есть трюк/переключатель, о котором я не знал) - я сообщу, если наткнусь на ответ...
Другой вариант - Pipe Monitor из набора инструментов IO Ninja. Этот инструмент требует лицензии, но предлагает бесплатный пробный период, чтобы поиграть с ним. Pipe Monitor предлагает функциональность для проверки активности pipe в системе и поставляется с несколькими основными фильтрами для процессов, имен файлов и т.п. Поскольку вы хотите найти все процессы и все имена файлов, я отфильтровал их по '*', запустил программу и использовал функцию поиска для поиска "Cannot open":
Если вы знаете какой-либо другой способ сделать это, используя инструментарий с открытым исходным кодом, дайте мне знать (/ 0xcsandker)
Убийство pipe-серверов
Если вы не можете найти неотвеченные попытки соединения по pipe, но обнаружили интересного клиента, с которым вы хотели бы поговорить или выдать себя за него, другой вариант получить соединение клиента - убить его текущий сервер.
В разделе Условия гонки при создании экземпляра я описал, что вы можете иметь несколько NamedPipe с одинаковыми именами в одном и том же "пространстве имен".
Если ваш целевой сервер не установил параметр nMaxInstances в '1', вы можете создать NamedPipe-cервер с тем же именем и поставить себя в очередь на обслуживание клиентов. Вы не получите ни одного клиентского вызова, пока оригинальный NamedPipe-cервер обслуживает клиентов, поэтому идея этой атаки заключается в том, чтобы нарушить работу или убить оригинальный NamedPipe-cервер чтобы на его место встал ваш вредоносный сервер.
Когда дело доходит до уничтожения или разрушения оригинального NamedPipe-cервер я не могу помочь с какими-либо общими предпосылками или реализациями, потому что это всегда зависит от того, кто управляет целевым сервером, а также от ваших прав доступа и привилегий пользователя.
При анализе целевого сервера на предмет применения техники kill старайтесь мыслить нестандартно, здесь есть нечто большее, чем просто отправка сигнала выключения процессу, например, могут быть условия ошибки, которые заставляют сервер выключиться или перезапуститься (помните, что вы номер 2 в очереди - перезапуска может быть достаточно, чтобы занять место).
Также обратите внимание, что pipe-сервер - это просто экземпляр, работающий на виртуальном FILE_OBJECT, поэтому все именованные pipe-серверы будут завершены, как только количество ссылок на их handle достигнет 0. Например, handle открывается клиентом, подключающимся к нему. Поэтому сервер также можно убить, уничтожив все его хэндлы (конечно, вы что-то получите, только если клиенты вернутся к вам после потери соединения).
PeekNamedPipe
Могут быть сценарии, в которых вас интересуют данные, которыми обмениваются, а не манипуляции или выдача себя за клиентов pipe.
Благодаря тому, что все экземпляры именованных труб живут под одной крышей, ака. в одном глобальном "пространстве имен" ака. на одном виртуальном диске устройства NPFS (как уже кратко упоминалось ранее), нет системного барьера, который помешает вам подключиться к любому произвольному (СИСТЕМНОМУ или не СИСТЕМНОМУ) экземпляру именованной трубы и посмотреть на данные в трубе (технически "в трубе" означает в разделе общей памяти, выделенной сервером труб).
Предварительные условия
Как упоминалось в разделе Безопасность NamedPipe единственное средство, которое вы можете использовать для защиты NamedPipe - это использование дескриптора безопасности в качестве последнего параметра (lpSecurityAttributes) вызова CreateNamedPipe. И это все, что может помешать вам получить доступ к произвольному экземпляру именованной трубы. Поэтому все, что вам нужно проверить при поиске цели, это установлен ли этот параметр и защищен ли он для предотвращения несанкционированного доступа.
Реализация
Когда вы нашли подходящую цель, нужно помнить еще об одном моменте: Если вы читаете из NamedPipe с помощью ReadFile, вы удаляете данные из общей памяти сервера, и следующий, потенциально легитимный клиент, который попытается прочитать из pipe, не найдет никаких данных и, возможно, выдаст ошибку.
Но вы можете использовать функцию PeekNamedPipe для просмотра данных, не удаляя их из общей памяти.
Фрагмент реализации, основанный на моем примере кода, может выглядеть следующим образом:
C++:
// all the vars you need
const int MESSAGE_SIZE = 512;
BOOL bSuccess;
LPCWSTR pipeName = L"\\\\.\\pipe\\fpipe";
HANDLE hFile = NULL;
LPWSTR pReadBuf[MESSAGE_SIZE] = { 0 };
LPDWORD pdwBytesRead = { 0 };
LPDWORD pTotalBytesAvail = { 0 };
LPDWORD pBytesLeftThisMessage = { 0 };
// connect to named pipe
hFile = CreateFile(pipeName, GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT | SECURITY_ANONYMOUS, NULL);
// sneak peek data
bSuccess = PeekNamedPipe(
hFile,
pReadBuf,
MESSAGE_SIZE,
pdwBytesRead,
pTotalBytesAvail,
pBytesLeftThisMessage
);
Вот и все, если вы хотите продолжить копаться в именованных каналах, вот несколько хороших ссылок для начала:
Pipes (Interprocess Communications) - Win32 apps
How to create, manage, and use pipes. A pipe is a section of shared memory that processes use for communication. The process that creates a pipe is the pipe server. A process that connects to a pipe is a pipe client.
docs.microsoft.com
Discovering and Exploiting Named Pipe Security Flaws for Fun and Profit
InterProcessCommunication-Samples/NamedPipes/CPP-NamedPipe-Basic-Client-Server at master · csandker/InterProcessCommunication-Samples
Some Code Samples for Windows based Inter-Process-Communication (IPC) - csandker/InterProcessCommunication-Samples
Перевёл эту статью