[Перевод] Windows Notification Facility: cамая недокументированная поверхность атаки
Под катом представлен перевод презентации «The Windows Notification Facility: The Most Undocumented Kernel Attack Surface Yet», представленной Alex Ionescu и Gabrielle Viala на конференции BlackHat 2018.
Что такое Windows Notification Facility (WNF)
Windows Notification Facility это механизм уведомлений (доступный как в ядре, так и в пользовательском режиме), который строится на модели издатель-подписчик (pubsub, Publisher/Subscriber). Механизм был добавлен в Windows 8: частично для того, чтобы решить некоторые давние конструктивные ограничения в ОС, но также он должен был послужить основой для реализации Push-уведомлений, аналогичных iOS/Android.
Его ключевой особенностью является то, что это слепая (в основном — без регистрации) модель, которая допускает неупорядоченную подписку и публикацию. Под этим подразумевается, что потребитель может подписаться на уведомление даже до того, как уведомление было опубликовано его источником. И что тот, кто генерирует события, не обязан «регистрировать» уведомление заранее.
Помимо всего прочего механизм поддерживает:
- постоянные и временные нотификации
- монотонно увеличивающиеся уникальные идентификаторы
- буфер полезной нагрузки (до 4-х килобайт) для каждого события
- модель уведомлений на основе пула потоков (thread-pool) с сериализацией на основе групп
- модель безопасности, основанная на области видимости и реализующая дескрипторы безопасности через стандартный механизм DACL / SACL
Почему появился WNF
Рассмотрим канонический пример: есть драйвер, который хочет знать о том, что был подключен том с доступом на чтение и запись. Чтобы уведомить об этом, Autochk (аналог fsck в Windows) сообщает о событии под названием VolumesSafeForWriteAccess. Но чтобы сообщить о событии нужно сначала создать сам объект события.
Нам так же надо знать, что Autochk уже работает над томом, но еще не сигнализировал событие, которого мы ждем. Плохое решение: сидеть в цикле со sleep (), проверяя наличие события, и когда событие будет создано — подождать его.
Но после выхода из приложения Windows все его дескрипторы закрываются. А когда у объекта нет дескрипторов, он уничтожается. Так кто же будет держать это событие?
Без WNF решение состоит в том, чтобы ядро ОС создало событие до того, как загрузятся какие-либо драйверы, и чтобы Autochk открывал его, как это делал бы потребитель, но вместо ожидания он должен сигнализировать это событие.
Имена состояний (State Names) WNF
В мире WNF имя состояния — это 64-х битное число. Но есть хитрость — на самом деле это закодированная структура. Имя состояния имеет версию, время жизни, область видимости, флаг постоянства данных и уникальный порядковый номер.
typedef struct _WNF_STATE_NAME_INTERNAL
{
ULONG64 Version:4;
ULONG64 NameLifetime:2;
ULONG64 DataScope:4;
ULONG64 PermanentData:1;
ULONG64 Unique:53;
} WNF_STATE_NAME_INTERNAL, *PWNF_STATE_NAME_INTERNAL;
Но эти данные станут доступны, только если мы про-XOR’им 64-х битное число с магической константой:
#define WNF_STATE_KEY 0x41C64E6DA3BC0074
Время жизни (Lifetime) имени состояния
Имя состояния WNF может быть (WNF_STATE_NAME_LIFETIME):
- общеизвестным (well-known)
- постоянным (permanent)
- устойчивым (persistent)
- временным (temporary)
Первые три связаны с соответствующими ключами в реестре, где будет храниться информация о состоянии:
- общеизвестные имена живут в HKLM\SYSTEM\CurrentControlSet\Control\Notifications
- постоянные имена живут в HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Notifications
- устойчивые имена живут в HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\VolatileNotifications
У общеизвестных имен есть своя особенность: их нельзя зарегистрировать. Такое имя уже должно быть представлено в реестре на момент загрузки системы. Постоянные и устойчивые имена требуют для своего создания включенную привилегию SeCreatePermanentPrivilege (как и другие глобальные объекты). Устойчивые имена живут и вне процесса-регистратора, а постоянные имена переживают перезагрузку системы.
Область видимости (Scope) данных
Область видимости данных определяет первую границу безопасности вокруг имени состояния WNF, она определяет кто видит его и имеет у нему доступ. Область видимости имени состояния может быть:
- система
- машина
- пользовательская сессия
- пользователь
- процесс
Помимо обеспечения границ безопасности, области видимости WNF могут использоваться для предоставления разных экземпляров данных для одного и того же имени. Ядро (как и в случае с другими механизмах безопасности) обходит проверки доступа к состоянию. Привилегия TCB позволяет осуществлять междиапазонный (cross-scope) доступ к именам состояний WNF.
Область видимости «система» и область видимости «машина» это глобальные области видимости. Они не имеют собственных идентификаторов (они используют разные глобальные контейнеры). Область видимости пользовательской сессии использует в качестве ID идентификатор сессии (session ID). Область видимости конкретного пользователя использует в качестве идентификатора SID этого пользователя. В качестве идентификатора области видимости процесса выступает адрес объекта EPROCESS.
Порядковые номера (Sequence Numbers)
Чтобы гарантировать уникальность, каждое имя состояния имеет уникальный 51-битный порядковый номер. Общеизвестные (well-known) имена включают в свой порядковый номер 4-символьный тег семейства, а оставшиеся 21 бит используются в качестве уникального идентификатора. Постоянные имена хранят свой увеличивающийся номер значением реестра «SequenceNumber». Устойчивые и временные имена используют общий возрастающий счетчик, который расположен в глобальной переменной. Эти данные хранятся и обрабатываются отдельно для каждого контейнера (per-silo) и доступны в PspHostSiloGlobals→WnfSiloState.
Внутри Microsoft каждое имя WNF имеет «дружественный» идентификатор, который используется в коде, иногда он хранится в пространстве глобальных имен с тем же именем. Например — символ nt! WNF_BOOT_DIRTY_SHUTDOWN, который имеет значение 0×1589012fa3bc0875. После XOR’а с магической константой WNF_STATE_KEY получаем значение 0×544f4f4200000801, которое можно побитово трактовать как:
BOOT1, Well-Known Lifetime, System Scope, Version 1
Системные вызовы для работы с WNF
Системные вызовы ядра позволяют регистрировать и удалять имена состояний WNF, публиковать и получать данные имен состояний WNF, а так же получать различные уведомления от WNF.
Регистрация имени состояния WNF
За исключением общеизвестных имен (как уже помянуто ранее), имя состояния WNF может быть зарегистрировано во время работы ОС:
NTSTATUS
ZwCreateWnfStateName (
_Out_ PWNF_STATE_NAME StateName,
_In_ WNF_STATE_NAME_LIFETIME NameLifetime,
_In_ WNF_DATA_SCOPE DataScope,
_In_ BOOLEAN PersistData,
_In_opt_ PCWNF_TYPE_ID TypeId, // необязательная возможность обеспечения безопасности типов
_In_ ULONG MaximumStateSize, // не больше 4-х килобайт
_In_ PSECURITY_DESCRIPTOR SecurityDescriptor // *ОБЯЗАТЕЛЬНЫЙ* параметр
);
Существует симметричный системный вызов ZwDeleteWnfStateName с помощью которого можно удалить зарегистрированное имя состояния (опять же, кроме общеизвестных).
Публикация данных состояния WNF
Чтобы задать или изменить данные имени состояния WNF, можно использовать системный вызов ZwUpdateWnfStateData:
NTSTATUS
ZwUpdateWnfStateData (
_In_ PCWNF_STATE_NAME StateName,
_In_reads_bytes_opt_(Length) const VOID* Buffer,
_In_opt_ ULONG Length, // Должно быть меньше, чем значение MaximumSize, указанное при регистрации
_In_opt_ PCWNF_TYPE_ID TypeId, // необязательная возможность обеспечения безопасности типов
_In_opt_ const PVOID ExplicitScope, // Описатель процесса, SID пользователя, идентификатор (ID) сессии
_In_ WNF_CHANGE_STAMP MatchingChangeStamp, // Ожидаемая текущая метка изменений
_In_ LOGICAL CheckStamp // Требование соблюдения совпадения вышеуказанной метки или игнорирование ее
);
Для удаления (очистки) данных имени состояния WNF существует симметричный системный вызов ZwDeleteWnfStateData.
Получение данных состояния WNF
Для того, что бы запросить данные имени состояния WNF может использоваться следующий системный вызов (большинство параметров аналогичны функции Update):
NTSTATUS
ZwQueryWnfStateData (
_In_ PCWNF_STATE_NAME StateName,
_In_opt_ PCWNF_TYPE_ID TypeId,
_In_opt_ const VOID* ExplicitScope,
_Out_ PWNF_CHANGE_STAMP ChangeStamp,
_Out_writes_bytes_to_opt_(*BufferSize, *BufferSize) PVOID Buffer,
_Inout_ PULONG BufferSize // Можно указать 0, что бы получить текущий размер
);
Настоящая сила заложена в том, что API-функции Update и Query по факту не требуют зарегистрированного имени состояния WNF. А если имя не является временным (и у вызывающего кода есть достаточные привилегии), экземпляр имени может быть зарегистрирован в реальном времени!
Уведомления WNF
До сих пор мы предполагали, что потребитель знает, когда вызывать функцию получения данных. Но также есть блокирующие чтение, которое работает с использованием системы уведомлений (что ближе к истинной модели издатель-подписчик).
Во-первых, процесс должен зарегистрировать событие вызовом функции ZwSetWnfProcessNotificationEvent. Затем нужно вызвать функцию ZwSubscribeWnfStateChange, указав маску событий, что бы получить идентификатор подписки на выходе. События могу быть двух типов:
- Уведомления о данных (data notifications):
- 0×01 — появление данных
- 0×10 — уничтожение имени
- Мета-уведомления (meta metanotifications)
- 0×02 — появление подписчика, получающего уведомления о данных (Data Subscriber)
- 0×04 — появление подписчика, получающего мета-уведомления (Meta Subscriber)
- 0×08 — появление подписчика, получающего уведомления о данных и мета-уведомления (Generic Subscriber)
Затем необходимо дождаться события, которое было зарегистрировано. И всякий раз, как событие становится сигнальным, нужно вызывать функцию ZwGetCompleteWnfStateSubscription, которая возвращает WNF_DELIVERY_DESCRIPTOR.
Но у этих низкоуровневых API-функций есть проблема (спасибо Gabi за ее иследование): у каждого процесса может существовать только одно зарегистрированное событие.
Высокоуровневое API пользовательского режима (ntdll)
Когда дело доходит до уведомлений, все усложняется, поэтому Rtl-слой из ntdll.dll предоставляет более простой интерфейс:
NTSTATUS
RtlSubscribeWnfStateChangeNotification (
_Outptr_ PWNF_USER_SUBSCRIPTION* Subscription,
_In_ WNF_STATE_NAME StateName,
_In_ WNF_CHANGE_STAMP ChangeStamp,
_In_ PWNF_USER_CALLBACK Callback,
_In_opt_ PVOID CallbackContext,
_In_opt_ PCWNF_TYPE_ID TypeId,
_In_opt_ ULONG SerializationGroup,
_In_opt_ ULONG Unknown
);
Фактически, нет необходимости вызывать системные сервисы напрямую: достаточно использовать единую управляемую ntdll.dll очередь событий.
За кулисами содержимое WNF_DELIVERY_DESCRIPTOR преобразуется в параметры обратного вызова:
typedef NTSTATUS (*PWNF_USER_CALLBACK) (
_In_ WNF_STATE_NAME StateName,
_In_ WNF_CHANGE_STAMP ChangeStamp,
_In_opt_ PWNF_TYPE_ID TypeId,
_In_opt_ PVOID CallbackContext,
_In_ PVOID Buffer,
_In_ ULONG BufferSize);
Для каждой новой подписки заводится запись, которая помещается в список, на который указывает глобальная переменная RtlpWnfProcessSubscriptions. Список строится на одном из полей WNF_NAME_SUBSCRIPTION, которое имеет тип LIST_ENTRY. Каждая из WNF_NAME_SUBSCRIPTION, в свою очередь, имеет еще одно поле LIST_ENTRY для организации списка из WNF_USER_SUBSCRIPTION с обратным вызовом и контекстом.
Высокоуровневое API уровня ядра (Ex)
WNF также предоставляет практически идентичные функции для кода режима ядра (которые можно использовать из драйвера): как через экспортированные системные вызовы, так и через высокоуровневые API-функции в исполнительной среде выполнения (Ex-слой).
Функция ExSubscribeWnfStateChange принимает на вход имя состояния, маски типов и адрес функции обратного вызова + контекст, а возвращает дескриптор подписки. Функции обратного вызова получают целевое имя, маску события, метку изменения, но не буфер или его размер.
Функция ExQueryWnfStateData по переданному дескриптору подписки читает текущие данные состояния. Фактически, каждый обратный вызов заканчивает тем, что вызывается функция ExQueryWnfStateData, чтобы получить данные, связанные с уведомлением.
И для подписок режима ядра, и для подписок пользовательского режима WNF (для отслеживания подписки) создает экземпляр структуры WNF_SUBSCRIPTION. Но для пользовательского режима некоторые поля не будут заполнены, например Callback и Context, так как для пользовательского режима адреса обработчиков хранит и обрабатывает ntdll.dll.
Структуры данных WNF
От переводчика: смотри следующий раздел.
Утилиты анализа WNF
От переводчика: тут стоит снова напомнить, что презентация велась не только Alex’ом, но еще и Gabrielle Viala. В частности, её авторству принадлежит описанный далее модуль WnfCom. Кроме того Gabrielle достаточно подробно описала внутренние структуры WNF (смотри иллюстрацию в предыдущем разделе). Большая часть ее слайдов, к сожалению, отсутствует в pdf презентации (указанной в качестве оригинала) или обозначена исключительно заголовками. Но:
И еще от переводчика: Если кто-то захочет дополнить текущий перевод содержимым слайдов Gabrielle или расширить переводом стенографии из любой части видеозаписи выступления — welcome. Для удобства добавления/изменения больших кусков могу опубликовать исходник перевода на github (или другом сервере системы контроля версий).
WnfCom
WnfCom это python-модуль (исходный код на github), который показывает возможность взаимодействия посредством WNF. Ключевые возможности:
- позволяет читать/писать данные существующих экзепляров имен
- позволяет создавать временные имена состояний (в виде сервера)
- позволяет получать экземпляр объекта клиентской стороны, который будет обрабатывать уведоления об изменении конкретного экземпляра имени
Пример использования:
>>> from wnfcomimport Wnfcom
>>> wnfserver = Wnfcom()
>>> wnfserver.CreateServer()
[SERVER] StateNamecreated: 41c64e6da5559945
>>> wnfserver.Write(b"potatosoup")
Encoded Name: 41c64e6da5559945, Clear Name: 6e99931
Version: 1, Permanent: No, Scope: Machine, Lifetime: Temporary, Unique: 56627
State update: 11 bytes written
>>> from wnfcomimport Wnfcom
>>> wnfclient = Wnfcom()
>>> wnfclient.SetStateName("41c64e6da5559945")
>>> wnfclient.Listen()
[CLIENT] Event registered: 440
[CLIENT] Timestamp: 0x1 Size: 0xb
Data:00000000: 70 6F 74 61 74 6F 20 73 6F 75 70 potato soup
WnfDump
WnfDump это утилита командной строки, написанная на C. Исполняемый файл можно найти в https://github.com/ionescu007/wnfun, выбрав под-директорию нужной разрядности. Утилита может быть использована для поиска информации об именах состояний WNF:
- -d (Dump) Дамп всех имен состояний WNF с использованием перечисления на основе реестра. Можно дополнять опциями:
- -v (Verbose) Подробный вывод, который включает в себя шестнадцатеричный дамп данных состояний WNF;
- -s (Security) Дескрипторы безопасности — SDDL-строки разрешений имени состояния WNF.
- -b (Brute-force) Прямой перебор временных имен состояний WNF (более побробно об этом будет рассказано далее)
- -i (Information) Отображение информацию об одном указанном имени состояния WNF
- -r (Read) Чтение данных указанного имени состояния WNF
- -w (Write) Запись данных в указанное имя состояния WNF
- -n (Notification) Регистрация подписчика уведомлений для указанного имени состояния WNF (далее будет более конкретный пример использования с Edge)
Поверхность атаки на WNF
В этом разделе (точнее его подразделах) пойдет речь о возможных атаках и интересных чувствительных данных WNF.
Раскрытие привелегированных данных
Читая тысячи имен состояний WNF, существующих в системе, можно отметить несколько, данные которых выглядят весьма интересно. В их числе были некоторые, данные которых подозрительно похожи на указатели или другие привилегированные данные.
После воспроизведения на нескольких машинах, в некоторых случаях удавалось обнаружить кучу, стек и другую привилегированную информацию, разглашаемую через границы привилегий. Отчеты об ошибках/уязвимостях были переданы в MSRC июле, но исправлены в ноябре (уже после презентации). Например: через событие WNF_AUDC* утекало 4 килобайта стека!
Основные проблемы все те же, что мы видели в предыдущих исследованиях от j00ro, taviso, и других. Определенные имена состояний WNF содержат закодированные структуры данных с различными проблемами заполнения и/или выравнивания. В некоторых случаях утекает неинициализированная память.
От переводчика: перевод вступительной части документа Detecting Kernel Memory Disclosure with x86 Emulation and Taint Tracking от Mateusz Jurczyk aka j00ro.
Обнаружение имен состояний и разрешений
Первый подход состоял в том, чтобы обнаружить все возможные имена состояний, которыми можно было бы манипулировать злонамеренно. Для общеизвестных (well-known), постоянных (permanent) и устойчивых (persistent) имен перечисление выполнимо путем перечисления разделов реестра. Затем найденные значения можно сопоставить с дружественными идентификаторами (есть несколько мест, где их можно найти :))
Затем мы также можем посмотреть в реестре дескриптор безопасности (это первое, что находится в буфере данных). Дескриптор безопасности не каноничен: у него нет владельца и группы, поэтому технически он недействителен. Но нет никакой проблемы подставить поддельных владельца и группу, что бы исправить дескриптор безопасности.
Обнаружение временных имен состояний и их разрешений
Но с временными (temporary) именами описанные выше трюки не пройдут: их нет в реестре. И только ядро хранит в памяти структуры данных для них (! wnf). Но временные имена в действительности не так сложно перебрать (brute force):
- Версия всегда имеет значение 1
- Время жизни (lifetime) всегда имеет значение WnfTemporaryStateName
- Флаг permanent всегда сброшен (временное имя состояния не может иметь постоянных данных)
- Область видимости (scope) может принимать одно из 4-х значений
Да, но оставшийся порядковый номер это 51 бит! Действительно…, но не стоит забывать, что порядковые номера монотонно растут. И для временных имен последовательность сбрасывается до 0 при каждой загрузке. Условно, можно взять окно в миллион порядковый номеров: в цикле проверять существование каждого имени (начиная от 0) вызовом ZwQueryWnfStateNameInformation с запрашиваемым классом информации WnfInfoStateNameExist (учитывая, что ошибка доступа тоже говорит о существовании имени). Если очередной миллион имен не существует, то можно остановить перебор.
Дескрипторы безопасности временных имен (как и прочие данные временных имен) хранятся в ядре ОС. Поэтому единственный способ их запросить — расширение! wnf при отладке режима ядра. Но мы можем:
- Сделать вывод о разрешенности чтения при попытке читать данные.
- Сделать вывод о разрешенности записи попыткой писать данные. Но стоит учитывать, что успешная запись даже 0 байт уничтожает данные, которые еще не успел получить их реальный потребитель. И снова есть хитрость: мы можем применить соответствующую метку изменения (change stamp). Пытаемся писать с меткой 0xFFFFFFFF: проверка соответствия метки выполняется после проверки доступа, поэтому значение ошибки приводит к утечке разрешенности записи.
Это не дает нам полного дескриптора безопасности, но запустив код с разными привилегиями мы можем получить какое-то представление об ограничениях для разных учетных записей системы (Low IL/User/Admin/SYSTEM).
Перечисление подписчиков
В структуре WNF_PROCESS_CONTEXT одно из полей это голова списка (LIST_ENTRY) всех подписок этого процесса. Каждая подписка это отдельный экземпляр WNF_SUBSCRIPTION.
Подписчики режима ядра, в основном, принадлежат процессу System. Мы можем использовать команду отладчика ! list для дампа обработчиков и их параметров, зарегистрированных в WNF_SUBSCRIPTION процесса System. Стоит обратить внимание, что в некоторых случаях используется агрегатор событий (CEA.SYS), который скрывает реальные адреса обратных вызовов в своей структуре контекста.
Мы можем повторить этот подход и для процессов пользовательского режима, но адрес обработчика (Callback) будет NULL, так как это подписчики режима пользователя. Поэтому нам необходимо присоединиться к пользовательскому пространству процесса, получить таблицу RtlpWnfProcessSubscriptions, а затем дампить список экземпляров WNF_USER_SUBSCRIPTION, каждый из которых уже содержит адрес обработчика (Callback). К сожалению, этот символ является статическим, что означает, что его нет в открытых символах, но его можно найти дизассемблированием. И снова стоит обратить внимание (по аналогии с CEA.SYS режима ядра), что многие среди обработчиков пользовательского режима используют агрегатор событий (EventAggregation.dll), который хранит обратный вызов в своем контексте.
Интересные и чувствительные имена состояний WNF
В этом разделе будет несколько интересных примеров того, как некоторые имена состояний WNF раскрывают информацию о системе.
Определение состояния системы и поведения пользователя с помощью WNF
Некоторые идентификаторы WNF могут быть использованы для получения интересующей информации о состоянии машины:
- WNF_WIFI_CONNECTION_STATUS — cостояние беспроводного соединения
- WNF_BLTH_BLUETOOTH_STATUS — аналогично, но для Bluetooth (также WNF_TETH_TETHERING_STATE)
- WNF_UBPM_POWER_SOURCE — показывает источник питания (батарея или адаптер питания)
- WNF_SEB_BATTERY_LEVEL — содержит уровень заряда батареи
- WNF_CELL_* — на Windows Phone содержит информацию о: сети, номере, уровне сигнала, EDGE или 3G, …
Другие идентификаторы WNF могут быть использованы для получения информации о действиях пользователя:
- WNF_AUDC_CAPTURE/RENDER — Указывает процесс (включая PID), который осуществляет захват/воспроизведения аудио
- WNF_TKBN_TOUCH_EVENT — Событие каждого щелчка мыши, нажатие клавиатуры или нажатие сенсорного экрана
- WNF_SEB_USER_PRESENT/WNF_SEB_USER_PRESENCE_CHANGED — Присутствие пользователя в Windows
Альтернативы стандартным API уведомлений
Даже в ситуациях, когда определенные пользовательские действия уже имеют документированные API для уведомлений, эти API могут, например, генерировать новые записи в журнале событий/аудита. Иногда существуют соответствующие идентификаторы WNF для тех же отслеживаемых действий. И, если повезет, данные в WNF могут даже быть даже более детальными.
Например: WNF_SHEL_(DESKTOP)_APPLICATION_(STARTED/TERMINATED) предоставляет информацию как о запусках modern-приложений (включая фактическое имя пакета, которое было запущено) через DCOM, так и о запусках обычных приложений Win32. Но есть и серьезное ограничение — приложение должно быть запущено через вызов ShellExecute: двойной клик в Explorer, запуск через cmd.exe, …
Есть и примеры, когда WNF может служить альтернативой API пользовательского режима, не имеющего эквивалента со стороны ядра:
- WNF_SHEL_LOCKSCREEN_ACTIVE — событие активации экрана блокировки
- WNF_EDGE_LAST_NAVIGATED_HOST — указывает каждый URL, который пользователь вводит (или нажимает) в Edge
Воздействие на систему с использованием WNF
Существует набор идентификаторов WNF, запись данных в которые приводит к изменению системы. Например: WNF_FSRL_OPLOCK_BREAK — получает, среди прочих данных (количество/размер), список PID’ов и уничтожает указанные процессы!
Внимательно изучить поведение всех подобных идентификаторов WNF пока еще не хватило времени, но многие из них выглядят очень интересно. Например: WNF_SHEL_DDC_(WNS/SMS)_COMMAND — размер буфера 4 килобайта, что указывает на широкие возможности существования ошибок его разбора.
Аналогичным образом, существуют также некоторые идентификаторы WNF, которые выступают триггерами к некоторым действиям. Например: WNF_CERT_FLUSH_CACHE_TRIGGER (сброс хранилища сертификатов), WNF_BOOT_MEMORY_PARTITIONS_RESTORE, WNF_RTDS_RPC_INTERFACE_TRIGGER_CHANGED, …
Внедрение в процесс с использованием WNF
Основные техники внедрения кода в другой процесс включают в себя:
- WriteProcessMemory — непосредственная запись кода
- Проецирование файлов (объекты секций) — проецирование объектов секций в целевой процесс или запись в уже спроецированный процессом объект секции
- Объекты атомов (Atom) — сохранение данных в атоме и затем запрос целевым процессом данных атома
- Сообщения оконной подсистемы — использование сообщений, таких как WM_COPYDATA и DDE, для доставки данных в целевой процесс
- GUI объекты — изменение заголовка окна (или имени его класса) на данные, которые необходимы нам в целевом процессе
Использование WNF предоставляет еще пару способов передачи данных в целевой процесс:
- Повторное использование одного из существующих идентификаторов WNF, данные которого читает целевой процесс (особенно, когда данные сохраняются в предсказуемой области памяти процесса)
- Принудительно заставить процесс выполнить вызов Rtl/ZwQueryWnfStateData для целевого идентификатора WNF
Для того, что бы заставить процесс выполнить внедренный код обычно используют следующие механизмы ОС:
- APCs
- создание новых нитей в целевом процессе (Remote Threads)
- изменение контекста уже существующих нитей в целевом процессе (Changing Thread Context)
- изменение »window long» — атрибута окна, что бы изменить адрес обработчика, ассоциированного с окном целевого процесса
Однако другой подход может заключаться в анализе WNF_USER_SUBSCRIPTION целевого процесса (которые являются элементами списка из WNF_NAME_SUBSCRIPTION, на который ссылается RtlpWnfProcessSubscriptions). Функцию обратного вызова можно изменить (учитывая CFG), а основную полезную нагрузку передать через данные уведомления (параметры 5 и 6 у функции обратного вызова).
Еще один подход может заключаться в том, что бы менять контекст обратного вызова: в контексте может быть, например, таблица виртуальных функций, где можно подменить адрес метода-обработчика.
Направления для дальнейших исследований
Большая часть событий WNF начинается с SEB_, они относятся к брокеру системных событий (System Events Broker). SystemEventsBrokerServer.dll и SystemEventsBrokerClient.dll являются высокоуровневыми API пользовательского режима. Вероятно, что некоторые из этих событий SEB затем анализируются внутри клиентов SEB, что маскирует некоторых истинных потребителей.
Многие из зарегистрированных обратных вызовов режима ядра и пользовательского режима принадлежат CEA.SYS или EventAggregation.dll. Они являются частью «библиотеки агрегации событий» (Event Aggregation Library), которая позволяет управлять обратными вызовами, на основании накопления определенного набора условий: могут быть заданы пороговые значения, или может быть задано условие того, что несколько WNF событий должны произойти в одно и то же время или в заданной последовательности, или условие возникновения хотя бы одного события из группы. По сути это конечный автомат вокруг событий WNF, позволяющий регистрировать собственные обратные вызовы. А реальные потребители скрыты за библиотекой агрегации событий.
От переводчика: Далее я хотел бы немного рассказать про исследования других авторов и дать соответствующие ссылки.
До презентации
Нельзя не отметить, что исследования Windows Notification Facility начались задолго до выступления Alex’а и Gabrielle. Одним из первых (известных мне) публичных исследователей является redp.
Он добавил дампинг структур WNF (задолго до выступления) в свой широко известный в узких кругах инструмент wincheck. Более того, есть прямые ссылки из работ Gabrielle Viala на то, что ее исследования опираются на посты redp, с которыми можно ознакомиться тут: http://redplait.blogspot.com/search/label/wnf.
После презентации
Не так давно был представлен PoC (исходный код на github) внедрения в explorer (полезная нагрузка — запуск notepad). modexp представил реализацию одного из возможных векторов атаки из перезентации: подмена поля Callback в WNF_USER_SUBSCRIPTION. Опубликованный код сводится к следующему алгоритму:
- Открыть существующий процесс explorer.exe
- Найти в нем WNF_USER_SUBSCRIPTION
- Выделить непрерывный блок RWX-памяти и записать туда полезную нагрузку, вызовом WriteProcessMemory (жаль, что тут обычная связка VirtualAllocEx + WriteProcessMemory)
- Заменить адрес обработчика в WNF_USER_SUBSCRIPTION (это еще один вызов WriteProcessMemory)
- Вызвать ntdll! NtUpdateWnfStateData (…) в своем процессе, что приводит к вызову внедренной полезной нагрузки
- Восстановить оригинальный обработчик в WNF_USER_SUBSCRIPTION и освободить выделенные ресурсы