• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Играем с Windows Notification Facility (WNF)

Azrv3l

win32kfull
Эксперт
Регистрация
30.03.2019
Сообщения
215
Реакции
539
В этом сообщении кратко представлена функция Windows Notification и написан write-up на хорошее упражнение, которое Брюс Данг преложил во время своего семинара на Recon Montreal 2018.
*Автор женского пола, но перевод я писал от мужского имени. Так было проще переводить, и думаю так будет удобнее воспринимать материал

Преамбула
Этот пост был написан через несколько дней после окончания тренировок Брюса Данга по разведке в Монреале, но я решил отложить его публикацию по некоторым причинам. Я почти забыл об этом, пока Алекс не напомнил мне. Поскольку Брюс уже написал действительно хороший пост о WNF (если вы его пропустили, я рекомендую вам прочитать его сейчас), я чувствовал, что мой не принесет ничего нового, и что для меня не было особого смысла его дорабатывать, но кое-кто не оставил мне выбора ...: ')

1.PNG


Введение
Некоторое время назад Брюс Данг пригласил пять дам из BlackHoodie на тренинг Windows Kernel Rootkit в Recon Montreal. Барби, Прия, Ориан, Энил и я имели возможность побыть там в течение этих четырех дней интенсивной работы.

В этом посте я не буду описывать программу тренинга (поверьте мне, это было здорово), но я сосредоточусь на одном из упражнений, которые мне действительно понравились: реверсированг и (неправильное) использование WNF!

Входные данные

Я вообще не знал этого компонента, так как очень мало информации о нем доступно в Интернете. Единственными данными в моем распоряжение была следующая подсказка:
Код:
14. Reverse engineer the following Windows kernel functions.
    The result of this exercise will be used in the next exercise.

    • ExSubscribeWnfStateChange
    • ExQueryWnfStateData

15. Write a driver that gets notified when an application is using the microphone.
    Print its pid.

Hints:
    • check contentDeliveryManager_Utilities.dll for the WNF StateName.
    • some interesting info is available here:

http://redplait.blogspot.com/2017/08/wnf-ids-from-perfntcdll.html

Предыстория
Средство уведомлений Windows или WNF - это (не очень известный) компонент ядра, используемый для отправки уведомлений в системе. Его можно использовать либо в режиме ядра, либо в пользовательском режиме с набором экспортированных (но явно не документированных) функций API и связанных структур данных. Приложение может подписаться на событие определенного типа (идентифицируемое StateName), чтобы получать уведомления каждый раз, когда происходит изменение его состояния (которое может быть связано с StateData). С другой стороны, компонент отправителя отвечает за предоставление данных, которые будут отправлены с уведомлением, и за запуск события.

Следует отметить, что имена состояний WNF могут быть экземплярами (Scope) для отдельного процесса, изолированного пространства (контейнер Windows) или для всей машины. Например, если приложение запущено внутри изолированного хранилища, оно будет получать уведомления только о событиях в изолированном пространстве, происходящих внутри его собственного контейнера.

В этом сообщении в блоге я не буду говорить о механизмах пользовательского уровня, задействованных при использовании высокоуровневого API: они выходят за рамки упражнения, и объяснения сделают пост слишком тяжелым. Мы с Алексом Ионеску подробно рассказали о WNF в обоих вариантах на BlackHat USA 2018, который должен опубликовать видео и слайды где-то в ноябре 2018 года (выпуск которого ожидает устранения некоторых уязвимостей, которые должны быть устранены MSRC).

Структуры данных
В WNF задействовано множество структур, и вот упрощенное представление их взаимосвязи в памяти:

2.jpg


Событие или экземпляр имени состояния WNF представлен в памяти структурой WNF_NAME_INSTANCE. Эти структуры сортируются в бинарные деревья и связаны с областью действия, в которой происходит событие. Области определяют, какую информацию компонент может просматривать или получать доступ. Они также позволяют создавать экземпляры разных данных для одного и того же имени состояния.

