Автор: 0xcsandker
Оригинальная статья тут.
Кратко о LPC
Механизм локального вызова процедур был представлен в оригинальном ядре Windows NT в 1993-94 годах в качестве синхронного средства межпроцессного взаимодействия. Его синхронная природа означала, что клиенты/серверы должны были ждать отправки сообщения и выполнения действий, прежде чем выполнение могло быть продолжено. Это был один из основных недостатков, который был призван заменить ALPC, и причина, по которой ALPC некоторые называют асинхронным LPC.
О ALPC стало известно в Windows Vista, и, по крайней мере, начиная с Windows 7, LPC был полностью удален из ядра NT. Чтобы не ломать устаревшие приложения и обеспечить обратную совместимость, которой так славится Microsoft, функция, используемая для создания порта LPC, была сохранена, но вызов функции был перенаправлен на создание не LPC, а ALPC порта.
Поскольку LPC практически отсутствует после Windows 7, этот пост будет посвящен только ALPC, так что давайте вернемся к нему.
Но если вам, как и мне, нравится читать старую документацию о том, как все начиналось и как все работало раньше, вот статья, в которой подробно рассказывается о том, как LPC работал в Windows NT 3.5: http: //web.archive.org/web/20090220111555/http://www.windowsitlibrary.com/Content/356/08/1.html
Назад к ALPC
ALPC — это быстрое, очень мощное и очень широко используемое в ОС Windows (внутренне) средство межпроцессного взаимодействия, но оно не предназначено для использования разработчиками, поскольку для Microsoft ALPC — это внутреннее средство IPC, что означает, что ALPC недокументирован и используется только в качестве базовой технологии транспортировки для других, задокументированных и предназначенных для использования разработчиками протоколов транспортировки сообщений, например RPC.
Тот факт, что ALPC не задокументирован (Microsoft), однако, не означает, что ALPC — это полный черный ящик, поскольку умные люди, такие как Алекс Ионеску , как он работает и какие компоненты у него есть. Но на самом означает , что вы не должны полагаться на какое-либо поведение ALPC для какого-либо долгосрочного производственного использования, и, более того, вам действительно не следует использовать ALPC напрямую для создания программного обеспечения, поскольку существует множество неочевидных ловушек, которые могут вызвать проблемы с безопасностью или стабильностью.
Если вы запустите WinObj из Sysinternals Suite , вы обнаружите, что в каждой ОС Windows работает много портов ALPC, некоторые из них можно найти в корневом пути, как показано ниже:
… но большинство портов ALPC размещены по пути «Управление RPC» (помните, что RPC использует ALPC под капотом):
Чтобы начать работу с ALPC-связью, сервер открывает порт ALPC, к которому могут подключаться клиенты, который называется портом подключения ALPC , однако это не единственный порт ALPC, который создается во время потока связи ALPC (как вы увидим в следующей главе). Еще два порта ALPC создаются для клиента и сервера для передачи сообщений.
Итак, первое, на что следует обратить внимание, это:
Несмотря на то, что всего в ALPC-коммуникациях используется 3 порта ALPC, и все они называются по-разному (например, «Порты подключения ALPC»), существует только один объект ядра порта ALPC, в котором все три порта используются в Связь ALPC, создание экземпляра. Скелет этого объекта ядра ALPC выглядит следующим образом:
Как видно выше, объект ядра ALPC — довольно сложный объект ядра, ссылающийся на различные другие типы объектов. Это делает его интересной целью для исследований, но также оставляет хороший запас для ошибок и/или пропущенных путей атаки.
После того, как порты связи сервера и клиента установлены, обе стороны могут отправлять сообщения друг другу, используя единую функцию NtAlpcSendWaitReceivePortподвергается воздействию ntdll.dll .
Название этой функции звучит как три вещи сразу — Send, Wait и Receive — и это именно то, что есть. Сервер и клиент используют эту единственную функцию для ожидания сообщений, отправки и получения сообщений через свой порт ALPC. Это звучит ненужно сложно, и я не могу точно сказать, почему он был построен таким образом, но вот мое предположение: помните, что ALPC был создан как быстрое средство связи только для внутреннего использования, а канал связи был построен вокруг одного объект ядра (порт ALPC). Использование этой трехсторонней функции позволяет выполнять несколько операций, например отправку и получение сообщения, в одном вызове и, таким образом, экономит время и уменьшает количество переключений между пользователем и ядром. Кроме того, эта функция выступает в качестве единого входа в процесс обмена сообщениями и, следовательно, упрощает изменение кода и оптимизацию (связь ALPC используется во многих различных компонентах ОС, от драйверов ядра до пользовательских приложений с графическим интерфейсом, разработанных разными внутренними командами). Наконец, ALPC задуман как внутренний механизм IPC, поэтому Microsoft не нужно проектировать его в первую очередь для пользователя или стороннего разработчика.
В рамках этой единственной функции вы также указываете, какое сообщение вы хотите отправить (существуют разные типы с разными последствиями, мы вернемся к этому позже ) и какие другие атрибуты вы хотите отправить вместе с вашим сообщением (опять же, мы перейти к вещам, которые вы можете отправить вместе с сообщением, позже в главе Атрибуты сообщения ALPC ).
Пока это звучит довольно прямолинейно: сервер открывает порт, клиент подключается к нему, оба получают дескриптор коммуникационного порта и отправляют сообщения через единую функцию. NtAlpcSendWaitReceivePort… легкий.
Мы будем на высоком уровне, это так просто, но вы наверняка пришли сюда за подробностями, а в заголовке поста было сказано «внутренности», так что давайте пристегнемся и посмотрим поближе:
…подождите… Почему сервер отправляет/принимает данные через порт соединения а не связи , если у него есть выделенный порт связи?… Это была одна из многих вещей, которые озадачили меня в ALPC, и вместо того, чтобы тяжелая работа заднего хода, чтобы понять это самостоятельно, я сжульничал, связался с Алексом Ионеску и просто спросил эксперта. Я поместил ответ в Приложение А в конце этого поста, так как я не хочу слишком далеко уходить от потока сообщений в этот момент… извините за вешалку…
В любом случае, оглядываясь назад на поток сообщений сверху, мы можем понять, что клиент и сервер используют вызовы различных функций для создания портов ALPC, а затем отправляют и получают сообщения через единую функцию. NtAlpcSendWaitReceivePort. Хотя он содержит достаточный объем информации о потоке сообщений, важно всегда помнить, что сервер и клиент не имеют прямого однорангового соединения, а вместо этого направляют все сообщения через ядро, которое отвечает за размещение сообщений в очереди сообщений, уведомляя каждую сторону о полученных сообщениях и другие вещи, такие как проверка сообщений и атрибутов сообщений. Чтобы представить это в перспективе, я добавил несколько вызовов ядра на эту картинку:
Я должен признать, что на первый взгляд эта диаграмма не очень интуитивна, но я обещаю, что в процессе все прояснится, потерпите меня.
Чтобы получить более полное представление о том, как ALPC выглядит внутри, нам нужно немного глубже погрузиться в части реализации сообщений ALPC, которые я расскажу в следующем разделе.
В простом старом C++ мы можем определить сообщение ALPC с помощью следующих двух структур:
Чтобы отправить сообщение, нам нужно сделать следующее:
Этот фрагмент кода отправит сообщение ALPC с текстом «Hello World!» на сервер, к которому мы подключились. Мы указали, что сообщение должно быть синхронным сообщением с ALPC_MSGFLG_SYNC_REQUESTфлаг, который означает, что этот вызов будет ожидать (блокироваться), пока сообщение не будет получено на коммуникационном порту клиента.
Конечно, нам не нужно ждать, пока придет новое сообщение, а использовать оставшееся до этого время для других задач (помните, что ALPC создавался как асинхронный, быстрый и эффективный). Для облегчения этого ALPC предоставляет три различных типа сообщений:
Еще раз у вас есть 3 варианта:
На данный момент я не собираюсь углубляться в параметры синхронизации сообщений и различные очереди — мне нужно где-то сделать вырез — однако, если кто-то заинтересован в поиске ошибок в этих областях кода, я настоятельно рекомендую посмотреть в главу 8 удивительного внутреннего устройства Windows, часть 2, 7-е издание . Я многому научился из этой книги и не могу не похвалить ее!
Наконец, что касается деталей обмена сообщениями в ALPC, есть последний момент, который еще не был подробно описан, а именно вопрос о том , как сообщение передается от клиента к серверу. Было упомянуто, какие сообщения можно отправлять, как выглядит структура сообщения, какой существует механизм синхронизации и остановки сообщений, но до сих пор не было подробно описано, как сообщение передается от одного процесса к другому.
У вас есть два варианта для этого:
Этот «механизм двойного буфера» использовался во фрагменте кода выше. Оглядываясь назад, буфер сообщения для отправленного и полученного сообщения был неявно выделен с помощью первых трех строк кода:
Затем этот буфер сообщений был передан ядру при вызове NtAlpcSendWaitReceivePort, который копирует буфер отправки в буфер приема на другой стороне.
Мы также могли бы покопаться в ядре, чтобы выяснить, как на самом деле выглядит сообщение ALPC (отправляемое через буферы сообщений). Реверс NtAlpcSendWaitReceivePortприводит нас к функции ядра AlpcpReceiveMessage, который в конечном итоге вызывает — для нашего пути кода — в AlpcpReadMessageData, где происходит копирование буфера.
Примечание: если вас интересуют все детали реверсирования, которые я здесь не упомянул, ознакомьтесь с моим последующим постом: Отладка и реверсирование ALPC.
В конце этого пути вы найдете простой RtlCopyMemory , который является просто макросом для memcpy , который копирует кучу байтов из одного пространства памяти в другое — это не так красиво, как можно было бы ожидать, но это то, что это ¯\ (ツ) /¯.
Чтобы увидеть это в действии, я поставил точку останова в AlpcpReadMessageData показанная выше функция для моего серверного процесса ALPC. Точка останова срабатывает, когда мой клиент ALPC подключается и отправляет начальное сообщение на сервер. Сообщение, которое отправляет клиент, это: Hello Server. Аннотированный вывод отладки показан ниже:
Эти экраны отладки показывают, как выглядит сообщение ALPC, отправленное через буфер сообщений… просто байты в памяти процесса.
Также обратите внимание, что приведенные выше экраны являются визуальным представлением «механизма двойного буфера» на втором этапе копирования буфера, когда сообщение копируется из пространства памяти ядра в пространство памяти процесса получателя. Действие копирования из отправителя в пространство ядра не отслеживалось, поскольку точка останова была установлена только для процесса-получателя.
При отправке и получении сообщений через NtAlpcSendWaitReceivePort, клиент и сервер могут указать набор атрибутов, которые они хотели бы отправлять и/или получать. Этот набор атрибутов, которые нужно отправить, и набор атрибутов, которые нужно получить, передаются в NtAlpcSendWaitReceivePortв двух дополнительных параметрах, показанных ниже:
Идея здесь заключается в том, что как отправитель вы можете передать дополнительную информацию получателю, а получатель на другом конце может указать, какой набор атрибутов он хотел бы получить, а это означает, что не обязательно вся дополнительная информация, которая была отправлена, также доступна получателю. получатель.
Следующие атрибуты сообщения могут быть отправлены и/или получены:
Атрибуты сообщения, как они инициализируются и отправляются, были еще одной вещью, которая озадачила меня при кодировании примера сервера и клиента ALPC. Так что вы не столкнетесь с теми же проблемами, которые были у меня здесь, это секрет, который я узнал об атрибутах сообщения ALPC:
Для начала необходимо знать, что структура атрибутов сообщения ALPC следующая:
Глядя на это, я сначала подумал, что вы вызываете функцию AlpcInitializeMessageAttribute, даете ей ссылку на приведенную выше структуру и флаг для атрибута сообщения, которое вы хотите отправить (на все атрибуты ссылается значение флага, вот список из моего кода ) и ядро затем устанавливает все это для вас. Затем вы помещаете указанную структуру в NtAlpcSendWaitReceivePort , повторяете процесс для каждого сообщения, которое хотите отправить, и все готово.
Это не так и кажется неправильным на нескольких уровнях. Только после того, как я нашел этот пост в твиттере от 2020 года и Алекса на SyScan'14 (я пересматривал его не менее 20 раз во время своего исследования), я пришел к тому, что я сейчас считаю правильным путем. Позвольте мне сначала выявить ошибки в моих первоначальных убеждениях, прежде чем объединять правильный курс действий:
Чтобы объединить это в полезные знания, вот как работают атрибуты отправки сообщения (в моем текущем понимании):
До сих пор мы представили различные компоненты ALPC, чтобы описать, как работает система обмена сообщениями ALPC и как выглядит сообщение ALPC. Позвольте мне завершить эту главу, рассмотрев некоторые из этих компонентов в перспективе. Приведенное выше описание и структура сообщения ALPC описывает, как сообщение ALPC выглядит для отправителя и получателя, но следует помнить, что ядро добавляет к этому сообщению гораздо больше информации — фактически оно берет предоставленные части и помещает их в гораздо большая структура сообщений ядра - как вы можете видеть ниже:
Таким образом, сообщение здесь таково: мы достигли хорошего понимания, но под капотом есть гораздо больше , чего мы не коснулись.
Оглядываясь назад на Поток сообщений ALPC» , мы можем вспомнить, что для обеспечения связи ALPC сервер должен открыть порт ALPC (соединение), дождаться входящих сообщений, а затем принять или отклонить эти сообщения. Хотя порт ALPC является защищаемым объектом ядра и может быть создан с помощью дескриптора безопасности , который определяет, кто может получить к нему доступ и подключиться к нему, в большинстве случаев процесс создания сервера ALPC не может (или не хочет) ограничивать доступ на основе вызываемого абонента SID . Если вы не можете (или хотите) ограничить доступ к вашему порту ALPC с помощью SID , единственный вариант, который у вас есть, — это разрешить всем подключаться к вашему порту и принимать/отклонять решение после того, как клиент подключится и отправит вам сообщение. Это, в свою очередь, означает, что многие встроенные серверы ALPC позволяют всем подключаться и отправлять сообщения на сервер. Даже если сервер сразу отклоняет клиента, отправки начального сообщения и некоторых атрибутов сообщения вместе с этим сообщением может быть достаточно для использования уязвимости.
Благодаря этой коммуникационной архитектуре и повсеместному распространению ALPC использование ALPC также является интересным способом избежать песочницы.
Обычно мне приходят в голову три способа идентифицировать такие процессы:
Найти объекты порта ALPC
Мы уже видели самый простой способ идентификации объектов порта ALPC в начале этого сообщения, который заключается в запуске WinObj и обнаружении объектов ALPC по столбцу «Тип». WinObj не может дать нам более подробной информации, поэтому мы обращаемся к WinDbg , чтобы проверить этот объект порта ALPC:
В приведенных выше командах мы использовали команду Windbg !object для запроса к диспетчеру именованного объекта по указанному пути. Это неявно уже говорило нам, что этот порт ALPC должен быть портом соединения ALPC , потому что порты связи не названы. В свою очередь мы можем сделать вывод, что мы можем использовать WinObj только для поиска портов подключения ALPC и через эти только серверные процессы ALPC.
Говоря о серверных процессах: как показано выше, можно использовать WinDbg. недокументированные !alpcКоманда для отображения информации о порте ALPC, который мы только что идентифицировали. Вывод включает в себя, наряду с большим количеством другой полезной информации, серверный процесс, владеющий портом, которым в данном случае является svchost.exe .
Теперь, когда мы знаем адрес объекта порта ALPC, мы можем использовать !alpcеще раз, чтобы отобразить активные соединения для этого порта соединения ALPC:
Примечание: команда !alpc Windbg не задокументирована, но устаревшая команда !lpc, которая существовала во времена LPC, задокументирована здесь и имеет отметку времени от декабря 2021 года. На этой странице документации упоминается, что команда !lpc устарела и что Вместо этого следует использовать команду !alpc, но синтаксис и параметры команды !alpc совершенно другие. Но, честно говоря, синтаксис команды !alpc отображается в WinDbg, если вы введете любую недопустимую команду !alpc:
Благодаря Джеймсу Форшоу и его NtObjectManager в .NET мы также можем легко запросить NtObjectManager в PowerShell для поиска объектов порта ALPC, и, что еще лучше, Джеймс уже предоставил для этого функцию-оболочку через Get-AccessibleAlpcPort .
Найти ALPC, используемый в процессах
Как всегда, существуют различные способы узнать об использовании портов ALPC в процессах, вот некоторые из них, которые пришли на ум:
После установки dumpbin.exe , который, например, поставляется с пакетом разработки Visual Studio C++, можно использовать следующие два однострочника PowerShell для поиска .exe и .dll , которые создают или подключаются к порту ALPC:
Я не кодировал 2-й вариант (разбор IAT) — если вы знаете инструмент, который это делает, дайте мне знать , но есть простой, но очень медленный способ решить вариант № 3 (найти дескрипторы ALPC в процессах), используя следующее Команда WinDbg: !handle 0 2 0 ALPC Port
Имейте в виду, что это очень медленно и, вероятно, займет несколько часов (я остановился через 10 минут и получил только около 18 дескрипторов).
Но еще раз спасибо Джеймсу Форшоу и его NtApiDotNet , есть более простой способ написать это самостоятельно и ускорить этот процесс, плюс мы также можем получить интересную статистику ALPC…
Вы можете найти этот инструмент здесь
Обратите внимание, что эта программа не запускается в пространстве ядра, поэтому я ожидаю лучших результатов с помощью команды WinDbg, но она выполняет свою работу по отображению некоторых портов ALPC, используемых различными процессами. Перебирая все процессы, к которым у нас есть доступ, мы также можем рассчитать некоторые базовые статистические данные об использовании ALPC, как показано выше. Эти цифры не являются точными на 100%, но учитывая, что в среднем около 14 дескрипторов коммуникационных портов ALPC используются для каждого процесса, мы можем определенно заключить, что ALPC довольно часто используется в Windows.
Как только вы определите процесс, который звучит как интересная цель, WinDbg можно использовать снова, чтобы копнуть глубже…
Используйте трассировку событий для Windows
Хотя ALPC не некоторые события ALPC отображаются как события Windows, которые можно зафиксировать с помощью отслеживания событий для Windows (ETW). Одним из инструментов, помогающих с ALPC-событиями, является ProcMonXv2 от zodiacon .
Имперсонация и неимперсонация
Для коммуникации ALPC процедуры имперсонализации привязаны к сообщениям, что означает, что и клиент, и сервер (а также каждая из общающихся сторон) могут выдавать себя за пользователя на другой стороне. Однако, для того, чтобы разрешить выдавать себя за другого, партнер по коммуникации должен разрешить выдавать себя за другого, а партнер по коммуникации, выдающий себя за другого, должен обладать привилегией SeImpersonate (это все еще защищенный канал связи, верно?)...
Глядя на код, кажется, что есть два варианта выполнения первого условия, которое заключается в разрешении быть выданным за другого:
Первый вариант: Через PortAttributes, например, так:
Если вы не очень хорошо знакомы с кодом VC++/ALPC, эти фрагменты могут ничего вам не сказать, что совершенно нормально. Дело в том, что теоретически есть два варианта указать, что вы разрешаете олицетворение.
Однако есть загвоздка:
Я рассмотрел оба маршрута: сервер, выдающий себя за клиента, и клиент, выдающий себя за сервер.
Мой первый путь заключался в поиске клиентов, пытающихся подключиться к несуществующему порту сервера, чтобы проверить условия имперсонализации. Я пробовал разные методы, но пока не нашел хорошего способа идентифицировать таких клиентов. Мне удалось использовать точки останова в ядре, чтобы вручную определить некоторые случаи, но пока не удалось найти ничего интересного, что позволило бы олицетворять клиента. Ниже приведен пример «ApplicationFrameHost.exe», пытающийся подключиться к несуществующему порту ALPC, который я мог перехватить с помощью своего демонстрационного сервера, однако процесс не допускает имперсонализации (и приложение работает как мой текущий пользователь) …
Не слишком удачная попытка персонализации, но, по крайней мере, она подтверждает идею.
Переходим к другому пути: Я нашел кучу портов подключения ALPC, используя Get-AccessibleAlpcPort, как было показано ранее, и дал команду своему ALPC клиенту подключиться к ним, чтобы проверить, позволяют ли они мне а) подключиться, б) отправить мне какое-либо фактическое сообщение обратно и в) отправить атрибуты сообщения имперсонализации вместе с сообщением. Для всех проверенных мной портов подключения ALPC в лучшем случае я получал короткое инициализационное сообщение с атрибутом ALPC_MESSAGE_CONTEXT_ATTRIBUTE, что не полезно для имперсонизации, но, по крайней мере, еще раз демонстрирует идею:
Неимперсонация сервера
В части цикла, посвященной RPC, я упоминал, что может быть интересно подключиться к серверу, который использует имперсонацию для изменения контекста безопасности своего потока на контекст безопасности вызывающего клиента, но не проверяет, удалась или не удалась имперсонация. В таком случае сервер может быть обманут и заставить выполнять задачи со своим собственным - потенциально повышенным - контекстом безопасности. Как подробно описано в статье о RPC, поиск таких случаев сводится к индивидуальному анализу конкретного процесса сервера ALPC, который вы рассматриваете. Для этого вам потребуется следующее:
Серверный процесс, открывающий порт ALPC, к которому может подключиться ваш клиент.
Сервер должен принимать сообщения о соединении и должен пытаться выдать себя за сервер
Сервер не должен проверять, удалось или не удалось выдать себя за другого.
(В соответствующих случаях сервер должен работать в другом контексте безопасности, чем ваш клиент, а именно: другой пользователь или другой уровень целостности).
На данный момент я не могу придумать хороший способ автоматизировать или полуавтоматизировать процесс поиска таких целей. Единственный вариант, который приходит мне на ум, это поиск портов подключения ALPC и реверсирование процессов хостинга.
Я буду обновлять этот пост, если наткнусь на что-нибудь интересное в этом направлении, но для основной части я хотел повторить путь атаки при неудачных попытках имперсонации.
Несвободные объекты сообщений
Как упоминалось в разделе Атрибуты сообщений ALPC, существует несколько атрибутов сообщений, которые клиент или сервер может отправить вместе с сообщением. Одним из них является атрибут ALPC_DATA_VIEW_ATTR, который может быть использован для отправки информации о сопоставленном представлении другой стороне коммуникации.
Напомнить: Это можно использовать, например, для хранения больших сообщений или данных в разделяемом представлении и отправки дескриптора этого разделяемого представления другой стороне вместо использования механизма обмена сообщениями с двойным буфером для копирования данных из одного пространства памяти в другое.
Интересным моментом здесь является то, что разделяемое представление (или раздел, как он называется в Windows) отображается в пространство процесса получателя, когда на него ссылается атрибут ALPC_DATA_VIEW_ATTR. Затем получатель может что-то сделать с этим разделом (если он знает о его отображении), но в конечном итоге получатель сообщения должен убедиться, что отображенное представление освобождено из собственного пространства памяти, а это требует определенного количества шагов, которые могут быть выполнены неправильно. Если получателю не удается освободить сопоставленное представление, например, потому что он вообще не ожидал получить представление, отправитель может посылать все больше и больше представлений с произвольными данными, чтобы заполнить пространство памяти получателя представлениями с произвольными данными, что сводится к атаке Heap Spray.
Я узнал об этом векторе атаки ALPC, только прослушав (в очередной раз) доклад Алекса Ионеску SyScan ALPC Talk, и я думаю, что нет способа лучше сформулировать и показать, как работает этот вектор атаки, чем он сам в этом докладе, поэтому я не буду копировать его содержание и слова и просто укажу вам на 32-ю минуту его доклада, где он начинает объяснять атаку. Также вам стоит посмотреть 53-ю минуту его выступления, чтобы увидеть демонстрацию атаки heap spray.
Та же логика применяется к другим атрибутам сообщения ALPC, например, к дескрипторам, которые отправляются в ALPC_MESSAGE_HANDLE_INFORMATION через атрибут дескриптора ALPC.
Поиск уязвимых целей для этого типа атак — это, опять же, индивидуальный процесс расследования, в котором необходимо:
В качестве примера я заполнил свой тестовый сервер ALPC представлениями размером 20 КБ, как показано ниже:
Это действительно работает, потому что я не какие пытался освободить либо выделенные атрибуты на моем тестовом сервере ALPC.
Я также случайным образом выбрал несколько — например, четыре или пять — процессов ALPC от Microsoft (которые я идентифицировал с помощью показанных выше методов), но те, которые я выбрал, похоже, не совершают ту же ошибку.
Честно говоря, может быть полезно проверить больше процессов для этого, но, насколько мне известно, мне не нужна такая ошибка, кроме сбоя процесса, который, если он достаточно критичен, также может привести к сбою ОС (отказ в обслуживании).
Интересное примечание :
В своем выступлении Алекс Ионеску упоминает, что диспетчер памяти Windows выделяет области памяти на границах 64 КБ, что означает, что всякий раз, когда вы выделяете память, диспетчер памяти помещает эту память в начало следующего доступного блока размером 64 КБ. Что позволяет вам, как злоумышленнику, создавать и отображать представления произвольного размера (желательно меньше 64 КБ, чтобы сделать исчерпание памяти эффективным), а ОС будет отображать представление в памяти сервера и помечать 64 КБ-YourViewSize как неиспользуемую память, потому что это необходимо выровнять все распределение памяти по границам 64 КБ. Вы хотите увидеть 54-ю минуту выступления Алекса, чтобы получить визуальное и словесное объяснение этого эффекта.
Рэймонд Чен объясняет причину гранулярности в 64 КБ здесь .
В конце концов, атаки с исчерпанием памяти, конечно, не единственный жизнеспособный вариант использования примитива памяти/кучи, который люди умнее меня могут превратить в путь эксплойта…
В ALPC есть гораздо больше, чем я рассказал в этом посте. Потенциально можно было бы написать целую книгу об ALPC, но я надеюсь, что хотя бы коснулся основ, чтобы вы начали интересоваться ALPC.
Чтобы получить первое впечатление «Где и сколько ALPC находится на моем ПК», я рекомендую запустить ProcMonXv2 (от zodiacon ) на вашем хосте, чтобы увидеть тысячи событий ALPC, запускаемых за несколько секунд.
Чтобы продолжить оттуда, вы можете найти мой код клиента и сервера ALPC полезным для игры с процессами ALPC, а также для выявления и использования уязвимостей в ALPC. Если вы обнаружите, что кодируете и/или исследуете ALPC, обязательно ознакомьтесь с справочным разделом, чтобы узнать, как другие справились с ALPC.
Наконец, в качестве последнего слова и в заключение моей рекомендации с самого начала: если вы чувствуете, что можете услышать другой голос и точку зрения на ALPC, я настоятельно рекомендую взять еще один напиток и насладиться следующим часом Алекса Ионеску о LPC, RPC и АЛК:
ALPC сервера, коммуникационном должно быть, было неверным, что меня озадачило, поскольку клиент на другой стороне действительно получает сообщения на свой коммуникационный порт. Я некоторое время зависал над этим вопросом, пока не связался с Алексом Ионеску , чтобы спросить его об этом, и узнал, что мое предположение было действительно неверным, а точнее, со временем стало неверным: Алекс объяснил мне, что идея, которую я имел (сервер прослушивает и отправляет сообщения через свой коммуникационный порт) был способ, которым LPC (предшественник ALPC) был разработан для работы. Однако такой дизайн заставит вас прослушивать растущее число коммуникационных портов с каждым новым клиентом, которого принимает сервер. Представьте, что с сервером разговаривают 100 клиентов, затем серверу необходимо прослушивать 100 коммуникационных портов, чтобы получать клиентские сообщения, что часто приводит к созданию 100 потоков, где каждый поток взаимодействует с другим клиентом. Это было сочтено неэффективным, и гораздо более эффективным решением было иметь один поток, прослушивающий (и отправляющий) порт подключения сервера, где все сообщения отправляются на этот порт подключения.
Это, в свою очередь, означает: сервер принимает клиентское соединение, получает дескриптор коммуникационного порта клиента, но по-прежнему использует дескриптор порта соединения сервера в вызовах NtAlpcSendWaitReceivePort для отправки и получения сообщений от всех подключенных клиентов.
Означает ли это, что коммуникационный порт сервера устарел (и это был мой дополнительный вопрос Алексу )? Его ответ снова имел смысл и прояснил мое понимание ALPC: порт связи сервера для каждого клиента используется внутри ОС для привязки сообщения, отправленного конкретным клиентом, к конкретному порту связи этого клиента. Это позволяет ОС связать специальную структуру контекста с каждым клиентским портом связи, которая может использоваться для идентификации клиента. Этой специальной контекстной структурой является PortContext , которая может быть любой произвольной структурой, которую можно передать в NtAlpcAcceptConnectPort и которую впоследствии можно извлечь из любого сообщения с ALPC_CONTEXT_ATTR .
Это означает: когда сервер прослушивает свой порт соединения, он получает сообщения от всех клиентов, но если он хочет знать, какой клиент отправляет сообщение, сервер может получить структуру контекста порта (через ALPC_CONTEXT_ATTR ), которую он назначил для этот клиент после принятия соединения, и ОС будет получать эту структуру контекста из внутренне сохраненного порта связи клиента.
На данный момент мы можем заключить, что коммуникационный порт сервера для каждого клиента по-прежнему важен для ОС и по-прежнему имеет свое место и роль в коммуникационной структуре ALPC. Это, однако, не отвечает на вопрос, зачем серверу на самом деле нужен дескриптор порта связи каждого клиента (поскольку клиентский PortContext может быть извлечен из сообщения, полученного с помощью дескриптора порта соединения).
Ответ здесь — имперсонализация . Когда сервер хочет олицетворять клиента, ему необходимо передать порт связи клиента в NtAlpcImpersonateClientOfPort . Причина этого в том, что информация о контексте безопасности, необходимая для выполнения олицетворения, привязана (если это разрешено клиентом) к коммуникационному порту клиента. Не имеет смысла привязывать эту информацию к порту подключения, потому что все клиенты используют этот порт подключения, а каждый клиент имеет свой уникальный порт связи для каждого сервера.
Следовательно: если вы хотите имперсонализировать своих клиентов, вы хотите сохранить дескриптор порта связи каждого клиента.
Справочные проекты, в которых используется ALPC
Оригинальная статья тут.
Введение
После обсуждения двух протоколов межпроцессного взаимодействия (IPC), которые можно использовать как удаленно, так и локально, а именно Named Pipes и RPC, мы рассмотрим технологию ALPC, которую можно использовать только локально. В то время как RPC расшифровывается как Remote Procedure Call, ALPC расшифровывается как Advanced Local Procedure Call, иногда также упоминается как Asynchronous Local Procedure Call. Особенно позднее упоминание (асинхронный) является отсылкой к временам Windows Vista, когда ALPC был представлен для замены LPC (Local Procedure Call), который был предшествующим механизмом IPC, используемым до появления Windows Vista.Кратко о LPC
Механизм локального вызова процедур был представлен в оригинальном ядре Windows NT в 1993-94 годах в качестве синхронного средства межпроцессного взаимодействия. Его синхронная природа означала, что клиенты/серверы должны были ждать отправки сообщения и выполнения действий, прежде чем выполнение могло быть продолжено. Это был один из основных недостатков, который был призван заменить ALPC, и причина, по которой ALPC некоторые называют асинхронным LPC.
О ALPC стало известно в Windows Vista, и, по крайней мере, начиная с Windows 7, LPC был полностью удален из ядра NT. Чтобы не ломать устаревшие приложения и обеспечить обратную совместимость, которой так славится Microsoft, функция, используемая для создания порта LPC, была сохранена, но вызов функции был перенаправлен на создание не LPC, а ALPC порта.
Поскольку LPC практически отсутствует после Windows 7, этот пост будет посвящен только ALPC, так что давайте вернемся к нему.
Но если вам, как и мне, нравится читать старую документацию о том, как все начиналось и как все работало раньше, вот статья, в которой подробно рассказывается о том, как LPC работал в Windows NT 3.5: http: //web.archive.org/web/20090220111555/http://www.windowsitlibrary.com/Content/356/08/1.html
Назад к ALPC
ALPC — это быстрое, очень мощное и очень широко используемое в ОС Windows (внутренне) средство межпроцессного взаимодействия, но оно не предназначено для использования разработчиками, поскольку для Microsoft ALPC — это внутреннее средство IPC, что означает, что ALPC недокументирован и используется только в качестве базовой технологии транспортировки для других, задокументированных и предназначенных для использования разработчиками протоколов транспортировки сообщений, например RPC.
Тот факт, что ALPC не задокументирован (Microsoft), однако, не означает, что ALPC — это полный черный ящик, поскольку умные люди, такие как Алекс Ионеску , как он работает и какие компоненты у него есть. Но на самом означает , что вы не должны полагаться на какое-либо поведение ALPC для какого-либо долгосрочного производственного использования, и, более того, вам действительно не следует использовать ALPC напрямую для создания программного обеспечения, поскольку существует множество неочевидных ловушек, которые могут вызвать проблемы с безопасностью или стабильностью.
ALPC — очень интересная цель, но она не предназначена для (не Microsoft) использования в разработке продукции. Также вам не следует полагаться на то, что вся информация в этом посте является или продолжает быть точной на 100%, поскольку ALPC не задокументирован.
ALPC Внутренние устройства
Хорошо, давайте углубимся в некоторые внутренности ALPC, чтобы понять, как работает ALPC, какие движущиеся части участвуют в коммуникациях и как выглядят сообщения, чтобы, наконец, понять, почему ALPC может быть интересной целью с точки зрения наступательной безопасности.Основы
Чтобы оторваться от земли, следует отметить, что основными компонентами связи ALPC являются портовые объекты ALPC. Объект порта ALPC является объектом ядра, и его использование аналогично использованию сетевого сокета, когда сервер открывает сокет, к которому клиент может подключиться для обмена сообщениями.Если вы запустите WinObj из Sysinternals Suite , вы обнаружите, что в каждой ОС Windows работает много портов ALPC, некоторые из них можно найти в корневом пути, как показано ниже:
… но большинство портов ALPC размещены по пути «Управление RPC» (помните, что RPC использует ALPC под капотом):
Чтобы начать работу с ALPC-связью, сервер открывает порт ALPC, к которому могут подключаться клиенты, который называется портом подключения ALPC , однако это не единственный порт ALPC, который создается во время потока связи ALPC (как вы увидим в следующей главе). Еще два порта ALPC создаются для клиента и сервера для передачи сообщений.
Итак, первое, на что следует обратить внимание, это:
- Всего в обмене данными ALPC задействовано 3 порта ALPC (2 на стороне сервера и 1 на стороне клиента).
- Порты, которые вы видели на WinObj выше, — это порты подключения ALPC , к которым может подключаться клиент.
Несмотря на то, что всего в ALPC-коммуникациях используется 3 порта ALPC, и все они называются по-разному (например, «Порты подключения ALPC»), существует только один объект ядра порта ALPC, в котором все три порта используются в Связь ALPC, создание экземпляра. Скелет этого объекта ядра ALPC выглядит следующим образом:
Как видно выше, объект ядра ALPC — довольно сложный объект ядра, ссылающийся на различные другие типы объектов. Это делает его интересной целью для исследований, но также оставляет хороший запас для ошибок и/или пропущенных путей атаки.
Поток сообщений ALPC
Чтобы углубиться в ALPC, мы рассмотрим поток сообщений ALPC, чтобы понять, как сообщения отправляются и как они могут выглядеть. Прежде всего, мы уже узнали, что 3 объекта порта ALPC участвуют в сценарии связи ALPC, первый из которых является портом соединения ALPC , который создается серверным процессом и к которому могут подключаться клиенты (аналогично сетевому сокету). . Как только клиент подключается к порту подключения сервера ALPC, ядро создает два новых порта, называемых портом связи сервера ALPC и портом связи клиента ALPC .
После того, как порты связи сервера и клиента установлены, обе стороны могут отправлять сообщения друг другу, используя единую функцию NtAlpcSendWaitReceivePortподвергается воздействию ntdll.dll .
Название этой функции звучит как три вещи сразу — Send, Wait и Receive — и это именно то, что есть. Сервер и клиент используют эту единственную функцию для ожидания сообщений, отправки и получения сообщений через свой порт ALPC. Это звучит ненужно сложно, и я не могу точно сказать, почему он был построен таким образом, но вот мое предположение: помните, что ALPC был создан как быстрое средство связи только для внутреннего использования, а канал связи был построен вокруг одного объект ядра (порт ALPC). Использование этой трехсторонней функции позволяет выполнять несколько операций, например отправку и получение сообщения, в одном вызове и, таким образом, экономит время и уменьшает количество переключений между пользователем и ядром. Кроме того, эта функция выступает в качестве единого входа в процесс обмена сообщениями и, следовательно, упрощает изменение кода и оптимизацию (связь ALPC используется во многих различных компонентах ОС, от драйверов ядра до пользовательских приложений с графическим интерфейсом, разработанных разными внутренними командами). Наконец, ALPC задуман как внутренний механизм IPC, поэтому Microsoft не нужно проектировать его в первую очередь для пользователя или стороннего разработчика.
В рамках этой единственной функции вы также указываете, какое сообщение вы хотите отправить (существуют разные типы с разными последствиями, мы вернемся к этому позже ) и какие другие атрибуты вы хотите отправить вместе с вашим сообщением (опять же, мы перейти к вещам, которые вы можете отправить вместе с сообщением, позже в главе Атрибуты сообщения ALPC ).
Пока это звучит довольно прямолинейно: сервер открывает порт, клиент подключается к нему, оба получают дескриптор коммуникационного порта и отправляют сообщения через единую функцию. NtAlpcSendWaitReceivePort… легкий.
Мы будем на высоком уровне, это так просто, но вы наверняка пришли сюда за подробностями, а в заголовке поста было сказано «внутренности», так что давайте пристегнемся и посмотрим поближе:
- Серверный процесс вызывает NtAlpcCreatePort с выбранным именем порта ALPC, например ' CSALPCPort ', и, необязательно, с SecurityDescriptor , чтобы указать, кто может к нему подключаться.
Ядро создает объект порта ALPC и возвращает дескриптор этого объекта на сервер, этот порт называется портом подключения ALPC. - Сервер вызывает NtAlpcSendWaitReceivePort , передавая дескриптор своего ранее созданного порта подключения, чтобы дождаться клиентских подключений
- Затем клиент может вызвать NtAlpcConnectPort с помощью:
- Имя порта ALPC сервера ( CSALPCPort )
- (НЕОБЯЗАТЕЛЬНО) сообщение для сервера (например, отправить волшебное ключевое слово или что-то еще)
- (НЕОБЯЗАТЕЛЬНО) SID сервера, чтобы клиент мог подключиться к нужному серверу.
- (НЕОБЯЗАТЕЛЬНО) атрибуты сообщения для отправки вместе с запросом на подключение клиента
(Атрибуты сообщения будут подробно описаны в главе Атрибуты сообщения ALPC )
- Затем этот запрос на подключение передается на сервер , который вызывает NtAlpcAcceptConnectPort , чтобы принять или отклонить запрос клиента на подключение.
(Да, хотя функция называется NtAlpcAccept… эту функцию также можно использовать для отклонения клиентских подключений. Последний параметр этой функции является логическим значением, указывающим, принимаются ли подключения (если установлено значение true) или отклонено (если установлено значение false).
Сервер также может:- (НЕОБЯЗАТЕЛЬНО) вернуть сообщение клиенту с принятием или отклонением запроса на подключение и/или…
- (НЕОБЯЗАТЕЛЬНО) добавьте к этому сообщению атрибуты сообщения и/или ..
- (НЕОБЯЗАТЕЛЬНО) выделить пользовательскую структуру, например уникальный идентификатор, который прикрепляется к коммуникационному порту сервера для идентификации клиента
— Если сервер принимает запрос на соединение, сервер и клиент получают дескриптор коммуникационного порта —
- Клиент и сервер теперь могут отправлять и получать сообщения друг от друга через NtAlpcSendWaitReceivePort , где:
- Клиент прослушивает и отправляет новые сообщения на свой коммуникационный порт
- Сервер прослушивает и отправляет новые сообщения на свой подключения порт
- И клиент, и сервер могут указать, какие атрибуты сообщения (мы вернемся к этому чуть позже) они хотят получать при прослушивании новых сообщений.
…подождите… Почему сервер отправляет/принимает данные через порт соединения а не связи , если у него есть выделенный порт связи?… Это была одна из многих вещей, которые озадачили меня в ALPC, и вместо того, чтобы тяжелая работа заднего хода, чтобы понять это самостоятельно, я сжульничал, связался с Алексом Ионеску и просто спросил эксперта. Я поместил ответ в Приложение А в конце этого поста, так как я не хочу слишком далеко уходить от потока сообщений в этот момент… извините за вешалку…
В любом случае, оглядываясь назад на поток сообщений сверху, мы можем понять, что клиент и сервер используют вызовы различных функций для создания портов ALPC, а затем отправляют и получают сообщения через единую функцию. NtAlpcSendWaitReceivePort. Хотя он содержит достаточный объем информации о потоке сообщений, важно всегда помнить, что сервер и клиент не имеют прямого однорангового соединения, а вместо этого направляют все сообщения через ядро, которое отвечает за размещение сообщений в очереди сообщений, уведомляя каждую сторону о полученных сообщениях и другие вещи, такие как проверка сообщений и атрибутов сообщений. Чтобы представить это в перспективе, я добавил несколько вызовов ядра на эту картинку:
Я должен признать, что на первый взгляд эта диаграмма не очень интуитивна, но я обещаю, что в процессе все прояснится, потерпите меня.
Чтобы получить более полное представление о том, как ALPC выглядит внутри, нам нужно немного глубже погрузиться в части реализации сообщений ALPC, которые я расскажу в следующем разделе.
Детали обмена сообщениями ALPC
Итак, прежде всего, давайте проясним структуру сообщения ALPC. Сообщение ALPC всегда состоит из так называемого PORT_HEADER или PORT_MESSAGE , за которым следует фактическое сообщение, которое вы хотите отправить, например текст, двоичное содержимое или что-то еще.
В простом старом C++ мы можем определить сообщение ALPC с помощью следующих двух структур:
C++:
typedef struct _ALPC_MESSAGE {
PORT_MESSAGE PortHeader;
BYTE PortMessage[100]; // using a byte array of size 100 to store my actual message
} ALPC_MESSAGE, * PALPC_MESSAGE;
typedef struct _PORT_MESSAGE
{
union {
struct {
USHORT DataLength;
USHORT TotalLength;
} s1;
ULONG Length;
} u1;
union {
struct {
USHORT Type;
USHORT DataInfoOffset;
} s2;
ULONG ZeroInit;
} u2;
union {
CLIENT_ID ClientId;
double DoNotUseThisField;
};
ULONG MessageId;
union {
SIZE_T ClientViewSize;
ULONG CallbackId;
};
} PORT_MESSAGE, * PPORT_MESSAGE;
Чтобы отправить сообщение, нам нужно сделать следующее:
C++:
// specify the message struct and fill it with all 0's to get a clear start
ALPC_MESSAGE pmSend, pmReceived;
RtlSecureZeroMemory(&pmSend, sizeof(pmSend));
RtlSecureZeroMemory(&pmReceived, sizeof(pmReceived));
// getting a pointer to my payload (message) byte array
LPVOID lpPortMessage = pmSend->PortMessage;
LPCSTR lpMessage = "Hello World!";
int lMsgLen = strlen(lpMessage);
// copying my message into the message byte array
memmove(lpPortMessage, messageContent, lMsgLen);
// specify the length of the message
pMessage->PortHeader.u1.s1.DataLength = lMsgLen;
// specify the total length of the ALPC message
pMessage->PortHeader.u1.s1.TotalLength = sizeof(PORT_MESSAGE) + lMsgLen;
// Send the ALPC message
NTSTATUS lSuccess = NtAlpcSendWaitReceivePort(
hCommunicationPort, // the client's communication port handle
ALPC_MSGFLG_SYNC_REQUEST, // message flags: synchronous message (send & receive message)
(PPORT_MESSAGE)&pmSend, // our ALPC message
NULL, // sending message attributes: we don't need that in the first step
(PPORT_MESSAGE)&pmReceived, // ALPC message buffer to receive a message
&ulReceivedSize, // SIZE_T ulReceivedSize; Size of the received message
NULL, // receiving message attributes: we don't need that in the first step
0 // timeout parameter, we don't want to timeout
);
Этот фрагмент кода отправит сообщение ALPC с текстом «Hello World!» на сервер, к которому мы подключились. Мы указали, что сообщение должно быть синхронным сообщением с ALPC_MSGFLG_SYNC_REQUESTфлаг, который означает, что этот вызов будет ожидать (блокироваться), пока сообщение не будет получено на коммуникационном порту клиента.
Конечно, нам не нужно ждать, пока придет новое сообщение, а использовать оставшееся до этого время для других задач (помните, что ALPC создавался как асинхронный, быстрый и эффективный). Для облегчения этого ALPC предоставляет три различных типа сообщений:
- Синхронный запрос : как упоминалось выше, синхронные сообщения блокируются до тех пор, пока не поступит новое сообщение (как логический результат этого, при вызове необходимо указать принимающий буфер сообщений ALPC). NtAlpcSendWaitReceivePortс синхронными сообщениями)
- Асинхронный запрос : асинхронные сообщения отправляют ваше сообщение, но не ждут и не воздействуют на какие-либо полученные сообщения.
- Запросы дейтаграмм: запросы дейтаграмм похожи на пакеты UDP, они не ожидают ответа, и поэтому ядро не блокируется в ожидании полученного сообщения при отправке запроса дейтаграммы.
Еще раз у вас есть 3 варианта:
- Вы можете использовать список завершения ALPC, и в этом случае ядро не информирует вас (как получателя) о получении новых данных, а вместо этого просто копирует данные в память вашего процесса. Вы (как получатель) должны знать о наличии этих новых данных. Например, это может быть достигнуто с помощью события уведомления, которое совместно используется вами и сервером ALPC¹. Как только сервер сигнализирует о событии, вы знаете, что поступили новые данные.
¹Взято из Windows Internals, Part 2, 7th Edition . - Вы можете использовать порт завершения ввода-вывода, который является задокументированным средством синхронизации.
- Вы можете получить обратный вызов ядра, чтобы получить ответы, но это разрешено только в том случае, если ваш процесс находится в земле ядра.
- Основная очередь : сообщение было отправлено, и клиент его обрабатывает.
- Ожидающая очередь : сообщение было отправлено, и вызывающий абонент ожидает ответа, но ответ еще не отправлен.
- Большая очередь сообщений : сообщение было отправлено, но буфер вызывающей стороны был слишком мал для его получения. Вызывающий получает еще один шанс выделить больший буфер и снова запросить полезную нагрузку сообщения.
- Отмененная очередь : сообщение, которое было отправлено на порт, но с тех пор было отменено.
- Прямая очередь : сообщение, которое было отправлено с прикрепленным прямым событием.
На данный момент я не собираюсь углубляться в параметры синхронизации сообщений и различные очереди — мне нужно где-то сделать вырез — однако, если кто-то заинтересован в поиске ошибок в этих областях кода, я настоятельно рекомендую посмотреть в главу 8 удивительного внутреннего устройства Windows, часть 2, 7-е издание . Я многому научился из этой книги и не могу не похвалить ее!
Наконец, что касается деталей обмена сообщениями в ALPC, есть последний момент, который еще не был подробно описан, а именно вопрос о том , как сообщение передается от клиента к серверу. Было упомянуто, какие сообщения можно отправлять, как выглядит структура сообщения, какой существует механизм синхронизации и остановки сообщений, но до сих пор не было подробно описано, как сообщение передается от одного процесса к другому.
У вас есть два варианта для этого:
- Механизм двойного буфера : в этом подходе буфер сообщений выделяется в пространстве памяти отправителя и получателя (виртуальном), и сообщение копируется из (виртуальной) памяти отправителя в (виртуальную) память ядра, а оттуда в память получателя (виртуальную). Память. Он называется двойным буфером, потому что буфер, содержащий сообщение, выделяется и копируется дважды (отправитель -> ядро и ядро -> получатель).
- Механизм объекта раздела : вместо выделения буфера для хранения сообщения клиент и сервер могут также выделить раздел общей памяти, к которому могут получить доступ обе стороны, отобразить представление этого раздела, что в основном означает ссылку на определенную область этого выделенный раздел — скопируйте сообщение в сопоставленное представление и, наконец, отправьте это представление в качестве атрибута сообщения (обсуждается в следующей главе) получателю. Получатель может извлечь указатель на то же представление, которое отправитель использовал через атрибут сообщения представления, и прочитать данные из этого представления.
Этот «механизм двойного буфера» использовался во фрагменте кода выше. Оглядываясь назад, буфер сообщения для отправленного и полученного сообщения был неявно выделен с помощью первых трех строк кода:
C++:
ALPC_MESSAGE pmSend, pmReceived; // these are the message buffers
RtlSecureZeroMemory(&pmSend, sizeof(pmSend));
RtlSecureZeroMemory(&pmReceived, sizeof(pmReceived));
Затем этот буфер сообщений был передан ядру при вызове NtAlpcSendWaitReceivePort, который копирует буфер отправки в буфер приема на другой стороне.
Мы также могли бы покопаться в ядре, чтобы выяснить, как на самом деле выглядит сообщение ALPC (отправляемое через буферы сообщений). Реверс NtAlpcSendWaitReceivePortприводит нас к функции ядра AlpcpReceiveMessage, который в конечном итоге вызывает — для нашего пути кода — в AlpcpReadMessageData, где происходит копирование буфера.
Примечание: если вас интересуют все детали реверсирования, которые я здесь не упомянул, ознакомьтесь с моим последующим постом: Отладка и реверсирование ALPC.
В конце этого пути вы найдете простой RtlCopyMemory , который является просто макросом для memcpy , который копирует кучу байтов из одного пространства памяти в другое — это не так красиво, как можно было бы ожидать, но это то, что это ¯\ (ツ) /¯.
Чтобы увидеть это в действии, я поставил точку останова в AlpcpReadMessageData показанная выше функция для моего серверного процесса ALPC. Точка останова срабатывает, когда мой клиент ALPC подключается и отправляет начальное сообщение на сервер. Сообщение, которое отправляет клиент, это: Hello Server. Аннотированный вывод отладки показан ниже:
Эти экраны отладки показывают, как выглядит сообщение ALPC, отправленное через буфер сообщений… просто байты в памяти процесса.
Также обратите внимание, что приведенные выше экраны являются визуальным представлением «механизма двойного буфера» на втором этапе копирования буфера, когда сообщение копируется из пространства памяти ядра в пространство памяти процесса получателя. Действие копирования из отправителя в пространство ядра не отслеживалось, поскольку точка останова была установлена только для процесса-получателя.
Атрибуты сообщения ALPC
Хорошо, есть еще одна последняя деталь, которую необходимо детализировать, прежде чем собрать все вместе, а именно атрибуты сообщения ALPC. Я уже несколько раз упоминал атрибуты сообщения, так что вот что это значит.При отправке и получении сообщений через NtAlpcSendWaitReceivePort, клиент и сервер могут указать набор атрибутов, которые они хотели бы отправлять и/или получать. Этот набор атрибутов, которые нужно отправить, и набор атрибутов, которые нужно получить, передаются в NtAlpcSendWaitReceivePortв двух дополнительных параметрах, показанных ниже:
Идея здесь заключается в том, что как отправитель вы можете передать дополнительную информацию получателю, а получатель на другом конце может указать, какой набор атрибутов он хотел бы получить, а это означает, что не обязательно вся дополнительная информация, которая была отправлена, также доступна получателю. получатель.
Следующие атрибуты сообщения могут быть отправлены и/или получены:
- Атрибут безопасности : Атрибут безопасности содержит информацию о контексте безопасности, которая, например, может использоваться для олицетворения отправителя сообщения (подробно описано в Имитация »). Эта информация контролируется и проверяется ядром. Структура этого атрибута показана ниже:
C++:
typedef struct _ALPC_SECURITY_ATTR {
ULONG Flags;
PSECURITY_QUALITY_OF_SERVICE pQOS;
HANDLE ContextHandle;
} ALPC_SECURITY_ATTR, * PALPC_SECURITY_ATTR;
- Атрибут просмотра : как описано в конце главы « Подробности обмена сообщениями », этот атрибут можно использовать для передачи указателя на раздел общей памяти, который может использоваться принимающей стороной для чтения данных из этого раздела памяти. Структура этого атрибута показана ниже:
C++:
typedef struct _ALPC_DATA_VIEW_ATTR {
ULONG Flags;
HANDLE SectionHandle;
PVOID ViewBase;
SIZE_T ViewSize;
} ALPC_DATA_VIEW_ATTR, * PALPC_DATA_VIEW_ATTR;
- Атрибут контекста : Атрибут контекста хранит указатели на определяемые пользователем структуры контекста, которые были назначены конкретному клиенту (коммуникационному порту) или конкретному сообщению. Структура контекста может быть любой произвольной структурой, например уникальным номером, и предназначена для идентификации клиента. Сервер может извлечь и сослаться на структуру порта, чтобы однозначно идентифицировать клиента, отправляющего сообщение. Пример структуры порта, которую я использовал, можно найти здесь . Ядро установит порядковый номер, идентификатор сообщения и идентификатор обратного вызова, чтобы включить структурированную обработку сообщений (аналогично TCP). Этот атрибут сообщения всегда может быть извлечен получателем сообщения, отправитель не должен указывать это и не может запретить получателю доступ к этому. Структура этого атрибута показана ниже:
C++:
typedef struct _ALPC_CONTEXT_ATTR {
PVOID PortContext;
PVOID MessageContext;
ULONG Sequence;
ULONG MessageId;
ULONG CallbackId;
} ALPC_CONTEXT_ATTR, * PALPC_CONTEXT_ATTR;
- Атрибут дескриптора: Атрибут дескриптора может использоваться для передачи дескриптора конкретному объекту, например, файлу. Получатель может использовать этот дескриптор для ссылки на объект, например, при вызове ReadFile . Ядро проверит, действителен ли переданный дескриптор, и в противном случае выдаст ошибку. Структура этого атрибута показана ниже:
Код:
typedef struct _ALPC_MESSAGE_HANDLE_INFORMATION {
ULONG Index;
ULONG Flags;
ULONG Handle;
ULONG ObjectType;
ACCESS_MASK GrantedAccess;
} ALPC_MESSAGE_HANDLE_INFORMATION, * PALPC_MESSAGE_HANDLE_INFORMATION;
- Атрибут токена: Атрибут токена может использоваться для передачи ограниченной информации о токене отправителя. Структура этого атрибута показана ниже:
Код:
typedef struct _ALPC_TOKEN_ATTR
{
ULONGLONG TokenId;
ULONGLONG AuthenticationId;
ULONGLONG ModifiedId;
} ALPC_TOKEN_ATTR, * PALPC_TOKEN_ATTR;
- Прямой атрибут : Прямой атрибут можно использовать для связывания созданного события с сообщением. Получатель может получить событие, созданное отправителем, и сигнализировать об этом, чтобы сообщить отправителю, что сообщение отправки было получено (особенно полезно для запросов дейтаграмм). Структура этого атрибута показана ниже:
C++:
typedef struct _ALPC_DIRECT_ATTR
{
HANDLE Event;
} ALPC_DIRECT_ATTR, * PALPC_DIRECT_ATTR;
- Атрибут Work-On-Behalf-Of : этот атрибут можно использовать для отправки рабочего билета , связанного с отправителем. Я не играл с этим, поэтому я не могу вдаваться в подробности. Структура этого атрибута показана ниже:
C++:
typedef struct _ALPC_WORK_ON_BEHALF_ATTR
{
ULONGLONG Ticket;
} ALPC_WORK_ON_BEHALF_ATTR, * PALPC_WORK_ON_BEHALF_ATTR;
Атрибуты сообщения, как они инициализируются и отправляются, были еще одной вещью, которая озадачила меня при кодировании примера сервера и клиента ALPC. Так что вы не столкнетесь с теми же проблемами, которые были у меня здесь, это секрет, который я узнал об атрибутах сообщения ALPC:
Для начала необходимо знать, что структура атрибутов сообщения ALPC следующая:
C++:
typedef struct _ALPC_MESSAGE_ATTRIBUTES
{
ULONG AllocatedAttributes;
ULONG ValidAttributes;
} ALPC_MESSAGE_ATTRIBUTES, * PALPC_MESSAGE_ATTRIBUTES;
Глядя на это, я сначала подумал, что вы вызываете функцию AlpcInitializeMessageAttribute, даете ей ссылку на приведенную выше структуру и флаг для атрибута сообщения, которое вы хотите отправить (на все атрибуты ссылается значение флага, вот список из моего кода ) и ядро затем устанавливает все это для вас. Затем вы помещаете указанную структуру в NtAlpcSendWaitReceivePort , повторяете процесс для каждого сообщения, которое хотите отправить, и все готово.
Это не так и кажется неправильным на нескольких уровнях. Только после того, как я нашел этот пост в твиттере от 2020 года и Алекса на SyScan'14 (я пересматривал его не менее 20 раз во время своего исследования), я пришел к тому, что я сейчас считаю правильным путем. Позвольте мне сначала выявить ошибки в моих первоначальных убеждениях, прежде чем объединять правильный курс действий:
- AlpcInitializeMessageAttribute ни хрена вам не делает, на самом деле он только очищает ValidAttributesфлаг и устанавливает AllocatedAttributesфлаг в соответствии с указанными вами атрибутами сообщения (так что никакая магия ядра не заполняет данные вообще).
Должен признаться, я заметил это на раннем этапе обратного проектирования функции, но какое-то время я все еще надеялся, что она сделает что-то еще, поскольку название функции было таким многообещающим. - Чтобы правильно настроить атрибут сообщения, вы должны выделить соответствующую структуру сообщения и поместить ее в буфер после ALPC_MESSAGE_ATTRIBUTES . Таким образом, это похоже на ALPC_MESSAGE где фактическое сообщение необходимо поместить в буфер после PORT_MESSAGE .
- Не ядро устанавливает ValidAttributes для вашей ALPC_MESSAGE_ATTRIBUTES , вы должны установить его самостоятельно. Я понял это, поигравшись со структурой, и какое-то время думал, что это просто странный обходной путь, потому что зачем мне устанавливать ValidAttributesполе? Насколько я понимаю, мои атрибуты всегда действительны, и не должна ли задача ядра проверять их действительность.
Я провел еще один раунд выступления Алекса на SyScan'14, чтобы понять, что... - Вы не устанавливаете атрибуты сообщения для каждого вызова NtAlpcSendWaitReceivePort , вы устанавливаете все атрибуты сообщения один раз и используете ValidAttributes перед вызовом NtAlpcSendWaitReceivePort , чтобы указать, какие из всех ваших настроенных атрибутов действительны для этого самого сообщения, которое вы отправляете сейчас.
Чтобы объединить это в полезные знания, вот как работают атрибуты отправки сообщения (в моем текущем понимании):
- Прежде всего, у вас есть два буфера: Буфер для атрибутов сообщений, которые вы хотите получить (в моем коде с именем: MsgAttrReceived) и буфер для атрибутов сообщений, которые вы хотите отправить (в моем коде с именем: MsgAttrSend).
- Для MsgAttrReceivedбуфера, вам просто нужно выделить буфер, достаточно большой для хранения ALPC_MESSAGE_ATTRIBUTES , а также всех атрибутов сообщения, которые вы хотите получить. После выделения этого буфера установите AllocatedAttributesатрибут к соответствующему значению атрибута(ов) флага(ов). Этот AllocatedAttributesзначение может быть изменено для каждого сообщения, которое вы получаете.
Для моего примера сервера и клиентского приложения я просто хочу всегда получать все атрибуты, которые может дать мне ядро, поэтому я устанавливаю буфер для получения атрибутов один раз в начале моего кода следующим образом:
C++:
pMsgAttrReceived = alloc_message_attribute(ALPC_MESSAGE_ATTRIBUTE_ALL);
PALPC_MESSAGE_ATTRIBUTES alloc_message_attribute(ULONG ulAttributeFlags) {
NTSTATUS lSuccess;
PALPC_MESSAGE_ATTRIBUTES pAttributeBuffer;
LPVOID lpBuffer;
SIZE_T lpReqBufSize;
SIZE_T ulAllocBufSize;
ulAllocBufSize = AlpcGetHeaderSize(ulAttributeFlags); // required size for specified attribues
lpBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, ulAllocBufSize);
if (GetLastError() != 0) {
wprintf(L"[-] Failed to allocate memory for ALPC Message attributes.\n");
return NULL;
}
pAttributeBuffer = (PALPC_MESSAGE_ATTRIBUTES)lpBuffer;
// using this function to properly set the 'AllocatedAttributes' attribute
lSuccess = AlpcInitializeMessageAttribute(
ulAttributeFlags, // attributes
pAttributeBuffer, // pointer to attributes structure
ulAllocBufSize, // buffer size
&lpReqBufSize
);
if (!NT_SUCCESS(lSuccess)) {
return NULL;
}
else {
//wprintf(L"Success.\n");
return pAttributeBuffer;
}
}
- Для MsgAttrSendбуфер включает еще два шага. Вы должны выделить буфер, достаточно большой для хранения ALPC_MESSAGE_ATTRIBUTES , а также всех атрибутов сообщений, которые вы хотите отправить (как и раньше). Вы должны установить AllocatedAttributesатрибут (так же, как и раньше), но затем вы также должны инициализировать атрибуты сообщения (имеется в виду создание необходимых структур и заполнение их допустимыми значениями), которые вы хотите отправить, а затем, наконец, установить ValidAttributesатрибут. В моем коде я хотел отправлять разные атрибуты в разных сообщениях, вот как я это сделал:
C++:
// Allocate buffer and initialize the specified attributes
pMsgAttrSend = setup_sample_message_attributes(hConnectionPort, hServerSection, ALPC_MESSAGE_SECURITY_ATTRIBUTE | ALPC_MESSAGE_VIEW_ATTRIBUTE | ALPC_MESSAGE_HANDLE_ATTRIBUTE);
// ...
// Before sending a message mark certain attributes as valid, in this case ALPC_MESSAGE_SECURITY_ATTRIBUTE
pMsgAttrSend->ValidAttributes |= ALPC_MESSAGE_SECURITY_ATTRIBUTE
lSuccess = NtAlpcSendWaitReceivePort(hConnectionPort, ...)
//...
- С буфером атрибутов отправки есть еще один нюанс: вам не нужно выделять или инициализировать атрибут контекста или атрибут токена . Ядро всегда будет подготавливать эти атрибуты, и получатель всегда может их запросить.
- Если вы хотите отправить несколько атрибутов сообщения, у вас будет буфер, начинающийся с ALPC_MESSAGE_ATTRIBUTES , за которым следуют инициализированные структуры для всех нужных вам атрибутов сообщения.
Так как же ядро узнает, какая структура атрибутов какая? Ответ: Вы должны расположить атрибуты сообщения в заранее определенном порядке, который можно угадать по значению их флагов атрибута сообщения (от высшего к низшему ) или найти в _KALPC_MESSAGE_ATTRIBUTES :
- Вы могли заметить, что контекста и маркера не отслеживаются в этой структуре, потому что ядро всегда предоставляет их для любого сообщения и, следовательно, отслеживает их сообщение независимо.
- После отправки ядро проверит все атрибуты сообщения, заполнит значения (например, порядковые номера) или очистит недопустимые атрибуты, прежде чем предложить их получателю.
- Наконец, ядро скопирует атрибуты, указанные получателем как AllocatedAttributesв приемник MsgAttrReceivedбуфер, откуда они могут быть получены получателем.
До сих пор мы представили различные компоненты ALPC, чтобы описать, как работает система обмена сообщениями ALPC и как выглядит сообщение ALPC. Позвольте мне завершить эту главу, рассмотрев некоторые из этих компонентов в перспективе. Приведенное выше описание и структура сообщения ALPC описывает, как сообщение ALPC выглядит для отправителя и получателя, но следует помнить, что ядро добавляет к этому сообщению гораздо больше информации — фактически оно берет предоставленные части и помещает их в гораздо большая структура сообщений ядра - как вы можете видеть ниже:
Таким образом, сообщение здесь таково: мы достигли хорошего понимания, но под капотом есть гораздо больше , чего мы не коснулись.
Собираем кусочки вместе: образец приложения
Я написал образец клиентского и серверного приложения ALPC в качестве игровой площадки для понимания различных компонентов ALPC. Не стесняйтесь просматривать и изменять код, чтобы получить собственное представление об ALPC. Несколько справедливых предупреждений об этом коде:- Код не предназначен для масштабирования/развития. Код предназначен для легкого чтения и руководства по основным шагам отправки/получения сообщений ALPC.
- Этот код никоим образом не оптимизирован по производительности, ресурсам или чему-то еще. Это для обучения.
- Я не удосужился предпринять какие-либо усилия по освобождению буферов, сообщений или любых других ресурсов (что связано с прямым путем атаки, как описано в разделе «Неосвобожденные объекты сообщений» ).
- Вы можете найти, как я устанавливаю атрибуты типовых сообщений здесь .
- Вы можете найти звонок NtAlpcSendWaitReceivePortкоторый и отправляет, и получает сообщение здесь .
- Вы можете найти флаги портов ALPC, флаги атрибутов сообщений, флаги сообщений и соединений здесь .
Поверхность атаки
Прежде чем углубляться в поверхность атаки каналов связи ALPC, я хотел бы указать на интересную концептуальную слабость связи ALPC, на которой основаны приведенные ниже пути атаки, и о которых следует помнить, чтобы найти дальнейший потенциал для использования.Оглядываясь назад на Поток сообщений ALPC» , мы можем вспомнить, что для обеспечения связи ALPC сервер должен открыть порт ALPC (соединение), дождаться входящих сообщений, а затем принять или отклонить эти сообщения. Хотя порт ALPC является защищаемым объектом ядра и может быть создан с помощью дескриптора безопасности , который определяет, кто может получить к нему доступ и подключиться к нему, в большинстве случаев процесс создания сервера ALPC не может (или не хочет) ограничивать доступ на основе вызываемого абонента SID . Если вы не можете (или хотите) ограничить доступ к вашему порту ALPC с помощью SID , единственный вариант, который у вас есть, — это разрешить всем подключаться к вашему порту и принимать/отклонять решение после того, как клиент подключится и отправит вам сообщение. Это, в свою очередь, означает, что многие встроенные серверы ALPC позволяют всем подключаться и отправлять сообщения на сервер. Даже если сервер сразу отклоняет клиента, отправки начального сообщения и некоторых атрибутов сообщения вместе с этим сообщением может быть достаточно для использования уязвимости.
Благодаря этой коммуникационной архитектуре и повсеместному распространению ALPC использование ALPC также является интересным способом избежать песочницы.
Определить цели
Первым шагом в отображении поверхности атаки является определение целей, которыми в нашем случае являются клиентские или серверные процессы ALPC.Обычно мне приходят в голову три способа идентифицировать такие процессы:
- Определите объекты порта ALPC и сопоставьте их с процессами-владельцами
- Проверить процессы и определить, используется ли в них ALPC
- Используйте отслеживание событий для Windows (ETW) для составления списка событий ALPC.
Найти объекты порта ALPC
Мы уже видели самый простой способ идентификации объектов порта ALPC в начале этого сообщения, который заключается в запуске WinObj и обнаружении объектов ALPC по столбцу «Тип». WinObj не может дать нам более подробной информации, поэтому мы обращаемся к WinDbg , чтобы проверить этот объект порта ALPC:
В приведенных выше командах мы использовали команду Windbg !object для запроса к диспетчеру именованного объекта по указанному пути. Это неявно уже говорило нам, что этот порт ALPC должен быть портом соединения ALPC , потому что порты связи не названы. В свою очередь мы можем сделать вывод, что мы можем использовать WinObj только для поиска портов подключения ALPC и через эти только серверные процессы ALPC.
Говоря о серверных процессах: как показано выше, можно использовать WinDbg. недокументированные !alpcКоманда для отображения информации о порте ALPC, который мы только что идентифицировали. Вывод включает в себя, наряду с большим количеством другой полезной информации, серверный процесс, владеющий портом, которым в данном случае является svchost.exe .
Теперь, когда мы знаем адрес объекта порта ALPC, мы можем использовать !alpcеще раз, чтобы отобразить активные соединения для этого порта соединения ALPC:
Примечание: команда !alpc Windbg не задокументирована, но устаревшая команда !lpc, которая существовала во времена LPC, задокументирована здесь и имеет отметку времени от декабря 2021 года. На этой странице документации упоминается, что команда !lpc устарела и что Вместо этого следует использовать команду !alpc, но синтаксис и параметры команды !alpc совершенно другие. Но, честно говоря, синтаксис команды !alpc отображается в WinDbg, если вы введете любую недопустимую команду !alpc:
Благодаря Джеймсу Форшоу и его NtObjectManager в .NET мы также можем легко запросить NtObjectManager в PowerShell для поиска объектов порта ALPC, и, что еще лучше, Джеймс уже предоставил для этого функцию-оболочку через Get-AccessibleAlpcPort .
Найти ALPC, используемый в процессах
Как всегда, существуют различные способы узнать об использовании портов ALPC в процессах, вот некоторые из них, которые пришли на ум:
- Подобно подходам в предыдущих сообщениях ( здесь ), можно использовать dumpbin.exe для вывода списка импортированных функций исполняемых файлов и поиска в них вызовов функций, специфичных для ALPC.
- Поскольку описанный выше подход работает с исполняемыми файлами на диске, но не с запущенными процессами, можно перенести метод, используемый dumpbin.exe и проанализировать таблицу адресов импорта (IAT) запущенных процессов, чтобы найти вызовы функций, специфичных для ALPC.
- Можно подключиться к запущенным процессам, запросить открытые дескрипторы для этого процесса и отфильтровать те дескрипторы, которые указывают на порты ALPC.
После установки dumpbin.exe , который, например, поставляется с пакетом разработки Visual Studio C++, можно использовать следующие два однострочника PowerShell для поиска .exe и .dll , которые создают или подключаются к порту ALPC:
Код с оформлением (BB-коды):
## Get ALPC Server processes (those that create an ALPC port)
Get-ChildItem -Path "C:\Windows\System32\" -Include ('*.exe', '*.dll') -Recurse -ErrorAction SilentlyContinue | % { $out=$(C:\"Program Files (x86)"\"Microsoft Visual Studio 14.0"\VC\bin\dumpbin.exe /IMPORTS:ntdll.dll $_.VersionInfo.FileName); If($out -like "*NtAlpcCreatePort*"){ Write-Host "[+] Executable creating ALPC Port: $($_.VersionInfo.FileName)"; Write-Output "[+] $($_.VersionInfo.FileName)`n`n $($out|%{"$_`n"})" | Out-File -FilePath NtAlpcCreatePort.txt -Append } }
## Get ALPC client processes (those that connect to an ALPC port)
Get-ChildItem -Path "C:\Windows\System32\" -Include ('*.exe', '*.dll') -Recurse -ErrorAction SilentlyContinue | % { $out=$(C:\"Program Files (x86)"\"Microsoft Visual Studio 14.0"\VC\bin\dumpbin.exe /IMPORTS:ntdll.dll $_.VersionInfo.FileName); If($out -like "*NtAlpcConnectPor*"){ Write-Host "[+] Executable connecting to ALPC Port: $($_.VersionInfo.FileName)"; Write-Output "[+] $($_.VersionInfo.FileName)`n`n $($out|%{"$_`n"})" | Out-File -FilePath NtAlpcConnectPort.txt -Append } }
Я не кодировал 2-й вариант (разбор IAT) — если вы знаете инструмент, который это делает, дайте мне знать , но есть простой, но очень медленный способ решить вариант № 3 (найти дескрипторы ALPC в процессах), используя следующее Команда WinDbg: !handle 0 2 0 ALPC Port
Имейте в виду, что это очень медленно и, вероятно, займет несколько часов (я остановился через 10 минут и получил только около 18 дескрипторов).
Но еще раз спасибо Джеймсу Форшоу и его NtApiDotNet , есть более простой способ написать это самостоятельно и ускорить этот процесс, плюс мы также можем получить интересную статистику ALPC…
Вы можете найти этот инструмент здесь
Обратите внимание, что эта программа не запускается в пространстве ядра, поэтому я ожидаю лучших результатов с помощью команды WinDbg, но она выполняет свою работу по отображению некоторых портов ALPC, используемых различными процессами. Перебирая все процессы, к которым у нас есть доступ, мы также можем рассчитать некоторые базовые статистические данные об использовании ALPC, как показано выше. Эти цифры не являются точными на 100%, но учитывая, что в среднем около 14 дескрипторов коммуникационных портов ALPC используются для каждого процесса, мы можем определенно заключить, что ALPC довольно часто используется в Windows.
Как только вы определите процесс, который звучит как интересная цель, WinDbg можно использовать снова, чтобы копнуть глубже…
Используйте трассировку событий для Windows
Хотя ALPC не некоторые события ALPC отображаются как события Windows, которые можно зафиксировать с помощью отслеживания событий для Windows (ETW). Одним из инструментов, помогающих с ALPC-событиями, является ProcMonXv2 от zodiacon .
Имперсонация и неимперсонация
Для коммуникации ALPC процедуры имперсонализации привязаны к сообщениям, что означает, что и клиент, и сервер (а также каждая из общающихся сторон) могут выдавать себя за пользователя на другой стороне. Однако, для того, чтобы разрешить выдавать себя за другого, партнер по коммуникации должен разрешить выдавать себя за другого, а партнер по коммуникации, выдающий себя за другого, должен обладать привилегией SeImpersonate (это все еще защищенный канал связи, верно?)...
Глядя на код, кажется, что есть два варианта выполнения первого условия, которое заключается в разрешении быть выданным за другого:
Первый вариант: Через PortAttributes, например, так:
C++:
// QOS
SecurityQos.ImpersonationLevel = SecurityImpersonation;
SecurityQos.ContextTrackingMode = SECURITY_STATIC_TRACKING;
SecurityQos.EffectiveOnly = 0;
SecurityQos.Length = sizeof(SecurityQos);
// ALPC Port Attributs
PortAttributes.SecurityQos = SecurityQos;
PortAttributes.Flags = ALPC_PORTFLG_ALLOWIMPERSONATION;
- Второй вариант: Через ALPC_MESSAGE_SECURITY_ATTRIBUTEатрибут сообщения
C++:
pMsgAttrSend = setup_sample_message_attributes(hSrvCommPort, NULL, ALPC_MESSAGE_SECURITY_ATTRIBUTE); // setup security attribute
pMsgAttrSend->ValidAttributes |= ALPC_MESSAGE_SECURITY_ATTRIBUTE; // specify it to be valid for the next message
NtAlpcSendWaitReceivePort(...) // send the message
Однако есть загвоздка:
- Если сервер (тот, у которого есть дескриптор порта подключения) хочет олицетворять клиента, то олицетворение разрешено, если клиент указал ЛИБО первый вариант ИЛИ второй (или оба, но одного варианта достаточно).
- Однако, если клиент хочет выдать себя за сервер, сервер должен предоставить второй вариант. Другими словами: сервер должен отправить ALPC_MESSAGE_SECURITY_ATTRIBUTEчтобы позволить клиенту олицетворять сервер.
Я рассмотрел оба маршрута: сервер, выдающий себя за клиента, и клиент, выдающий себя за сервер.
Мой первый путь заключался в поиске клиентов, пытающихся подключиться к несуществующему порту сервера, чтобы проверить условия имперсонализации. Я пробовал разные методы, но пока не нашел хорошего способа идентифицировать таких клиентов. Мне удалось использовать точки останова в ядре, чтобы вручную определить некоторые случаи, но пока не удалось найти ничего интересного, что позволило бы олицетворять клиента. Ниже приведен пример «ApplicationFrameHost.exe», пытающийся подключиться к несуществующему порту ALPC, который я мог перехватить с помощью своего демонстрационного сервера, однако процесс не допускает имперсонализации (и приложение работает как мой текущий пользователь) …
Не слишком удачная попытка персонализации, но, по крайней мере, она подтверждает идею.
Переходим к другому пути: Я нашел кучу портов подключения ALPC, используя Get-AccessibleAlpcPort, как было показано ранее, и дал команду своему ALPC клиенту подключиться к ним, чтобы проверить, позволяют ли они мне а) подключиться, б) отправить мне какое-либо фактическое сообщение обратно и в) отправить атрибуты сообщения имперсонализации вместе с сообщением. Для всех проверенных мной портов подключения ALPC в лучшем случае я получал короткое инициализационное сообщение с атрибутом ALPC_MESSAGE_CONTEXT_ATTRIBUTE, что не полезно для имперсонизации, но, по крайней мере, еще раз демонстрирует идею:
Неимперсонация сервера
В части цикла, посвященной RPC, я упоминал, что может быть интересно подключиться к серверу, который использует имперсонацию для изменения контекста безопасности своего потока на контекст безопасности вызывающего клиента, но не проверяет, удалась или не удалась имперсонация. В таком случае сервер может быть обманут и заставить выполнять задачи со своим собственным - потенциально повышенным - контекстом безопасности. Как подробно описано в статье о RPC, поиск таких случаев сводится к индивидуальному анализу конкретного процесса сервера ALPC, который вы рассматриваете. Для этого вам потребуется следующее:
Серверный процесс, открывающий порт ALPC, к которому может подключиться ваш клиент.
Сервер должен принимать сообщения о соединении и должен пытаться выдать себя за сервер
Сервер не должен проверять, удалось или не удалось выдать себя за другого.
(В соответствующих случаях сервер должен работать в другом контексте безопасности, чем ваш клиент, а именно: другой пользователь или другой уровень целостности).
На данный момент я не могу придумать хороший способ автоматизировать или полуавтоматизировать процесс поиска таких целей. Единственный вариант, который приходит мне на ум, это поиск портов подключения ALPC и реверсирование процессов хостинга.
Я буду обновлять этот пост, если наткнусь на что-нибудь интересное в этом направлении, но для основной части я хотел повторить путь атаки при неудачных попытках имперсонации.
Несвободные объекты сообщений
Как упоминалось в разделе Атрибуты сообщений ALPC, существует несколько атрибутов сообщений, которые клиент или сервер может отправить вместе с сообщением. Одним из них является атрибут ALPC_DATA_VIEW_ATTR, который может быть использован для отправки информации о сопоставленном представлении другой стороне коммуникации.
Напомнить: Это можно использовать, например, для хранения больших сообщений или данных в разделяемом представлении и отправки дескриптора этого разделяемого представления другой стороне вместо использования механизма обмена сообщениями с двойным буфером для копирования данных из одного пространства памяти в другое.
Интересным моментом здесь является то, что разделяемое представление (или раздел, как он называется в Windows) отображается в пространство процесса получателя, когда на него ссылается атрибут ALPC_DATA_VIEW_ATTR. Затем получатель может что-то сделать с этим разделом (если он знает о его отображении), но в конечном итоге получатель сообщения должен убедиться, что отображенное представление освобождено из собственного пространства памяти, а это требует определенного количества шагов, которые могут быть выполнены неправильно. Если получателю не удается освободить сопоставленное представление, например, потому что он вообще не ожидал получить представление, отправитель может посылать все больше и больше представлений с произвольными данными, чтобы заполнить пространство памяти получателя представлениями с произвольными данными, что сводится к атаке Heap Spray.
Я узнал об этом векторе атаки ALPC, только прослушав (в очередной раз) доклад Алекса Ионеску SyScan ALPC Talk, и я думаю, что нет способа лучше сформулировать и показать, как работает этот вектор атаки, чем он сам в этом докладе, поэтому я не буду копировать его содержание и слова и просто укажу вам на 32-ю минуту его доклада, где он начинает объяснять атаку. Также вам стоит посмотреть 53-ю минуту его выступления, чтобы увидеть демонстрацию атаки heap spray.
Та же логика применяется к другим атрибутам сообщения ALPC, например, к дескрипторам, которые отправляются в ALPC_MESSAGE_HANDLE_INFORMATION через атрибут дескриптора ALPC.
Поиск уязвимых целей для этого типа атак — это, опять же, индивидуальный процесс расследования, в котором необходимо:
- Найдите процессы (интересующие), используя связь ALPC
- Определите, как целевой процесс обрабатывает атрибуты сообщения ALPC, особенно если атрибуты сообщения ALPC освобождены.
- Проявите творческий подход к вариантам злоупотребления неосвобожденными ресурсами, где очевидным вариантом PoC будет исчерпание памяти процесса.
В качестве примера я заполнил свой тестовый сервер ALPC представлениями размером 20 КБ, как показано ниже:
Это действительно работает, потому что я не какие пытался освободить либо выделенные атрибуты на моем тестовом сервере ALPC.
Я также случайным образом выбрал несколько — например, четыре или пять — процессов ALPC от Microsoft (которые я идентифицировал с помощью показанных выше методов), но те, которые я выбрал, похоже, не совершают ту же ошибку.
Честно говоря, может быть полезно проверить больше процессов для этого, но, насколько мне известно, мне не нужна такая ошибка, кроме сбоя процесса, который, если он достаточно критичен, также может привести к сбою ОС (отказ в обслуживании).
Интересное примечание :
В своем выступлении Алекс Ионеску упоминает, что диспетчер памяти Windows выделяет области памяти на границах 64 КБ, что означает, что всякий раз, когда вы выделяете память, диспетчер памяти помещает эту память в начало следующего доступного блока размером 64 КБ. Что позволяет вам, как злоумышленнику, создавать и отображать представления произвольного размера (желательно меньше 64 КБ, чтобы сделать исчерпание памяти эффективным), а ОС будет отображать представление в памяти сервера и помечать 64 КБ-YourViewSize как неиспользуемую память, потому что это необходимо выровнять все распределение памяти по границам 64 КБ. Вы хотите увидеть 54-ю минуту выступления Алекса, чтобы получить визуальное и словесное объяснение этого эффекта.
Рэймонд Чен объясняет причину гранулярности в 64 КБ здесь .
В конце концов, атаки с исчерпанием памяти, конечно, не единственный жизнеспособный вариант использования примитива памяти/кучи, который люди умнее меня могут превратить в путь эксплойта…
Заключение
ALPC недокументирован и довольно сложен, но в качестве мотивационного преимущества: уязвимости внутри ALPC могут стать очень серьезными, поскольку ALPC повсеместно распространен в ОС Windows, все встроенные процессы с высокими привилегиями используют ALPC, и благодаря своей коммуникационной архитектуре это привлекательная цель даже с точки зрения песочницы.В ALPC есть гораздо больше, чем я рассказал в этом посте. Потенциально можно было бы написать целую книгу об ALPC, но я надеюсь, что хотя бы коснулся основ, чтобы вы начали интересоваться ALPC.
Чтобы получить первое впечатление «Где и сколько ALPC находится на моем ПК», я рекомендую запустить ProcMonXv2 (от zodiacon ) на вашем хосте, чтобы увидеть тысячи событий ALPC, запускаемых за несколько секунд.
Чтобы продолжить оттуда, вы можете найти мой код клиента и сервера ALPC полезным для игры с процессами ALPC, а также для выявления и использования уязвимостей в ALPC. Если вы обнаружите, что кодируете и/или исследуете ALPC, обязательно ознакомьтесь с справочным разделом, чтобы узнать, как другие справились с ALPC.
Наконец, в качестве последнего слова и в заключение моей рекомендации с самого начала: если вы чувствуете, что можете услышать другой голос и точку зрения на ALPC, я настоятельно рекомендую взять еще один напиток и насладиться следующим часом Алекса Ионеску о LPC, RPC и АЛК:
Приложение A: Использование портов подключения и связи
Изучая ALPC, я сначала подумал, что сервер прослушивает свой коммуникационный порт , который он получает, когда принимает клиентское соединение через NtAlpcConnectPort . Это имело бы смысл, так как это называется коммуникационным портом. Однако прослушивание входящих сообщений на коммуникационном порту сервера привело к блокировке вызова NtAlpcSendWaitReceivePort , который так и не вернулся с сообщением.ALPC сервера, коммуникационном должно быть, было неверным, что меня озадачило, поскольку клиент на другой стороне действительно получает сообщения на свой коммуникационный порт. Я некоторое время зависал над этим вопросом, пока не связался с Алексом Ионеску , чтобы спросить его об этом, и узнал, что мое предположение было действительно неверным, а точнее, со временем стало неверным: Алекс объяснил мне, что идея, которую я имел (сервер прослушивает и отправляет сообщения через свой коммуникационный порт) был способ, которым LPC (предшественник ALPC) был разработан для работы. Однако такой дизайн заставит вас прослушивать растущее число коммуникационных портов с каждым новым клиентом, которого принимает сервер. Представьте, что с сервером разговаривают 100 клиентов, затем серверу необходимо прослушивать 100 коммуникационных портов, чтобы получать клиентские сообщения, что часто приводит к созданию 100 потоков, где каждый поток взаимодействует с другим клиентом. Это было сочтено неэффективным, и гораздо более эффективным решением было иметь один поток, прослушивающий (и отправляющий) порт подключения сервера, где все сообщения отправляются на этот порт подключения.
Это, в свою очередь, означает: сервер принимает клиентское соединение, получает дескриптор коммуникационного порта клиента, но по-прежнему использует дескриптор порта соединения сервера в вызовах NtAlpcSendWaitReceivePort для отправки и получения сообщений от всех подключенных клиентов.
Означает ли это, что коммуникационный порт сервера устарел (и это был мой дополнительный вопрос Алексу )? Его ответ снова имел смысл и прояснил мое понимание ALPC: порт связи сервера для каждого клиента используется внутри ОС для привязки сообщения, отправленного конкретным клиентом, к конкретному порту связи этого клиента. Это позволяет ОС связать специальную структуру контекста с каждым клиентским портом связи, которая может использоваться для идентификации клиента. Этой специальной контекстной структурой является PortContext , которая может быть любой произвольной структурой, которую можно передать в NtAlpcAcceptConnectPort и которую впоследствии можно извлечь из любого сообщения с ALPC_CONTEXT_ATTR .
Это означает: когда сервер прослушивает свой порт соединения, он получает сообщения от всех клиентов, но если он хочет знать, какой клиент отправляет сообщение, сервер может получить структуру контекста порта (через ALPC_CONTEXT_ATTR ), которую он назначил для этот клиент после принятия соединения, и ОС будет получать эту структуру контекста из внутренне сохраненного порта связи клиента.
На данный момент мы можем заключить, что коммуникационный порт сервера для каждого клиента по-прежнему важен для ОС и по-прежнему имеет свое место и роль в коммуникационной структуре ALPC. Это, однако, не отвечает на вопрос, зачем серверу на самом деле нужен дескриптор порта связи каждого клиента (поскольку клиентский PortContext может быть извлечен из сообщения, полученного с помощью дескриптора порта соединения).
Ответ здесь — имперсонализация . Когда сервер хочет олицетворять клиента, ему необходимо передать порт связи клиента в NtAlpcImpersonateClientOfPort . Причина этого в том, что информация о контексте безопасности, необходимая для выполнения олицетворения, привязана (если это разрешено клиентом) к коммуникационному порту клиента. Не имеет смысла привязывать эту информацию к порту подключения, потому что все клиенты используют этот порт подключения, а каждый клиент имеет свой уникальный порт связи для каждого сервера.
Следовательно: если вы хотите имперсонализировать своих клиентов, вы хотите сохранить дескриптор порта связи каждого клиента.
Рекомендации
Ниже приведены несколько ресурсов, которые я считаю полезными для изучения и изучения ALPC.Справочные проекты, в которых используется ALPC
- https://github.com/microsoft/terminal/blob/main/src/interactivity/onecore/ConIoSrvComm.cpp
- https://github.com/DownWithUp/ALPC-пример
- https://github.com/DynamoRIO/drmemory
- https://github.com/hakril/PythonForWindows
- https://docs.rs/
- https://github.com/googleprojectzero/sandbox-attacksurface-analysis-tools
- https://processhacker.sourceforge.io/doc/ntlpcapi_8h.html
- https://github.com/bnagy/w32
- https://github.com/taviso/ctftool
- https://github.com/googleprojectzero/sandbox-attacksurface-analysis-tools/blob/main/NtApiDotNet/NtAlpcNative.cs
- https://processhacker.sourceforge.io/doc/ntlpcapi_8h.html
- https://github.com/hakril/PythonForWindows/blob/master/windows/generated_def/windef.py
- Youtube: SyScan'14 Singapore: все о Rpc, Lrpc, Alpc и Lpc на вашем компьютере, Алекс Ионеску
- Слайды: SyScan'14 Singapore: все о RPC, Lrpc, Alpc и Lpc на вашем компьютере, Алекс Ионеску
- Youtube: Hack.lu 2017 Взгляд Клемана Руо и Томаса Имберта на ALPC-RPC
- Слайды: Инструментарий фаззинга ALPC