Существует пять возможных типов области действия, которые определены следующим образом:
C:
typedef enum _WNF_DATA_SCOPE
{
  WnfDataScopeSystem = 0x0,
  WnfDataScopeSession = 0x1,
  WnfDataScopeUser = 0x2,
  WnfDataScopeProcess = 0x3,
  WnfDataScopeMachine = 0x4,
} WNF_DATA_SCOPE;

Области видимости, идентифицированные структурами WNF_SCOPE_INSTANCE, хранятся в двусвязном списке. а их заголовки хранятся в WNF_SCOPE_MAP, который является специфичным для хранилища.

Когда компонент подписывается на имя состояния WNF, создается новая структура WNF_SUBSCRIPTION, которая добавляется в связанный список, принадлежащий соответствующему WNF_NAME_INSTANCE. Если подписчик использует API низкого уровня (например, тот, который описан ниже), обратный вызов добавляется в WNF_SUBSCRIPTION и вызывается, когда компонент должен быть уведомлен.

Объект WNF_PROCESS_CONTEXT отслеживает все различные структуры, задействованные для конкретного процесса подписчика. Он также хранит KEVENT, используемый для уведомления процесса. Этот контекст доступен либо через объект EPROCESS, либо при сканировании двусвязного списка, указанного параметром nt!ExpWnfProcessesListHead. Ниже вы можете увидеть представление этих связей.

3.jpg


Если вам интересно, что означает 0x906, это связано с тем фактом, что все структуры, используемые WNF, имеют крошечный заголовок (обычное явление в структурах данных, связанных с файловой системой Windows), который описывает тип структуры и размер:
C:
typedef struct _WNF_CONTEXT_HEADER
{
    CSHORT NodeTypeCode;
    CSHORT NodeByteSize;
} WNF_CONTEXT_HEADER, *PWNF_CONTEXT_HEADER;

Этот заголовок очень удобен при отладке, так как довольно легко обнаружить объекты в памяти. Вот некоторые коды типов узлов для структур WNF:
C:
#define WNF_SCOPE_MAP_CODE          ((CSHORT)0x901)
#define WNF_SCOPE_INSTANCE_CODE     ((CSHORT)0x902)
#define WNF_NAME_INSTANCE_CODE      ((CSHORT)0x903)
#define WNF_STATE_DATA_CODE         ((CSHORT)0x904)
#define WNF_SUBSCRIPTION_CODE       ((CSHORT)0x905)
#define WNF_PROCESS_CONTEXT_CODE    ((CSHORT)0x906)

Время реверсить
Теперь, когда у нас есть некоторое понимание основ, приступим к упражнениям! Первая часть заключалась в том, чтобы разобрать следующие функции, чтобы понять их назначение:
  • ExSubscribeWnfStateChange
  • ExQueryWnfStateData

ExSubscribeWnfStateChange​

C:
NTSTATUS
ExSubscribeWnfStateChange (
    _Out_ptr_ PWNF_SUBSCRIPTION* Subscription,
    _In_ PWNF_STATE_NAME StateName,
    _In_ ULONG DeliveryOption,
    _In_ WNF_CHANGE_STAMP CurrentChangeStamp,
    _In_ PWNF_CALLBACK Callback,
    _In_opt_ PVOID CallbackContext
    );

ExSubscribeWnfStateChange позволяет регистрировать новую подписку в движке WNF. В качестве параметров он принимает, помимо прочего, StateName, определяющий тип интересующего нас события, и обратный вызов, который будет вызываться всякий раз, когда срабатывает уведомление. Он также возвращает новый указатель подписки, который можно использовать для запроса данных, связанных с уведомлением.

Внутренне эта функция передает поток выполнения только своему частному аналогу (ExpWnfSubscribeWnfStateChange), который выполняет всю обработку.

Поскольку имена состояний WNF хранятся в непрозрачном формате, ExpWnfSubscribeWnfStateChange сначала декодирует «чистую» версию идентификатора с помощью ExpCaptureWnfStateName.

Это чистое имя состояния WNF можно декодировать следующим образом:
C:
#define WNF_XOR_KEY 0x41C64E6DA3BC0074

ClearStateName = StateName ^ WNF_XOR_KEY;
Version = ClearStateName & 0xf;
LifeTime = (ClearStateName >> 4) & 0x3;
DataScope = (ClearStateName >> 6) & 0xf;
IsPermanent = (ClearStateName >> 0xa) & 0x1;
Unique = ClearStateName >> 0xb;

Более формально это дает следующую структуру:
C:
typedef struct _WNF_STATE_NAME_INTERNAL
{
    ULONG64 Version : 4;
    ULONG64 Lifetime : 2;
    ULONG64 DataScope : 4;
    ULONG64 IsPermanent : 1;
    ULONG64 Unique : 53;
} WNF_STATE_NAME_INTERNAL, *PWNF_STATE_NAME_INTERNAL;

Затем ExpWnfSubscribeWnfStateChange вызывает ExpWnfResolveScopeInstance. Последний извлекает глобальные хранилища серверов (или nt!PspHostSiloGlobals в случае, когда не задействован серверный разделитель) и просматривает несколько структур, чтобы найти WNF_SCOPE_INSTANCE, которому принадлежит экземпляр имени. Если этот экземпляр области не существует, он создается и добавляется в соответствующий список WNF_SCOPE_MAP. Это показано ниже:

4.png


Из этой структуры экземпляра области ExpWnfSubscribeWnfStateChange выполняет поиск (с ExpWnfLookupNameInstance) для WNF_NAME_INSTANCE, соответствующего заданному имени состояния WNF.

5.png


Если совпадений не найдено, создается новый WNF_NAME_INSTANCE с помощью ExpWnfCreateNameInstance. Этот новый экземпляр добавляется к двоичному дереву, основанному на WNF_SCOPE_INSTANCE.

Следующим шагом функции является вызов ExpWnfSubscribeNameInstance для создания нового объекта подписки. Как объяснялось ранее, этот объект будет содержать всю информацию, необходимую движку для запуска уведомлений.

Наконец, ExpWnfSubscribeWnfStateChange вызывает ExpWnfNotifySubscription, чтобы вставить новую подписку в ожидающую очередь и инициировать уведомление.

ExQueryWnfStateData
C:
NTSTATUS
ExQueryWnfStateData (
    _In_ PWNF_SUBSCRIPTION Subscription,
    _Out_ PWNF_CHANGE_STAMP ChangeStamp,
    _Out_ PVOID OutputBuffer,
    _Out_ OPULONG OutputBufferSize
);

Эта функция довольно проста, поскольку выполняет всего два действия. Сначала он извлекает WNF_NAME_INSTANCE из подписки с ExpWnfAcquireSubscriptionNameInstance. Затем он считывает данные, хранящиеся в нем с помощью ExpWnfReadStateData, и пытается скопировать их в буфер. Если этот буфер слишком мал, он запишет только необходимый размер в OuputBufferSize и вернет с STATUS_BUFFER_TOO_SMALL.

Для записи все имена состояний WNF хранят свои данные в памяти в структуре WNF_STATE_DATA. Эта структура содержит различные метаданные, такие как размер данных и количество раз, когда они были обновлены (так называемый ChangeStamp). Указатель на WNF_STATE_DATA хранится непосредственно в WNF_NAME_INSTANCE, как показано ниже.

6.jpg


Алекс также хотел бы, чтобы я указал, что имена состояний WNF могут быть помечены как постоянные, что означает, что данные (и отметка об изменении) будут сохраняться при перезагрузках (очевидно, с использованием вторичного хранилища данных). Подробнее об этом будет в нашей презентации!

Давайте кодить!
По сути, с функциями, которые мы изменили, мы должны иметь возможность зарегистрировать новую подписку и получать уведомления, как любое другое законное приложение, использующее WNF :)

Однако нам все еще не хватает одного элемента: нахождения необходимого имени состояния WNF для микрофонна.
Я подробно остановлюсь только на тех частях драйвера, которые имеют отношение к взаимодействию с WNF. Если вас интересует разработка драйверов для Windows, вы можете взглянуть на документацию Windows Driver Kit и их образцы, а еще лучше - посетить один из учебных курсов Брюса;)

Ищем правильное имя состояния WNF
В качестве подсказки для получения имени состояния WNF Брюс предоставил ссылку на сообщение и имя библиотеки (contentDeliveryManager_Utilities.dll).

В своей заметке в блоге Redplait определил несколько имен состояний, используемых WNF. К сожалению, того, что мы ищем, нет в списке. Однако это все еще дает нам хорошее начало, поскольку теперь мы знаем, как выглядят имена состояний WNF.

Один наивный подход к поиску того, что мы ищем, - это поискать одно из имен состояний WNF в блоге в contentDeliveryManager_Utilities.dll и надеяться, что будут другие идентификаторы ... К счастью, это работает очень хорошо! Следуя перекрестной ссылке сопоставленного шаблона в IDA, мы можем получить полный список имен состояний WNF, на которые есть ссылки в DLL. Каждая запись в этом списке имеет свое имя и описание, что очень удобно для наших целей! (Для получения дополнительной информации этот список используется GetWellKnownWnfStateByName).

7.PNG


Теперь нам просто нужно найти тот, который специфичен для микрофона (помните наше задание?: P)
Код:
.rdata:00000001800E3680  dq offset WNF_AUDC_CAPTURE
.rdata:00000001800E3688  dq offset aWnf_audc_captu ; "WNF_AUDC_CAPTURE"
.rdata:00000001800E3690  dq offset aReportsTheNu_0 ; "Reports the number of, and process ids "...
// “Reports the number of, and process ids of all applications currently capturing audio.
//  Returns a WNF_CAPTURE_STREAM_EVENT_HEADER data structure”
Следует отметить, что эта же таблица также доступна с библиотекой perf_nt_c.dll, которая входит в состав анализатора производительности Windows.

Подписываемся на событие
Чтобы подписаться на новое событие, нам просто нужно вызвать ExSubscribeWnfStateChange в нашем драйвере с именем состояния WNF, которое мы узнали выше. Эта функция экспортируется, но не определена ни в одном заголовке, поэтому мы должны вручную объявить ее сами, вставив определение сверху. Обратите внимание, что ntoskrnl.lib содержит заглушку библиотеки импорта, поэтому нет необходимости извлекать ее адрес вручную (спасибо Alex за подсказку;)).

Единственное, что здесь нужно сделать, это вызвать функцию с правильными параметрами:
C:
NTSTATUS
CallExSubscribeWnfStateChange (
    VOID
    )
{
    PWNF_SUBSCRIPTION wnfSubscription= NULL;
    WNF_STATE_NATE stateName;
    NTSTATUS status;

    stateName.Data = 0x2821B2CA3BC4075; // WNF_AUDC_CAPTURE

    status = ExSubscribeWnfStateChange(&wnfSubscription, &stateName, 0x1, NULL, &notifCallback, NULL);
    if (NT_SUCCESS(status)) DbgPrint("Subscription address: %p\n", Subscription_addr);

    return status;
}

Определяем обратный вызов
Как мы видели ранее, ExSubscribeWnfStateChange принимает среди своих параметров обратный вызов, который будет вызываться каждый раз при запуске события. Этот обратный вызов будет использоваться для получения и обработки данных о событиях, связанных с уведомлением.

Прототип обратного вызова выглядит так:
C:
NTSTATUS
notifCallback (
    _In_ PWNF_SUBSCRIPTION Subscription,
    _In_ PWNF_STATE_NAME StateName,
    _In_ ULONG SubscribedEventSet,
    _In_ WNF_CHANGE_STAMP ChangeStamp,
    _In_opt_ PWNF_TYPE_ID TypeId,
    _In_opt_ PVOID CallbackContext
    );

Чтобы получить данные в нашем обратном вызове, мы должны вызвать ExQueryWnfStateDataName. И снова эта функция экспортируется, но не определена ни в одном заголовке, поэтому мы должны определить ее сами:
C:
NTSTATUS
ExQueryWnfStateData (
    _In_ PWNF_SUBSCRIPTION Subscription,
    _Out_ PWNF_CHANGE_STAMP CurrentChangeStamp,
    _Out_writes_bytes_to_opt_(*OutputBufferSize, *OutputBufferSize) PVOID OutputBuffer,
    _Inout_ PULONG OutputBufferSize
    );
[...]

Нам нужно вызвать этот API дважды: один раз, чтобы получить размер, необходимый для выделения буфера для данных, и второй раз, чтобы фактически получить данные.
C:
NTSTATUS
notifCallback(...)
{
    NTSTATUS status = STATUS_SUCCESS;
    ULONG bufferSize = 0x0;
    PVOID pStateData;
    WNF_CHANGE_STAMP changeStamp = 0;

    status = ExQueryWnfStateDataFunc(Subscription, &changeStamp, NULL, &bufferSize);
    if (status != STATUS_BUFFER_TOO_SMALL) goto Exit;

    pStateData = ExAllocatePoolWithTag(PagedPool, bufferSize, 'LULZ');
    if (pStateData == NULL) {
        status = STATUS_UNSUCCESSFUL;
        goto Exit;
    }

    status = ExQueryWnfStateDataFunc(Subscription, &changeStamp, pStateData, &bufferSize);
    if (NT_SUCCESS(status)) DbgPrint("## Data processed: %S\n", pStateData);

    [...] // do stuff with the data

Exit:
    if (pStateData != nullptr) ExFreePoolWithTag(pStateData, 'LULZ');
    return status;
}

Убираем беспорядок при выгрузке драйвера
Если вы вслепую попробуете приведенный выше код, вы получите уродливый синий экран смерти, как я с болью узнал, когда впервые попробовал это упражнение и выгрузил свой драйвер! : P Нам нужно заранее удалить нашу подписку.

Для этого мы можем вызвать ExUnsubscribeWnfStateChange в подпрограмме выгрузки драйвера (и убедиться, что PWNF_SUBSCRIPTION wnfSubscription преобразован в глобальную переменную).
C:
PVOID
ExUnsubscribeWnfStateChange (
    _In_ PWNF_SUBSCRIPTION Subscription
    );

VOID
DriverUnload (
    _In_ PDRIVER_OBJECT DriverObject
    )
{
   [...]
   ExUnsubscribeWnfStateChange(g_WnfSubscription);
}

Эпичный провал
Все, что нам нужно сделать, это запустить драйвер, включить Кортану, немного поиграть с ней и дождаться срабатывания события.

...

Аааа ...! Ничего!

Результат моего упражнения полностью провалился, так как я забыл, что у меня нет звуковой карты на моей виртуальной машине (вероятно, причина, по которой я не мог запустить ни одно из приложений, связанных со звуком ..?>.> ') И, что наиболее важно, из-за к конфигурации моего хоста, я вообще не мог заставить его работать (не спрашивайте).

Тем не менее, чтобы убедиться, что мой драйвер работает правильно, мне пришлось выбрать другое событие и выбрать WNF_SHEL_DESKTOP_APPLICATION_STARTED. Это уведомление отображается каждый раз при запуске настольного приложения. В свою очередь, он просто выводит имя запущенного приложения. С этим именем состояния WNF было довольно просто получить некоторые результаты:

8.PNG


Последние события с WNF
Ранее я показал простой способ поиска имен WNF, просто выполнив поиск имени в IDA в одной из библиотек DLL, содержащих таблицу. Более надежный способ и расширяемый подход - получить имена состояний WNF путем синтаксического анализа библиотеки DLL для поиска таблицы и ее сброса. Хотя это был не тот подход, который я использовал в упражнении, Алексу нужно было легко следить за изменениями в именах состояний WNF (добавления / удаления / модификации) из-за его нездоровой одержимости различать каждую сборку kernel, и я придумал скрипт, который поможет ему в этом.

Так как я забыл упомянуть об этом на BlackHat, я чувствую, что делаю рекламу. :-^ Этот скрипт можно использовать для сравнения двух библиотек DLL и быстрого получения различий в таблицах, а также для выгрузки данных таблицы из одной библиотеки DLL. Результат - тот, который мы с Алексом использовали для наших приложений wnftool, и его можно легко адаптировать для использования в других программах на C и Python.
Код:
$ python .\StateNamediffer.py -h
usage: StateNamediffer.py [-h] [-dump | -diff] [-v] [-o OUTPUT] [-py]
file1 [file2]

Stupid little script to dump or diff wnf name table from dll

positional arguments:
    file1
    file2 Only used when diffing

optional arguments:
    -h, --help show this help message and exit
    -dump Dump the table into a file
    -diff Diff two tables and dump the difference into a file
    -v, --verbose Print the description of the keys
    -o OUTPUT, --output OUTPUT Output file (Default: output.txt)
    -py, --python Change the output language to python (by default it's c)

Пример вывода (я опубликую скрипт после снятия ноябрьского эмбарго):
C:
typedef struct _WNF_NAME
{
    PCHAR Name;
    ULONG64 Value;
} WNF_NAME, *PWNF_NAME;

WNF_NAME g_WellKnownWnfNames[] =
{
    {"WNF_A2A_APPURIHANDLER_INSTALLED", 0x41877c2ca3bc0875}, // An app implementing windows.AppUriHandler contract has been installed
    {"WNF_AAD_DEVICE_REGISTRATION_STATUS_CHANGE", 0x41820f2ca3bc0875}, // This event is signalled when device changes status of registration in Azure Active Directory.
    {"WNF_AA_CURATED_TILE_COLLECTION_STATUS", 0x41c60f2ca3bc1075}, // Curate tile collection for all allowed apps for current AssignedAccess account has been created
    {"WNF_AA_LOCKDOWN_CHANGED", 0x41c60f2ca3bc0875}, // Mobile lockdown configuration has been changed
    [...]
}

Заключение

Благодаря этому упражнению у меня была возможность покопаться в компоненте ядра, о котором я вообще не знал и с которым довольно весело играть. Я многому научился, и мне очень понравилось пытаться понять, как использовать WNF. Я определенно провел чудесную неделю в Recon, и я очень счастлив, что побывал там.

Итак, я еще раз хотел бы поблагодарить Брюса за семинар и за это упражнение. Я так рад, что смог присутствовать. Это круто! :)

Я также хотел бы поблагодарить Алекса за то, что он позволил мне присоединиться к BlackHat и за наставничество в этом процессе. Я действительно ценю то, что он пытается заставить меня найти ответы на мои вопросы самостоятельно, давая мне подсказки и заставляя сомневаться в моих собственных предположениях. Самостоятельное выяснение того, чего вы не понимаете, действительно полезно! Он определенно отличный педагог, и меня до сих пор поражает количество знаний, которые я получаю от него.
И хотя я не хотел заканчивать этот пост, я уверен, что многому научился, написав его, поэтому я также очень рад, что он подтолкнул меня продолжить. :)

Я очень рад познакомиться с ним и надеюсь, что когда-нибудь мы снова будем работать вместе!

И наконец, я очень благодарен своим коллегам за то, что они классные (;D)

От ТС
Эта статья является переводом, а оригинал доступен тут
В переводе могут быть ошибки, по возможности буду исправлять.

Перевод:
Azrv3l cпециально для xss.pro
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх