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

Мануал/Книга ERS - Exploiting Reversing Series

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
1. Введение

Добро пожаловать в первую статью серии Exploiting Reversing (ER), в которой я рассмотрю концепции, методы и практические шаги, связанные с двоичными файлами, и, в конце концов, проанализирую уязвимости в целом. Если читатели не читали прошлые статьи о других моих сериях (MAS — Malware Analysis Series), то все они доступны по следующим ссылкам:

1686748486648.png


В различных случаях нам приходится анализировать драйверы ядра или драйверы мини-фильтров, чтобы понять уязвимость или даже вредоносный драйвер (известный как руткит), и эта тема обычно сложна и содержит много деталей, которые в конечном итоге заслуживают объяснения. Тем не менее, мне все еще нужна была лучшая мотивация, чтобы начать эту новую серию, и она возникла, когда я анализировал детали минифильтра Microsoft Security Events Component Minifilter (C:\Windows\system32\drivers\mssecflt.sys), который является обязательной зависимостью, позволяет запускать службу FltMgr (fltmgr.sys), и наткнулся на функции этого драйвера, которые косвенно напомнили мне о методах, используемых для обнаружения различных видов уклонений с использованием NtCreateProcessEx(), которые я прочитал в хорошей статье, опубликованной Microsoft в прошлом году:
https://www.microsoft.com/security/...ation-properties-to-catch-evasion-techniques/.

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

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

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

Читатели из моих предыдущих статей могли задаться вопросом, есть ли у меня планы продолжать MAS (Серия анализа вредоносных программ), и, безусловно, я продолжу ее писать. Единственная разница в том, что я буду чередовать серии по вдохновению и свободному времени, конечно.

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

2. Благодарности
Я не мог написать эту серию и MAS (Malware Analysis Series) без решающей помощи от Ильфака Гильфанова (@ilfak), от Hex-Rays SA (@HexRaysSA), потому что у меня не было собственной лицензии IDA Pro, и он любезно предоставил мне все необходимое для написания этой серии статей о реверсировании и уязвимостях, а также о других, которые появятся. Однако его помощь не прекратилась в 2021 году, и он и Hex-Rays постоянно помогал до настоящего момента, оказывая немедленную поддержку во всем, что мне нужно для сохранения этих публичных проектов. Кроме того, Ильфак всегда очень любезен, отвечая мне каждый раз, когда я отправляю ему сообщение. Этот раздел, посвященный благодарностям, можно перевести одним словом: благодарность. Лично все сообщения от Ilfak и Hex-Rays, выражающие их доверие и похвалу моим предыдущим статьям, являются одной из самых больших мотиваций продолжать писать, как и читатели, которые присылают мне хотя бы одно сообщение с благодарностью. Еще раз: спасибо тебе за все, Ильфак.

Я выбрал цитату для начала каждой статьи, чтобы тонко показать свои размышления о жизни и информационной безопасности в целом, иногда отражая сегодняшние дни и все проблемы, которые заставили меня глубоко задуматься. В конце концов, мы должны инвестировать в работу, которую мы действительно любим делать, независимо от нашего возраста, потому что жизнь коротка, и завтрашний день — это наше будущее. Наслаждайся путешествием!

3. Ссылки

Всегда сложно предоставить ссылки и рекомендации по любой теме, но я хочу оставить несколько ссылок, которые я использовал в последние годы и которые могут помочь читателям узнать о теме, независимо от того, работает ли он над исследованием уязвимостей или анализом вредоносных программ :

*Microsoft Learn: https://learn.microsoft.com/en-us/windows-hardware/drivers/
* Образцы драйверов для Windows: https://github.com/Microsoft/Windows-driver-samples
*Книга Windows Internals 7th edition (Части 1 и 2) Павла Йосифовича, Алекса Ионеску, Марка Руссиновича и Дэвида Соломона, а также Андреа Аллиеви, Алекса Ионеску, Марка Руссиновича и Дэвида Соломона соответственно.
*Практический обратный инжиниринг Брюса Данга, Александра Газета и Элиаса Бачалани.

В основном (более 95% времени) я использовал официальную документацию Microsoft и соответствующие образцы драйверов Windows, упомянутые в первых двух пунктах выше, но и книги о внутренних компонентах Windows, и книга о практическом обратном инжиниринге предлагают отличное освещение темы.

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

В нашем контексте и проблемах (далеких от формальной классификации WDM) у нас есть разные типы драйверов:

*драйвер устройства: взаимодействует с аппаратными устройствами, такими как принтеры, USB-накопители и другие.
*программный драйвер ядра: этот тип драйвера запускается и устанавливает связь с ядром через ресурсы, предлагаемые системой. Кроме того, целью этого типа драйвера не является прямой обмен данными с физическим устройством.
* Драйвер мини-фильтра: это программный драйвер, который может отслеживать, перехватывать и изменять данные, передаваемые между приложениями и/или драйверами и системой (например, ядром или файловой системой). В то же время этот тип драйвера не взаимодействует напрямую с драйвером устройства.

Конечно, мы не заинтересованы в изучении драйверов устройств в этой статье (хотя это увлекательная тема), но обращение к драйверам устройств все еще является широким термином, который может вызвать некоторую путаницу. На самом деле, более точным названием было бы функциональные драйверы, и не забывая, что у нас есть еще драйверы шины, которые отвечают за установление связи между устройством, например шиной PCI-X или USB. В любом случае, в этом разделе мы рассмотрим основные концепции драйверов ядра, а в следующем обновим концепции, связанные с драйверами минифильтров.

Если читатель примет участие в разработке драйверов ядра, то он быстро поймет, что процесс разработки сопряжен с рядом проблем, потому что, поскольку драйвер работает на стороне ядра, поэтому любое необработанное исключение, вероятно, приведет к сбою системы и, согласно моему опыту, обнаружению ошибок строк кода не всегда являются чем-то тривиальным. Одна из многих вещей, которая будет объяснена далее в этой статье, заключается в том, что драйверы ядра могут работать на уровне DISPATCH_LEVEL (IRQL 2), что представляет собой иное последствие, чем пользовательские приложения, которые всегда работают на уровне PASSIVE_LEVEL (IRQL 0). На самом деле, существует довольно обширный список изменений при программировании и написании драйверов ядра, чем при написании приложения пользовательского режима, начиная с того факта, что большинство стандартных библиотек, которые очень помогают нам при написании приложений пользовательского пространства, недоступны в режиме ядра. У нас также есть те же опасения по поводу безопасности, и, например, если драйвер выгружается из памяти без выполнения необходимой очистки, возникает утечка памяти, которая освобождается только при следующей перезагрузке, что также является стандартной проблемой написание программ пользовательского режима. К сожалению, существует обширный список других препятствий для программирования. Конечно, все эти проблемы не возникают при реверсировании кода и понимании внутреннего устройства, но они по-прежнему важны для различения кода режима пользователя и режима ядра. Несмотря на эти трудности, драйверы ядра продолжают оставаться интересным материалом при исследовании уязвимостей, а также используются преступниками в качестве вектора заражения.

Еще один важный момент заключается в том, что при написании и даже анализе драйвера мы должны знать, что могут использоваться разные модели драйверов, которые могут мешать нашему пониманию основных характеристик:

* Драйверы ядра: модель драйвера Windows NT и KMDF (Kernel-Mode Driver Framework).
* Диски с мини-фильтром файловой системы: модель с мини-драйвером.
* драйверы устройств: KMDF (Kernel-Mode Driver Framework) и UMDF (User-Mode Framework Model) и WDM (Windows Driver Model).

Нам нужно выбрать отправную точку, поэтому объяснение концепций, связанных с кодом, которое поможет при реверсировании драйверов ядра, также может быть полезно для начала краткого обсуждения темы. Во всех драйверах ядра читатели найдут подпрограмму DriverEntry(), аналогичную основной функции в программах на языке C, работающих в пространстве пользователя. Эта подпрограмма служит точкой опоры для других функций, вызываемых драйвером. Собственно, одной из основных задач, выполняемых подпрограммой DriverEntry, является инициализация структур и ресурсов, которые будут использоваться драйвером в более поздний момент. Другими словами, он работает как промежуточная точка для вызова других подпрограмм и подготовки для них структуры данных.

В конце концов мы также найдем подпрограмму выгрузки, связанную с членом объекта драйвера с именем DriverUnload, которая вызывается автоматически при выгрузке драйвера и, как могут ожидать читатели, отвечает за выполнение задач очистки. Я буду обсуждать объект драйвера, объекты устройства и другие понятия в следующих абзацах, но сейчас вы должны знать, что объект драйвера является родителем любого другого объекта и различных объектов, таких как таймеры, спин-блокировки, объекты устройств и т. д. включены в этот список, и так же, как это происходит с приложением пользовательского режима, синхронизация также является важным компонентом на стороне ядра.

Драйверы можно установить как службу (sc create <имя драйвера> type= kernel binPath= <путь к драйверу>) и, как и другие службы, создать запись в разделе HKLM\System\CurrentControlSet\Services. Конечно, если Microsoft не подписала этот драйвер, необходимо настроить машину на загрузку в тестовом режиме, выполнив команду bcedit /set testsigning on с последующим завершением работы /r /t 0. Кроме того, если вы хотите загрузить драйвер без его установки, есть возможность использовать загрузчик OSR (доступен на https://www.osronline.com/article.cfm^article=157.htm). Честно говоря, я давно им не пользовался, но, вероятно, он все еще работает для устаревших драйверов и старых версий Windows.

Мы должны помнить, что существует три основных различных типа памяти, указанные перечислением POOL_TYPE (для устаревших API) из wdm.h (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ne-wdm-_pool_type) или перечисление POOL_FLAGS для новых API-интерфейсов (https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/pool_flags), которые используются драйверами:Paged Pool, Non-Paged Pool (страницы всегда хранятся в памяти) и NonPagedPoolNx (страницы всегда хранятся в памяти и не имеют разрешения на выполнение). Кроме того, имеет смысл упомянуть Session Paged Pool, в который можно выгружать страницы, но он не зависит от сеанса.
Поэтому при анализе драйверов ядра мы увидим вызовы нескольких функций выделения пула памяти, специфичных для ядра, таких как ExAllocatePool() (устарело в Windows 10 версии 2004), ExAllocatePoolWithTag() (устарело в Windows 10 версии 2004), ExAllocatePool2 ((https://learn.microsoft.com/en-us/w...i/wdm/nf-wdm-exallocatepool2),ExAllocatePool3 (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-exallocatepool3) и так далее. Общеизвестно, что области памяти, выделенные для большинства этих функций (устаревших и новых), могут иметь связанный тег со значением до четырех байт (обычно в ASCII) в обратном порядке, чтобы пометить (пометить) выделенная память.

Когда вредоносный драйвер заражает систему и выделяет память невыгружаемого пула ядра, у нас может быть возможность отследить эти области памяти, используемые угрозой, путем поиска определенного тега, если он используется, хотя в настоящее время это не так распространено. Даже не используя специальную утилиту, такую как Volatility, читатели могут отслеживать эти пулы с помощью таких команд, как poolmon (от WDK) и !lookaside (от WinDbg).

Существенным моментом в отношении драйверов ядра является понимание того, что один драйвер не делает все в одиночку. На самом деле, когда приложение отправляет запрос ввода-вывода, вероятно, будут драйверы, организованные в стек, каждый из которых отвечает за получение запроса, выполнение каких-либо действий или бездействие и передачу запроса следующему драйверу. Таким образом, из этого пункта вытекают важные понятия. После загрузки драйверов каждый из них представляется объектом драйвера, который имеет следующую структуру:

1686748606488.png


Объект-драйвер содержит жизненно важную информацию, вот некоторые из них:

*DeviceObject: указатель на объекты устройств, созданные драйвером (IoCreateDevice()).
*DriverExtension: указатель на расширение драйвера, которое используется драйвером для сохранения подпрограммы AddDevice в поле DriverExtension → AddDevice.
*DriverInit: точка входа, настроенная диспетчером ввода-вывода, в подпрограмму DriverEntry.
*DriverUnload: точка входа в процедуру выгрузки.
*MajorFunction: указатель на таблицу диспетчеризации, содержащую массив указателей входа на подпрограммы драйвера.

Драйверы составляют стек драйверов, и каждый из них связан с объектом драйвера. Каждый объект драйвера содержит один или несколько объектов устройств, представленных структурой _DEVICE_OBJECT:

1686748625165.png


Соответствующие поля в этой структуре:

*Тип: значение 3 в этом поле указывает на то, что данный объект является объектом драйвера.
*ReferenceCount: диспетчер ввода-вывода использует это поле для отслеживания количества открытых дескрипторов, связанных с объектом устройства.
*DriverObject: это поле содержит указатель на объект драйвера (DRIVER_OBJECT), представляющий загруженное изображение, как объяснялось ранее.
*NextDevice: это поле содержит указатель на следующий объект устройства.
*AttachedDevice: это поле содержит указатель на присоединенный объект устройства, который обычно связан с драйвером фильтра (не всегда).
*CurrentIrp: это поле содержит указатель на текущий IRP, если драйверы обрабатывают в данный момент и есть ли у него подпрограмма StartIo, точка входа которой была установлена в объекте драйвера. StartIo и IRP будут кратко прокомментированы позже.
*Таймер: это поле содержит указатель на объект таймера.
*Dpc: указатель на объект DPC (отложенный вызов процедуры) для объекта драйвера. DPC будет кратко объяснен позже.

Хотя есть и другие известные участники, упомянутых выше на данный момент достаточно. В любом случае, объект устройства (_DEVICE_OBJECT) является ключевым компонентом, поскольку он работает как интерфейс между клиентом и драйвером. Многие функции, используемые приложениями пользовательского режима, указывают на объект устройства через символические ссылки (IoCreateSymbolicLink() -- https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-
wdm- iocreatesymboliclink), который указывает на объект ядра.

Небольшой побочный эффект в этом контексте заключается в том, что символическая ссылка (например: \\.\ExampleDevice) обычно указывает на некоторый элемент в каталоге \Device (устройства как \Device\ExampleDevice создаются вызовом IoCreateDevice()): https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm- iocreatedevice), к которым нельзя получить доступ из пользовательского режима, поэтому необходимо вызвать IoGetDeviceObjectPointer(), чтобы получить к ним доступ ( https://learn.microsoft.com/en-us/windows- hardware/drivers/ddi/wdm/nf-wdm-iogetdeviceobjectpointer).

Что касается API, упомянутых в последних двух абзацах, у нас есть следующее:
1686748652630.png

Кратко о его параметрах:

*DriverObject: содержит указатель на объект драйвера, полученный в качестве параметра функции DriverEntry().
*DeviceExtensionSize: представляет количество байтов, зарезервированных для расширения устройства объекта драйвера. Расширение устройства может использоваться для хранения частной структуры данных, связанной с устройством, но обычно оно используется с драйверами устройств, а не с драйверами ядра.
*DeviceName: необязательно указывает на буфер, который содержит имя объекта устройства, как и ожидалось.
*DeviceType: определяет тип устройства, который задается константами FILE_DEVICE_*. Чтобы добавить их в IDA Pro как перечисление:
+Добавьте библиотеку типов с именем ntddk64_win10 (SHIFT+11 и горячие клавиши INS).
+ Перейдите на вкладку «Перечисления» (SHIFT+F10), вставьте новое перечисление, выберите «добавить стандартное перечисление по имени символа» и выберите FILE_DEVICE_DISK.
1686748713609.png


*DeviceCharacteristics: этот параметр указывает одну или несколько констант, но в контексте драйвера ядра в большинстве случаев он будет равен нулю (0) или FILE_DEVICE_SECURE_OPEN. Повторяя те же действия, что и для DeviceType, но на этот раз добавьте FILE_DEVICE_SECURE_OPEN.
1686748723150.png


*Exclusive: этот параметр определяет, представляет ли объект устройства монопольное устройство, которое контролирует и определяет, может ли несколько файловых объектов открывать устройство.

*DeviceObject: этот параметр содержит указатель на структуру DEVICE_OBJECT, размещенную в невыгружаемом пуле.
Исходя из объясненных понятий, имеем следующую схему:

*драйвер установлен → объект драйвера (_DRIVER_OBJECT) → один или несколько объектов устройства (_DEVICE_OBJECT).

До сих пор единственной упомянутой подпрограммой драйвера была DriverEntry, имеющая следующую сигнатуру:

1686748735799.png


Первый параметр — это указатель на DRIVER_OBJECT, а структура второго параметра — UNICODE_STRING, указывающий, что ключ Parameters-это указатель RegistryPath драйвера в реестре:

1686748743721.png


Помимо основных задач, выполняемых (на самом деле, вызываемых) в DriverEntry, существует еще одна, более важная роль, выполняемая той же подпрограммой, а именно инициализация подпрограмм Dispatch, которая представляет собой массив указателей на функции и является частью структуры _DRIVER_OBJECT ( Член MajorFunction).

Все индексы этого массива имеют префикс IRP_MJ_ и, как и ожидалось, представляют основные коды функций IRP. Драйверы должны устанавливать указатели входа в этот массив, которые устанавливают связанные и ответственные подпрограммы для обработки и манипулирования каждой из запланированных операций и, наконец, обслуживания запросов IRP.

У нас все еще есть незаконченный список концепций, которые необходимо объяснить и прояснить. IRP (пакет запроса ввода-вывода) — это структура, представляющая пакет запроса ввода-вывода, который используется драйверами для передачи информации и связи с другими драйверами. Другими словами, он работает к
ак формат данных, который должен использоваться в четко определенном стандарте для связи между уровнями драйверов.
IRP, определенный в файле wdm.h, представляет собой очень большую структуру и имеет много полей, но большинство из них являются объединениями. Если читатели хотят изучить структуру с помощью Интернета, следующая ссылка может быть интересна:
https://www.vergiliusproject.com/kernels/x64/Windows 11/22H2 (2022 Update)/_IRP

Лично я предпочитаю получать структуру _IRP из IDA Pro, выполнив следующие шаги:

1. откройте двоичный файл формата PE в IDA Pro
2. перейдите в Библиотеки типов (SHIFT+F11)
3. добавить ntddk64_win10 или любую другую подобную библиотеку (ntddk_win7).

Теперь перейдите на вкладку Structures (SHIFT+F9) и добавьте стандартную структуру с именем _IRP, как показано ниже:
1686748770137.png


Есть поля, которые предоставляют нам важный контекст и информацию о работе драйвера ядра, некоторые из них будут объяснены по мере необходимости и должны быть дополнены новыми понятиями, которые будут представлены позже. Даже если это не показано на предыдущем изображении, IRP имеет фиксированную часть, содержащую заголовок (идентификатор потока вызывающей стороны, адрес объекта устройства, блок состояния ввода-вывода и т. д.), который используется диспетчером ввода-вывода для управления IRP, и вторая часть, специфичная для каждого драйвера (местоположение стека ввода-вывода), которая содержит такие параметры, как код функции запрошенной операции и ее соответствующий контекст:

1686748783494.png


Мы собираемся сделать новые заметки на эту тему позже. Снова сосредоточившись на теме основных кодов IRP, существует ряд основных кодов IRP, которые используются драйверами для вызова соответствующей процедуры диспетчеризации в ответ на конкретный запрос ввода-вывода. Эти основные коды IRP работают как индексы в массиве указателей на функции.

Поскольку каждый драйвер ядра предлагает разные функции, они предоставляют разные процедуры диспетчеризации для обработки запросов ввода-вывода, передающих основные коды IRP, показанные ниже:

*IRP_MJ_CLEANUP: этот основной код IRP используется для вызова подпрограммы DispatchCleanup, когда драйверу необходимо освободить ресурсы в виде памяти и любого другого объекта, чей соответствующий счетчик ссылок достиг нуля, поэтому это подходящая и рекомендуемая подпрограмма для очистки, которая не связана с дескрипторы файлов.
*IRP_MJ_CLOSE: этот основной код IRP используется для вызова подпрограммы DispatchClose, когда последний дескриптор файлового объекта, связанного с объектом устройства, был закрыт, и любой запрос был закрыт или отменен.
*IRP_MJ_CREATE: этот основной код IRP используется для вызова подпрограммы DispatchCreate для открытия дескриптора устройства или файлового объекта. Хорошо известен пример, когда драйвер ядра вызывает такие функции, как NtCreateFile | ZwCreate, и отправляется IRP_MJ_CREATE для выполнения операции открытия.
*IRP_MJ_DEVICE_CONTROL: этот код IRP, с которым связана процедура DispatchDeviceControl, является следствием вызова DeviceIoControl(), который отвечает за отправку кода управления вводом-выводом (он может быть общеизвестным или частным) к цели драйвера устройства. В большинстве случаев подпрограмма передает IRP следующему более низкому драйверу, но бывают и исключения. Читатели должны помнить, что первые два члена DeviceIoControl() связаны с указанной целью:
1686748799347.png


Первые два параметра этой функции:

*hDevice: этот параметр представляет дескриптор драйвера устройства, который можно легко получить с помощью CreateFile() (https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi- создать файл).
*dwIoControlCode: этот параметр указывает управляющий код для операции. Существует несколько наборов управляющих кодов, организованных в соответствии с типом целевого устройства:
*cdrom: https://learn.microsoft.com/en-us/windows-hardware/drivers/storage/cd-rom-io-control-codes
*communication:: https://learn.microsoft.com/en-us/windows/win32/devio/communication-control-codes
*device management:: https://learn.microsoft.com/en-us/windows/win32/devio/device-management-control-codes
* directory management: https://learn.microsoft.com/en-us/windows/win32/fileio/directory-management-control-codes
* disk management: https://learn.microsoft.com/en-us/windows/win32/fileio/disk-management-control-codes
* file management: https://learn.microsoft.com/en-us/windows/win32/fileio/file-management-control-codes
*power management: https://learn.microsoft.com/en-us/windows/win32/power/power-management-control-codes
* volume management: https://learn.microsoft.com/en- us/windows/win32/fileio/volume-management-control-codes
*IRP_MJ_FILE_SYSTEM_CONTROL: как могут ожидать читатели, драйверы файловой системы обычно используют этот основной код IRP.
*IRP_MJ_FLUSH_BUFFERS: этот основной код IRP означает запрос к устройству на очистку его внутреннего кэша, и такой код используется для вызова подпрограммы DispatcFlushBuffers.
▪ IRP_MJ_INTERNAL_DEVICE_CONTROL: он очень похож на IRP_MJ_DEVICE_CONTROL, и читатели увидят этот код, например, когда другой драйвер вызывает IoBuildDeviceIoControlRequest() или даже IoAllocateIrp(). По сути, его можно интерпретировать как код, используемый для связи между драйверами, в то время как IRP_MJ_DEVICE_CONTROL используется для связи между приложением и водителем. Наконец, он используется для вызова подпрограммы DispatchInternalDeviceControl.
▪ IRP_MJ_PNP: этот код используется в запросе на любую операцию Plug & Play (например, перечисление или балансировку ресурсов) и используется для вызова процедуры DispatchPnP.
▪ IRP_MJ_POWER: этот код IRP используется запросами через Power Manager для вызова обратного вызова питания (подпрограмма DispatchPower).
▪ IRP_MJ_QUERY_INFORMATION: этот код IRP используется для вызова процедуры DispatchQueryInformation, которая обычно получает метаинформацию о файле или даже дескриптор. Например, это событие происходит, когда драйвер вызывает ZwQueryInformationFile() (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntqueryinformationfile). Конечно, драйвер не обязан обрабатывать такого рода запросы.
▪ IRP_MJ_SET_INFORMATION: этот код IRP отправляется операционной системой в качестве запроса (ZwSetInformationFile()) для установки метаданных о файле или даже дескрипторе и, как и в других случаях,вызывает процедуру DispatchSetInformation.
▪ IRP_MJ_SHUTDOWN: этот код IRP обрабатывается драйверами, отвечающими за массовое хранение данных с внутренними КЭШами, и используется для вызова процедуры DispatchShutdown. Поскольку драйверы организованы в стек, все промежуточные драйверы, связанные с запоминающими устройствами, должны иметь возможность управлять такими запросами. Конечно, драйверы должны завершить любую передачу данных, которые в данный момент находятся в кэше, прежде чем завершать запрос на завершение работы.
▪ IRP_MJ_SYSTEM_CONTROL: все драйверы должны предоставлять подпрограмму DispatchSystemControl, которая вызывается для обработки запросов IRP_MJ_SYSTEM_CONTROL, и эти запросы отправляются компонентами WMI, когда потребитель данных пользовательского режима запрашивает данные WMI.
▪ IRP_MJ_READ: этот код IRP используется для вызова процедуры DispatchRead, которая действует, когда приложение отправляет запросы (ReadFile() и ZwReadFile()) на передачу данных с устройства в приложение.
▪ IRP_MJ_WRITE: этот код IRP используется для вызова процедуры DispatchWrite, используемой драйверами, передающими данные из системы на связанное устройство.
Таким образом, пока у нас есть несколько выводов:
▪ объект драйвера (_DRIVER_OBJECT) содержит один или несколько объектов устройств (_DEVICE_OBJECT), которые являются основным интерфейсом связи между приложением и драйвером.
▪ API в пользовательском режиме относятся к объектам устройства как к своим параметрам.
▪ Чтобы драйвер ядра стал действительно полезным, он должен зарегистрировать подпрограммы отправки для обслуживания различных типов запросов (на уровне пользователя или на уровне ядра), которые выполняются путем отправки одного из кодов IRP.
▪ Во многих общедоступных драйверах читатели найдут драйверы, реализующие процедуры диспетчеризации для обработки вызовов пользовательских приложений, таких как, например, ReadFile(), DeviceIoControl() и WriteFile().
▪ Структура IRP (_IRP) содержит необходимую информацию из запроса и используется для переноса информации и связи с драйверами между уровнями в стеке драйверов.
▪ Содержимое IRP может содержать общую информацию для всех драйверов в стеке, но также содержит личную информацию для определенных драйверов в том же стеке.
▪ Объект устройства создается драйверами с помощью IoCreateDevice() (экспортируется диспетчером ввода/вывода).
▪ На рисунке 2 объект устройства (_DEVICE_OBJECT) связан со следующим через элемент NextDevice.

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

▪ Прием запросов от разных приложений.
▪ Для каждого запроса создается IRP для представления этого запроса.
▪ После этого он отправляет каждый запрос соответствующим драйверам.
▪ Он управляет и отслеживает эти IRP до тех пор, пока они не будут завершены.
▪ Наконец, он возвращает результат операции приложению, которое сделало запрос.

Тем не менее, несколько моментов все еще ожидают объяснения:

▪ Что такое IRQL и какие значения доступны?
▪ Что такое процедура StartIO?
▪ Что такое DPC и какова его цель?
▪ Как пакеты IRP передаются и сохраняются от верхнего драйвера ядра к нижнему?
 
Последнее редактирование:
IRQL (уровень запроса на прерывание) — это механизм Windows для управления прерываниями в соответствии с соответствующим уровнем важности в контексте операционной системы. Когда я упоминаю прерывания (IRQ), читатели, вероятно, помнят, что существуют аппаратные (асинхронные) и программные прерывания (синхронные), и Windows создает карту, назначающую приоритет (IRQL) данному источнику прерывания, испускаемому устройством, хотя эта карта отличается от процессора к процессору. Таким образом, каждый ЦП имеет связанное значение IRQL, и его можно интерпретировать как определенный регистр.

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

Следует отметить, что IRQL (уровень запроса на прерывание) не равен IRQ (запрос на прерывание), который связан с аппаратным обеспечением, а также не равен приоритету потока, поскольку приоритет потока является свойством отдельного потока.

Обычные уровни IRQL:

*ПАССИВНЫЙ УРОВЕНЬ (значение 0): на этом уровне никакие векторы прерываний не маскируются, и это уровень, на котором обычно выполняется большинство потоков. Это обычный IRQL. На самом деле, большинство подпрограмм драйвера ядра, таких как DriverEntry(), Unload(), AddDevice(), а также подпрограммы диспетчеризации выполняются на этом уровне.

*УРОВЕНЬ APC (значение 1): это уровень, используемый APC (асинхронными вызовами процедур), который является функцией, выполняемой в контексте потока. В двух словах, у каждого потока есть собственная очередь APC, и когда приложение отправляет APC в поток, вызывая QueueUserAPC() (на самом деле, оболочка для NtQueueApcThread() — https://learn.microsoft.com/en-us/w...sthreadsapi/nf-processthreadsapi-queueuserapc), он передает адрес функции APC в качестве аргумента, и система выдает прерывание. Таким образом, читатели могут понять, что постановка APC в очередь работает как запрос потока, вызывающего/вызывающего заданную функцию APC. Приложение может доставить APC потоку только тогда, когда этот поток находится в состоянии предупреждения (оно вызвало SleepEx(), WaitForSingleObjectEx(), WaitForMultipleObjectsEx() и т.д.), и этот APC из очереди потока выполняется, когда поток переходит из состояния предупреждения в рабочее состояние. Та же концепция используется, когда вредоносное ПО делает APC-инъекцию, что возможно только тогда, когда целевой поток находится в состоянии готовности. В конце концов, APC — это тонкий метод, который позволяет выполнять метод обратного вызова (функция, передаваемая в качестве аргумента в APC) асинхронным способом. APC можно перечислить, используя расширение !apc в WinDbg.

*DISPATCH LEVEL (значение 2): это более высокий IRQL, связанный с программным прерыванием. DPC (отложенный вызов процедуры) работает на этом уровне так же, как и диспетчер потоков, и отвечает за пост-обработку драйвера после того, как ISR (подпрограмма обслуживания прерываний) выполнила первое, критическое и короткое задание, зарегистрирован (IoConnectInterrupt() - https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-ioconnectinterrupt) драйвером устройства, выполняется в DIRQL (запрос прерывания устройства), и он отвечает за действительно минимальную работу перед постановкой в очередь (KeInsertQueueDpc() - https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-keinsertqueuedpc) DPC, который будет выполнен, когда IRQL упадет до более низкого уровня. Более того, в контексте драйвера ядра такие подпрограммы, как StartIo(), IoTimer(), Cancel(), DpcForIsr(), CustomDpc() и т. д., также выполняются на этом уровне. Наконец, уместно упомянуть, что любой поток, ожидающий объектов ядра (событий, семафоров, мьютексов…) на этом уровне, вызывает сбой системы.

*DIRQL (значение 3 и выше): эти уровни относятся к аппаратным прерываниям.

Код ядра, который может быть прерван другим кодом ядра с более высоким IRQL, может изменить текущий IRQL (от текущего ЦП) путем вызова таких функций, как KeLowerIrql()(https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-kelowerirql) иKeRaiseIrql() (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-keraiseirql). На стороне системы невозможно поднять IRQL из приложения пользовательского режима.

Хотя тема APC действительно привлекательна, единственная разница между PASSIVE_LEVEL и APC_LEVEL заключается в том, что процесс, работающий на уровне APC_LEVEL, не может быть прерван прерываниями APC. Объясняя высокоуровневые драйверы (не связанные с устройствами), которые обрабатывают IRP, мы сосредоточимся на PASSIVE_LEVEL и DISPATCH_LEVEL, чтобы не отвлекаться на другие темы.

В любом случае, я знаю, что профессионалы обычно спрашивают о IRQL и соответствующем контексте потока, когда вызывается одна из закомментированных процедур отправки (обратных вызовов), поэтому я получил список от Microsoft.( https://learn.microsoft.com/en-us/w.../ifs/dispatch-routine-irql-and-thread-Context), которые могут помочь вам:
1686759993155.png


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

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

Возвращаясь к нашему основному обсуждению, читатели могут проверить основную информацию о драйверах в соответствии с тем, что мы обсуждали до сих пор, с помощью WinDbg/WinDbg Preview (доступного в Microsoft Store):

1686760027613.png


Приведенный выше вывод основан на Windows 11. На случай, если читатели не знают, как установить WinDbg, это происходит из установки Windows SDK. На самом деле, если читатели заинтересованы в разработке драйверов ядра и минифильтра, рекомендуется установить несколько компонентов в следующем порядке:

*Visual Studio: https://visualstudio.microsoft.com/downloads/
* Windows SDK: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/.
*Windows WDK: https://learn.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk

Если читатели хотят использовать предварительную версию WinDbg, есть два способа ее установки:

*Из Microsoft: https://apps.microsoft.com/store/detail/windbg-preview/9PGJGD53TN86
* Из командной строки: winget install windbg

Лично я всегда настраиваю следующую переменную среды: _NT_SYMBOL_PATH= srv*c:\Symbols*http://msdl.microsoft.com/download/symbols

WinDbg может занять много времени, чтобы отобразить полный список имен устройств, но идея состоит в том, чтобы получить список устройств, зарегистрированных в каталоге \Device, и с этого момента собрать дополнительную информацию о конкретном драйвере. Поскольку у нас есть адрес объекта, указанный в выводе выше, наш следующий шаг — получить имя драйвера и связанные объекты устройств для этого драйвера. Помните: к объекту драйвера может быть присоединен один или несколько объектов устройств. Таким образом, выбрав в качестве примера устройство vmmemctl, выполните:

1686760064489.png


Из этих команд мы получили:

*список объектов устройств, связанных с драйвером.
* обобщенную информация о данном объекте устройства.
* список процедур диспетчеризации, связанных с объектом драйвера.

Если читатели задаются вопросом о том, как получить список всех ожидающих IRP, WinDbg также предлагает команду:

1686760080672.png


Мы узнали, что базовый драйвер ядра, вероятно, будет иметь соответствующие процедуры, механизмы и объекты, которые имеют решающее значение для его безупречной работы:

*Процедура DriverEntry(), которая вызывается из IRQL == PASSIVE_LEVEL и отвечает за предоставление точки входа в процедуры драйвера, инициализацию или даже создание объекта, выделение невыгружаемой или выгружаемой памяти с помощью ExAllocatePoolWithTag() (например) или извлечение ключевая информация из реестра. Кроме того, его также можно использовать для вызова подпрограммы PsCreateSystemThread, которая создает системный поток для выполнения в режиме ядра.
*Процедура Unload(), которая отвечает за освобождение ресурсов и является обязательным требованием для драйверов WDM (модель драйвера Windows). Диспетчер ввода-вывода вызывает процедуру выгрузки, если нет ссылки или ожидающего запроса IRP, связанного с объектами устройств драйвера. Читатели могут найти внутри этой подпрограммы ряд функций, таких как ExFreePool(), IoDeleteSymbolicLink(), PsTerminateSystemThread(), IoDeleteDevice() и так далее.
*Ассоциированный объект устройства (помните: объект устройства является фактическим интерфейсом связи с драйвером).
*Символическая ссылка (созданная IoCreateSymbolicLink()):https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-iocreatesymboliclink), связанная с объектом устройства.
* У нас будут драйверы ядра, которые содержат одну или несколько процедур диспетчеризации, обрабатывающих коды функций, такие как IRP_MJ_CLOSE, IRP_MJ_READ, IRP_MJ_CREATE или IRP_MJ_DEVICE_CONTROL, IRP_MJ_INTERNAL_DEVICE_CONTROL, IRP_MJ_SYSTEM_CONTROL, потому что эти процедуры обычно необходимы для большинства драйверов ядра, и в разных случаях у нас будет возможность работать с другими, например, IRP_MJ_SET_INFORMATION, IRP_MJ_CLEANUP и IRP_MJ_SHUTDOWN. Если читатели программируют, то системные функции/макросы, такие как ObDereferenceObject (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-obdereferenceobject), PsLookupThreadByThreadId (https:// Learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-pslookupthreadbythreadid) и IoCompleteRequest (поясняется ниже) будут очень полезны.
* Подпрограмма диспетчеризации может не иметь ничего общего с драйвером, поэтому она будет завершать ввод IRP с помощью простого STATUS_SUCCESS, но это может быть подходящим в контексте и сценариях.

Например, подпрограмма DispatchClose (обрабатывающая код функции ввода-вывода IRP_MJ_CLOSE) может отвечать за уведомление об удалении всех ссылок на данный файл. В конце концов, драйверы, которые никогда не могли быть недоступны, и подпрограмма DispatchClose не будут вызываться. Точно так же подпрограмма DispatchCleanup (обрабатывает код функции ввода-вывода IRP_MJ_CLEANUP) используется для выполнения операций очистки после освобождения дескрипторов данного объекта, и для каждого запроса IRP эта подпрограмма состоит из таких операций, как установка указателя процедуры Cancel в NULL, отменяя все запросы, связанные с IRP (например, связанные с закрытым объектом), которые все еще находятся в очереди, и, наконец, вызывая подпрограмму IoCompleteRequest() для завершения IRP и возвращая STATUS_SUCCESS. Возможно, самый важный урок заключается в том, что, хотя в большинстве программных драйверов можно увидеть несколько подпрограмм диспетчеризации, рекомендуется не предполагать, является ли одна из них более важной или даже критической, чем другая, потому что у каждого драйвера есть определенная цель и своя роль.

Конечно, список подпрограмм, упомянутых выше, относится только к основному программному драйверу ядра, что является частью цели этой статьи, но мы могли бы рассказать о них гораздо больше. Конечно, читателям, заинтересованным в написании драйвера устройства, могут быть интересны и другие подпрограммы, такие как AddDevice, StartIo, ISR, подпрограммы DPC и так далее.

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

Концепция, которую я еще не упомянул, — это подпрограмма завершения, необязательная функция/функция, которая вызывается функцией IoCompleteRequest() и играет важную роль в обработке ядра, поскольку драйвер может зарегистрировать подпрограмму завершения (подпрограмма IoCompletion()). -- https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-iocompleterequest), который будет вызываться диспетчером ввода-вывода, как только драйвер ядра завершит обработку IRP.

IoCompleteRoutine() выполняет обратный путь, отправляя обратно IRP драйверу верхнего уровня в стеке драйверов. Таким образом, в гипотетическом асинхронном сценарии драйвер ядра, вероятно, обрабатывает следующий IRP, в то время как диспетчер ввода-вывода вызывает процедуру завершения из другого драйвера, завершившего обработку IRP.

Драйверы предоставляют статус операции в блоке состояния ввода-вывода IRP. Кроме того, драйверы могут хранить статус операции внутри расширения драйвера, что очень полезно в контексте с двумя или более драйверами, которые являются частью одного и того же стека. Когда объект устройства создается с помощью функции IoCreateDevice (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-iocreatedevice), для подготовки драйвера используется параметр DriverExtensionSize для сценариев, описанных в этом параграфе. Расширение драйвера может быть создано или инициализировано функцией IoAllocateDriverObjectExtension(), которая вызывается подпрограммой DriverEntry().

При использовании концепции стека драйверов я не предполагаю конкретное количество драйверов в этом стеке, чтобы объяснение было достаточно широким. Тем не менее, уместно объяснить, что если какой-либо драйвер, который составляет часть стека, не получает дескриптор или даже не передает IRP следующему драйверу правильным путем, система может (и, вероятно, будет) аварийно завершать работу. Кроме того, и в качестве примечания, до сих пор мы в основном объясняли и обрабатывали операции ввода-вывода как запросы IRP. Тем не менее, существует другой тип операции, называемый быстрым вводом-выводом, который не генерирует IRP и обращается к определенным драйверам для выполнения запроса, но сейчас не время обсуждать этот тип операций в этом разделе.

Возвращаясь к нерешенным вопросам, пришло время дать краткое объяснение процедур ISR и StartIo. В общем, аппаратные прерывания связаны с приоритетом (IRQL, как мы узнали), устройство регистрирует (через подпрограммы IoConnectInterruptEx/WdmlibIoConnectInterruptEx) одну или несколько ISR (Interrupt Service Routine) для обработки прерываний. Драйверы, связанные с физическими устройствами, которые генерируют прерывания, должны иметь как минимум один ISR. Опять же, у потоков есть связанный приоритет, в то время как у ЦП есть связанный атрибут с именем IRQL.

Другими словами, каждый раз, когда для этого конкретного устройства генерируется прерывание, система вызывает ISR, которым могут быть подпрограммы InterruptService или InterruptMessageService. В любом случае, он будет выполняться с тем же ассоциированным IRQL, с которым поступил запрос (маскируя прерывания на более низком уровне), и, если IRQL равен нулю (например) до ISR, то он будет повышен до того же более высокого уровня прерывания (нет переключения контекста, когда IRQL равен 2 или выше, а доступ к выгружаемой памяти вызывает сбой системы), и после завершения ISR IRQL вернется к предыдущему уровню. Кроме того, можно включить или отключить ISR, вызвав функции IoReportInterruptActive() или IoReportInterruptInactive(), ссылки на которые приведены ниже:


ISR короткий и быстрый. В двух словах, он должен обрабатывать прерывание (останавливать прерывание), собирать и сохранять состояние (контекст) и ставить в очередь DPC (подпрограммы DpcForIsr или CustomDpc) с помощью подпрограмм IoRequestDpc или KeInsertQueueDpc соответственно, вскоре IRQL упадет ниже DISPATCH_LEVEL.

DPC будет отвечать за управление операциями ввода-вывода, которые будут выполняться на более низком уровне, чем ISR. ISR выполняет лишь небольшую часть обработки ввода-вывода (начальный запрос), а основная работа возлагается на DPC (отложенный вызов процедуры), которому поручено завершить операцию ввода-вывода, поставить в очередь следующий IRP. (обеспечивая следующую операцию ввода-вывода) и, как объяснено, завершать текущий IRP, когда это возможно.

Система предоставляет объект DPC для каждого объекта устройства, и первой (по умолчанию) процедурой является DpcForIsr(). Если драйверу необходимо создать дополнительные объекты DPC, то процедуры CustomDpc связываются с этими новыми объектами DPC. Подпрограммы DpcForIsr и CustomDpc вызываются в произвольном контексте DPC на уровне IRQL_DISPATCH_LEVEL (значение IRQL 2).

Процедура IoInitializeDpcRequest() отвечает за регистрацию процедуры DpcForIsr, получение указателя на объект устройства, представленный структурой DEVICE_OBJECT (помните: объект DPC для каждого объекта устройства), а также получение указателя на предоставленную процедуру DpcForIsr, как показано ниже:

1686760163042.png


Чтобы зарегистрировать подпрограмму CustomDpc, связанную с объектом устройства, драйвер должен вызвать подпрограмму KeInitializeDpc. Первый параметр — указатель на структуру KDPC, второй параметр — указатель на процедуру CustomDpc, а последний параметр содержит контекст. Настало время подчеркнуть, что подпрограмма CustomDpc не связана с объектом DeviceObject, как показано ниже:

1686760172174.png


Подпрограмма IoRequestDpc вызывается ISR для постановки в очередь подпрограммы DpcForIsr для выполнения:

1686760180316.png


Параметр Irp является указателем на текущий IRP, а параметр Context передается подпрограмме.
Другой подпрограммой для постановки DPC в очередь на выполнение является KeInsertQueueDpc, аргументом которой является указатель на подпрограмму KDPC и два аргумента, предназначенных для контекста, как показано ниже:

1686760189199.png


Согласно https://www.vergiliusproject.com/, представление структуры _KDPC следующее:

1686760211715.png


Хотя это введение не посвящено драйверам ядра, существует другой тип DPC, называемый Threaded DPC, который выполняется на уровне PASSIVE_LEVEL и может быть вытеснен обычным DPC, но не другими потоками. Анализируя эту функцию со строгой точки зрения, она представляет собой хорошую альтернативу, потому что, поскольку обычный DPC не может быть вытеснен другим обычным DPC, система с несколькими поставленными в очередь DPC может создавать большую задержку и, в конечном итоге, вызывать проблемы с производительностью. Таким образом, многопоточный DPC, включенный по умолчанию (HKLM\System\CCS\Control\SessionManager\Kernel\ThreadDpcEnable), в большинстве случаев может быть интерпретирован как лучший выбор, чем обычный DPC (но это не правило).

Помимо использования DPC с ISR, DPC также можно использовать с таймерами ядра, поведение которых удивительно похоже на другие объекты, такие как семафоры, события, мьютексы, события и т. д., поскольку любой драйвер может использовать эти объекты во время задач синхронизации, поскольку это происходит в IRQL. ==PASSIVE_LEVEL и непроизвольный контекст. Независимо от того, какой из упомянутых объектов ядра берется, мы можем использовать типичные процедуры ожидания, такие как:

*KeWaitForSingleObject (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-kewaitforsingleobject)
*KeWaitForMultipleObjects (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-kewaitformultipleobjects).

Если вдаваться в некоторые подробности, то таймер ядра связан и представлен структурой KTIMER или EX_TIMER и используется для тайм-аута операций подпрограмм ядра или даже для планирования новых операций (другие исследователи и программисты могут использовать термин «действия» или « задачи»), которые должны выполняться время от времени, что представляет собой устоявшееся периодическое поведение.

Таймеры ядра, основанные на структуре KTIMER, можно установить с помощью KeSetTimer (объект таймера должен быть инициализирован с помощью процедуры KeInitializeTimer/KeInitializeTimerEx, а его DPC также должен быть инициализирован с помощью вызова процедуры KeInitializeDPC) для установки абсолютного или даже относительного интервала, который после него истекает, он устанавливается в сигнальное состояние.

1686760240826.png


1686760246314.png


Сигнальное состояние для таймеров указывает, поскольку флаг поднят, что таймер завершен, и любой объект DPC, который был вставлен в очередь DPC, может выполняться, как только это возможно (во время операции красной команды это был бы момент для выполнения вводимый код, выполненный посредством внедрения DPC).
Чтобы установить повторяющееся время (для атрибутирования периодического поведения), используйте процедуру KeSetTimerEx. Если таймер основан на структуре EX_TIMER (он должен быть выделен с помощью процедуры ExAllocateTimer и может быть освобожден с помощью процедуры ExDeleteTimer), то процедура ExSetTimer может использоваться для запуска операции таймера и времени истечения срока действия. Прототип функции ExAllocateTimer показан ниже:

1686760256607.png


Следовательно, подпрограмма CustomTimerDpc может быть связана с таймером, который будет выполняться как можно скорее, когда таймер подаст сигнал. Два типа таймеров — это таймер уведомления (как только он сигнализирует, это означает, что заданное время достигнуто, все потоки получают зеленый свет для продолжения, и состояние таймера остается в соответствии с сигналом до тех пор, пока он не будет явно сброшен) и таймер синхронизации (как только он подал сигнал, он остается в сигнальном состоянии до тех пор, пока ожидающий его поток не будет освобожден, и он автоматически сбрасывается в несигнальное состояние). Если драйверу необходимо отключить таймер, есть возможность вызвать процедуру KeCancelTimer (для таймеров на основе структуры KTIMER) или ExCancelTimer (для таймеров на основе структуры EX_TIMER).

Согласно тому, что мы рассмотрели до сих пор, процедура DPC запускается, когда IRQL падает ниже DISPATCH_LEVEL или даже когда истекает настроенный таймер. Без сомнения, это объяснение может быть расширено на другие объекты диспетчера ядра, такие как мьютекс, события, семафоры или даже другие методы, такие как рабочие элементы и спин-блокировки, но все эти концепции можно легко изучить из любого ресурса, такого как веб-сайт Microsoft Learn (MSDN) и книг, упомянутых в начале статьи.

Возвращаясь к нашей запланированной повестке дня (снова), у нас есть ожидающие объяснения вопросы, по крайней мере, поэтому пришло время кратко прокомментировать расположение стека ввода-вывода, а также предлагает дополнительное представление о динамической передаче IRP на другие уровни.

Как мы уже знаем и объясняли ранее, все запросы ввода-вывода к драйверам на более низком уровне стека драйверов основаны на IRP (пакете запроса ввода-вывода). Диспетчер ввода-вывода выделяет массив местоположений стека ввода-вывода (структура IO_STACK_LOCATION) для каждого сконфигурированного IRP (в функции IoAllocateIRP есть параметр с именем StackSize для указания количества местоположений стека ввода-вывода), и каждый элемент этого массива связан с драйвером в стеке драйверов. Другими словами, количество местоположений стека ввода-вывода из этого массива можно преобразовать в количество драйверов в стеке драйверов.

1686760275606.png


Читатели могут использовать функцию IoAllocateIrpEx, которая имеет три параметра, первый из которых позволяет нам передать указатель на объект устройства. В этом случае, если для параметра DeviceObject задано значение DEVICE_WITH_IRP_EXTENSION, вызов предназначен для выделения места для расширения IRP.

Поскольку каждый драйвер является владельцем расположения стека ввода-вывода в IRP, этот драйвер может вызвать процедуру IoGetCurrentIrpStackLocation, которая возвращает указатель на расположение стека ввода-вывода вызывающей стороны в IRP, чтобы получить специфичную для драйвера информацию о вводе-выводе. На самом деле информация об операции ввода-вывода делится между заголовком IRP и текущим местоположением стека ввода-вывода.

1686760289444.png


Каждый драйвер стека драйверов отвечает за настройку расположения стека ввода-вывода следующего нижнего драйвера (местоположение стека ввода-вывода, которое составляет часть структуры IRP) путем вызова процедуры IoGetNextIrpStackLocation, которая предоставляет доступ точно к расположению нижнего стека ввода-вывода для выполнения этой настройки, и, как поняли читатели, это критическая задача в стеке драйверов. Таким образом, диспетчер ввода-вывода устанавливает заголовок IRP и расположение первого стека ввода-вывода, а все последующие (для каждого драйвера) настраиваются непосредственно вышестоящим драйвером.-

1686760297333.png


Другая возможность, о которой следует упомянуть, заключается в том, что драйвер может быть удовлетворен обработкой IRP и больше не заинтересован во внесении дальнейших изменений. Следовательно, он вызовет макрос IoSkipCurrentIrpStackLocation, чтобы установить для следующего драйвера в стеке точно такую же структуру IO_STACK_LOCATION, которую получил текущий драйвер.

Эти расположения стека ввода-вывода полезны для хранения контекста об операции, такой как подпрограмма завершения ввода-вывода (зарегистрированная вызовом функций IoSetCompletionRoutine или IoSetCompletionRoutineEx), и она будет вызываться после обработки IRP драйвером более низкого уровня, что позволяет вызвать процедуру завершения в/в, например, для выполнения задач очистки.

1686760307460.png


Аргумент CompletionRoutine является указателем на подпрограмму IoCompletion, которая вызывается при IRQL, равном или ниже DISPATCH_LEVEL, и должна быть вызвана, когда непосредственно нижний драйвер завершает обработку IRP. Второй параметр — это указатель на IO_COMPLETION_ROUTINE:

1686760316722.png


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

Кроме того, подпрограмма IoCompletion драйвера может выполняться в два разных момента или условия: в произвольном потоке (поэтому невозможно заранее узнать поток) или даже внутри контекста DPC.
Таким образом, после того как драйвер ядра завершит IRP, он вызывает процедуру IoCompleteRequest, которая обычно вызывается из процедуры DpcForIsr, чтобы уведомить о том, что все сделано. После этого диспетчер ввода/вывода проверяет, предлагают ли вышестоящие драйверы подпрограмму IoCompletion (как мы описали), и вызывает один за другим, от непосредственно вышестоящего драйвера до самого высокого драйвера. После того, как все сделано (все драйверы в стеке завершили обработку IRP), диспетчер ввода-вывода возвращает результат вызывающему приложению.

Остается вопрос: как драйвер перенаправляет IRP следующему более низкому драйверу в стеке? Он выполняет эту задачу, вызывая IoCallDriver, который представляет собой макрос-оболочку IofCallDriver, которая принимает два параметра, таких как DeviceObject (указатель на объект целевого устройства) и Irp (указатель на IRP):

1686760336192.png


Теперь у нас есть очень краткое представление об общении между драйверами через стек, нам нужно вернуться к основной идее в общении между приложением и драйверами, которая представляет собой реальную информацию (данные), передаваемые во время общения, поэтому уместно вспомнить еще раз о структуре IRP:

1686760346582.png


Как я упоминал ранее, я бы прокомментировал некоторые поля из структуры IRP в соответствии с необходимостью, и поскольку мы заинтересованы в понимании обмена данными между приложениями и драйверами, поэтому некоторые из этих полей важны, потому что, как правило, приложения могут взаимодействовать с драйвер путем записи (WriteFile: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefile), чтения (ReadFile: https://learn.microsoft.com/en -us/windows/win32/api/fileapi/nf-fileapi-readfile) или даже контроль (DeviceIoControl: https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol ) устройство или другой драйвер. Однако операция не имеет значения, будет какая-то передача информации от приложения к драйверу устройства или наоборот, и буфер, содержащий информацию, должен быть указан во время операции, и на этот раз другие поля IRP показывают свою важность. :
 
*UserBuffer: это поле содержит указатель (адрес) на пользовательский буфер. На самом деле, этот буфер является адресом выходного буфера и используется в определенных условиях кода управления вводом-выводом (METHOD_BUFFERED или METHOD_NEITHER) и соответствующего кода основной функции (IRP_MJ_DEVICE_CONTROL / IRP_MJ_INTERNAL_DEVICE_CONTROL), как мы скоро узнаем.

*SystemBuffer: это поле содержит указатель на системный буфер (буфер невыгружаемого пула), который будет полезен для драйверов, использующих буферизованный ввод-вывод, и назначение данного буфера определяется соответствующим основным кодом IRP, таким как IRP_MJ_READ. (буфер будет использоваться для чтения с устройства или драйвера), IRP_MJ_WRITE (будет использоваться для записи в устройство или драйвер) и IRP_MJ_DEVICE_CONTROL (буфер будет использоваться для отправки и получения управляющих данных в/из устройства или драйвера).

*MdlAddress: это поле указывает на MDL (список дескрипторов памяти), который определяется структурой MDL, за которым следует массив, описывающий макет физической страницы для буфера виртуальной памяти. Существует ряд функций для работы с MDL, таких как MmGetMdlVirtualAddress (получает виртуальный адрес буфера ввода-вывода, описанный MDL), MmGetMdlByCount (получает размер буфера ввода-вывода), IoAllocateMdl (эта функция выделяет MDL). ), IoFreeMdl (эта функция освобождает MDL), MmInitializeMld (эта функция форматирует невыгружаемый блок памяти как MDL), MmBuildMdlForNonPagedPool (для инициализации упомянутого массива в соответствии со структурой MDL) и многие другие.

Важным аспектом, который следует понимать, является то, что, независимо от вовлеченности любого поля выше, доступ к любому предоставленному буферу всегда контролируется системными правилами (включая аспекты безопасности), и в конечном итоге нарушение правила приведет к сбою системы. Например, доступ к пользовательскому буферу может осуществляться только из контекста потока приложения (IRQL==0), запрашивающего этот доступ. Тем не менее, связанные функции, такие как DPC или Start IO, могут выполняться из любого потока (произвольный контекст), где предоставленный адрес не имеет смысла (разные адресные пространства) и IRLQ == 2, доступ к пользовательской странице которого запрещен, поскольку часть буфера может быть были выгружены. К сожалению, даже процедура отправки не может быть надежной из-за того, что, хотя она работает в том же контексте запрашивающего потока и изначально с IRQL == 0, в конечном итоге она может работать с IRQL == 2 (или выше) , через действие IRP между драйверами в стеке.

Таким образом, диспетчер ввода-вывода предоставляет нам два подхода для безопасного доступа к предоставленному пользовательскому буферу:

▪ Буферизованный ввод/вывод
▪ Прямой ввод/вывод

В большинстве случаев метод буферизованного ввода-вывода следует использовать для интерактивных служб, передающих небольшой объем данных (вероятно, 4 КБ или меньше) между приложением и драйверами. Поскольку большинство операций — это чтение или запись (запросы IRP_MJ_READ и IRP_MJ_WRITE соответственно), поэтому драйвер выбирает этот метод операции, когда элемент Flag объекта Device (структура DEVICE_OBJECT — см. девятое поле на рис. 2), предоставленный IoCreateDevice ( ), устанавливается как DO_BUFFERED_IO (на самом деле элемент флага работает как операция ИЛИ). Если драйверу необходимо обрабатывать или выполнять операции управления устройством ввода-вывода через функцию DeviceIoControl (запросы IRP_MJ_DEVICE_CONTROL/IRP_MJ_INTERNAL_DEVICE_CONTROL), значение кода IOCTL должно отражать этот метод, используя METHOD_BUFFERED в качестве его значения TransferType.

Буферизованные операции ввода-вывода происходят путем выделения буфера с размером пользовательского буфера внутри для выделенного невыгружаемого пула (ExAllocatePoolWithTag / ExAllocatePool2), и этот новый адрес сохраняется как указатель в IRP (в частности, в элементе SystemBuffer из поля AssociatedIrp ). После этого он разрешает доступ к этому новому выделенному буферу для драйвера, и больше нет проблем, поскольку буфер хранится в невыгружаемом пуле, поэтому драйвер не рискует попытаться получить доступ к выгружаемым данным. Кроме того, поскольку адрес находится в пространстве ядра, он действителен для любого процесса и, что еще лучше, драйверу даже не нужно его блокировать перед доступом к нему. После создания невыгружаемого буфера данные могут быть скопированы (диспетчером ввода/вывода) из пользовательского буфера в этот новый невыгружаемый буфер для запросов IRP_MJ_WRITE или скопированы из этого нового невыгружаемого буфера в пользовательский буфер для IRP_MJ_READ. Запросы.

Операции прямого ввода-вывода, которые рекомендуются для случаев, когда требуется передать больший объем данных, представляют собой подход, отличный от буферизованного ввода-вывода. Вместо того, чтобы предлагать новый буфер в невыгружаемом пуле, как это делается для буферизованного ввода-вывода, этот метод предлагает прямой доступ к буферам, что повышает производительность, поскольку нет накладных расходов при первом копировании данных в вновь созданный буфер для употребления в последствии. По-видимому, это будет проблемой, потому что, как мы объяснили ранее, значение адреса действительно только для данного адресного пространства процесса, но механизм другой. Когда буфер создается пользовательским приложением, диспетчер ввода-вывода создает MDL, описывающий этот буфер. На самом деле содержимое буфера может быть разбросано по разным физическим местам в памяти, и созданный MDL представляет этот набор мест как единое целое в мире виртуальной памяти. Другими словами, MDL работает как своего рода сопоставление одной виртуальной памяти с одним или несколькими диапазонами физических адресов.

Вскоре после того, как MDL был связан с пользовательским буфером, диспетчер ввода-вывода проверяет, доступен ли такой пользовательский буфер, и блокирует его (делает его резидентным) в памяти (невыгружаемой памяти), вызывая MmProbeAndLockPages (определено в wdm.h) , который принимает MDL в качестве первого аргумента, и убедитесь, что содержимое страниц виртуальной памяти не будет освобождено и перемещено в любое время:

1687106694463.png


Второй параметр (AccessMode) указывает режим, используемый для проверки аргументов (KernelMode или UserMode), а третий параметр указывает тип запланированной операции (цели), которая будет выполняться при доступе к буферу виртуальной памяти через MDL, например IoWriteAddress, IoReadAddress или даже IoModifyAddress.

Буфер пользовательской памяти будет разблокирован только в том случае, если диспетчер ввода-вывода вызовет функцию MmUnlockPages после завершения драйвером обработки IRP.

Создав MDL, I/O Manager заполняет поле IRP → MdlAddress указателем на указатель (адрес) MDL. Если устройство выполняет операции DMA, то это делается потому, что драйверы устройств, работающие с операциями DMA, требуют только физических адресов. Однако это не наш случай, потому что мы заинтересованы в доступе к содержимому буфера. Таким образом, мы должны сопоставить предоставленный буфер с ассоциированным MDL с нестраничным системным адресом, и этот адрес извлекается вызовом MmGetSystemAddressForMdlSafe() с адресом MDL в качестве первого аргумента. Эта функция возвращает указатель на невыгружаемый виртуальный адрес для буфера, представленного MDL. Таким образом, у нас есть именно то, что нам нужно: нестраничный системный адрес, к которому можно получить доступ из любого процесса/потока (произвольный контекст) и любой IRQL, потому что, поскольку он заблокирован в памяти и не может быть выгружен, поэтому системный сбой никогда не происходит даже при доступе к нему из IRQL == 2 или выше.

Существует третья опция, называемая Neither I/O,, которая не управляется диспетчером ввода-вывода, и в этом случае управление буфером выполняется (функции ProbeForRead и ProbleForWrite) и осуществляется из того же контекста запрашивающего потока, поскольку исходный адрес буфера передается в IRP, который будет использоваться самим драйвером. Любое нарушенное правило, вероятно, вызовет сбой системы. Без диспетчера ввода-вывода непросто справиться с необходимыми требованиями для выполнения всех этих задач, и, в конце концов, самому драйверу придется самостоятельно вручную выполнять те же задачи, которые будет выполнять диспетчер ввода-вывода. Менеджер ввода-вывода.

В реальном мире, как я объяснял ранее, существуют операции записи, чтения и управления устройством. Первые два были рассмотрены операции буферизованного ввода-вывода и прямого ввода-вывода, но при работе с управлением устройством ввода-вывода (IRP_MJ_DEVICE_CONTROL) есть информация, которая предоставляется в управляющем коде, которая обычно определяется драйвером через CTL_CODE() — макрос со следующим прототипом:

* void CTL_CODE (тип устройства, функция, метод, доступ);

Ниже приводится быстрая расшифровка параметров:

*Первый параметр указывает, что DeviceType, но поскольку нас интересуют драйверы ядра, он равен нулю. Если читатели ищут здесь возможные используемые типы устройств, то их можно найти по адресу https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/specifying-device-types.

*Второй параметр содержит значение функции IOCTL, которое будет использоваться и доступно для приложений пользовательского режима, поэтому его необходимо использовать с запросами IRP_MJ_DEVICE_CONTROL. Если он используется только компонентами режима ядра, он должен использоваться с запросами IRP_MJ_INTERNAL_DEVICE_CONTROL.

*Третий параметр содержит код метода о том, как передачи буферов(METHOD_BUFFERED, METHOD_IN_DIRECT, METHOD_OUT_DIRECT и METHOD_NEITHER).

* Четвертый и последний параметр указывает операцию: FILE_ANY_ACCESS (обычно используется, поскольку работает в обоих направлениях), FILE_WRITE_ACCESS (от пользовательского приложения к драйверу) и FILE_READ_ACCESS (от драйвера к пользовательскому приложению).

Мы закончили наш краткий обзор драйверов ядра, и пришло время рассмотреть драйверы фильтров.
 
5. Обзор драйверов фильтров

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

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

Без сомнения, есть много общих вещей, таких как IRP (пакеты запросов ввода-вывода) для связи, методы обратного вызова, IOCTL и т. д., которые мы также можем использовать здесь и, в конечном итоге, адаптировать концепции для объяснения функциональности драйвера минифильтра. Драйверы минифильтров способны фильтровать и перехватывать IRP, быстрые операции ввода-вывода (синхронные операции ввода-вывода, когда данные передаются между заданным пользовательским буфером и системным кешем без вмешательства файловой системы или драйвера хранилища) и операции обратного вызова файловой системы.

Драйверы фильтров используются для настройки/изменения операций, связанных с файловой системой, и, как правило, драйверы фильтров файловой системы используются для перехвата, мониторинга и даже изменения запросов к файловой системе, помимо возможного расширения и замены текущей функциональности.

Таким образом, как и ожидалось, вы найдете драйверы файловой системы и драйверы файловой системы мини-фильтра в контекстах, где перехват и мониторинг являются основной целью, поскольку несколько продуктов защиты безопасности, таких как антивирус, EDR, программы резервного копирования и т. д., и этот факт не является сюрприз, и это довольно круто.

В Windows есть две модели фильтров системы фильтрации: модель минифильтра, которая поддерживается диспетчером фильтров, и модель фильтрации устаревшей файловой системы. Модель минифильтра является гораздо лучшим выбором, потому что она позволяет выгрузить драйвер минифильтра (FilterUnload() в пользовательском режиме, FltUnloadFilter() в режиме ядра и даже с помощью команды fltmc, как мы скоро узнаем) и обеспечивает связь между например, приложение пользовательского режима и собственный драйвер минифильтра. Кроме того, он также позволяет заблокировать/закрепить определенный тип операции с помощью обратных вызовов (определения будут приведены на следующих страницах), и, как показано ниже, есть возможность управлять порядком загрузки с помощью концепции соответствующей высоте (еще один термин, который будет объяснен).

Службы фильтров файловой системы доступны через диспетчер фильтров (представленный тем же файлом fltmgr.sys, упомянутым выше), которые включаются при загрузке предоставленного мини-фильтра и упрощают задачу программирования (или, по крайней мере, менее сложную) и, как и ожидалось, минифильтр — это модель, используемая для создания драйверов минифильтра файловой системы. Как и драйверы ядра, минифильтры также складываются в стек, но порядок их загрузки (фактически позиционирования в стеке) определяется их соответствующей высотой. Понятие высоты кажется сложным, но это не так, и читатели могут заметить это, соблюдая следующую последовательность:

а. Приложение запрашивает операцию ввода-вывода
б. Диспетчер ввода-вывода получает и перенаправляет этот запрос диспетчеру фильтров (fltmgr.sys).
в. Управление фильтрами получает запрос от диспетчера ввода-вывода (который является ключевым компонентом) и проверяет все зарегистрированные драйверы мини-фильтров (mfd1, mfd2, mfd3, mfd4...) в соответствии с зарегистрированной высотой.
д. После того, как минифильтр выполнит свои действия, запрос перенаправляется в драйвер фильтра файловой системы.
е. Наконец, запрос достигает стека драйверов хранилища.

Существует список различных способов представления потока информации с участием драйверов мини-фильтров, и один из них — с помощью следующего изображения, разработанного Microsoft (из MSDN):
1687108766208.png


Следовательно, значение высоты определяет порядок, в котором драйверы минифильтров будут вызываться диспетчером фильтров. Кроме того, может быть загружено более одного диспетчера фильтров, и каждый из них устанавливает фрейм для драйверов минифильтров. Подобно любой обычной службе, драйверы мини-фильтра могут быть загружены (по крайней мере, поскольку у пользователя есть должный SeLoadDriverPrivilege) с использованием информации в реестре (например, Get-Item -Path HKLM:\SYSTEM\CurrentControlSet\Services\SysmonDrv\ ), который передается в функцию FilterLoad() (https://learn.microsoft.com/en-us/windows/win32/api/fltuser/nf-fltuser-filterload), вызывающую FltLoadFilter() (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/fltkernel/nf-fltkernel-fltloadfilter). Точно так же операция выгрузки должна выполняться вызовом FilterUnload().

Драйвер файловой системы мини-фильтра должен зарегистрироваться (через функцию FltRegisterFilter) в диспетчере фильтров и указать операции, которые он (драйвер мини-фильтра) хочет перехватить и обработать, хотя драйверам мини-фильтра не нужно самостоятельно настраивать процедуры отправки, поскольку они не подключены напрямую в потоке выполнения (см. изображение выше). Обратные вызовы (до операции и после операции, о которых мы поговорим позже) задаются через массив структур FLT_OPERATION_REGISTRATION, который также определяет основные функции, такие как IRP_MJ_CREATE, IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_FILE_SYSTEM_CONTROL, IRP_MJ_DIRECTORY_CONTROL и так далее. Эта ключевая структура будет надлежащим образом использоваться в качестве аргумента функции FltRegisterFilter().

При обсуждении подпрограмм, связанных с драйверами мини-фильтров, есть несколько из них, которые хорошо известны, например:

*DriverEntry(): происходит и работает как для драйверов устройств, используется для инициализации.
*FltRegisterFilter(): эта функция используется для регистрации драйвера минифильтра (и связанных процедур обратного вызова) в диспетчере фильтров.
*FlsStartFiltering(): он отвечает за уведомление диспетчера фильтров о том, что драйвер минифильтра доступен и готов к подключению к томам и запросам фильтрации (IRP, быстрый ввод-вывод и операции обратного вызова файловой системы). Другими словами, он начинает реальную операцию фильтрации.

Эти подпрограммы содержат интересные детали, которые помогают объяснить концепции, упомянутые в предыдущих абзацах. Прототип FltRegisterFilter(), который на данный момент является одним из основных, довольно прост:

1687108791575.png


Как видят читатели, параметров всего три:

*Driver: это указатель на объект драйвера, представляющий драйвер мини-фильтра, и, как и ожидалось, это тот же самый указатель на объект драйвера, который передается подпрограмме DriverEntry().
*Registration: это указатель на регистрационную структуру минифильтра (структура FLT_REGISTRATION).
*RetFilter: это указатель на переменную, которая получает указатель фильтра, который возвращается вызывающей стороне (по сути, это возврат функции).

Структура _FLT_REGISTRATION имеет следующие элементы:

1687108805100.png


Эта информативная структура приносит информацию, связанную с массивами других структур, таких как FLT_CONTEXT_REGISTRATION и FLT_OPERATION_REGISTRATION, из которых первый приписывается каждому типу контекста, а второй приписывается каждому типу ввода-вывода, для которого минифильтр регистрирует процедуры обратного вызова перед операцией и после операции.

В любом случае, нет никаких сомнений в том, что наиболее важным полем этой структуры является OperationRegistration, которое является частью только что упомянутой нами структуры FLT_OPERATION_REGISTRATION, но оно не единственное. Существуют и другие соответствующие поля, такие как FilterUnloadCallback (содержит адрес функции, которая вызывается, когда драйвер собирается выгрузиться), InstanceSetupCallback (это указатель на обратный вызов, который вызывается диспетчером фильтров при наличии нового тома) , InstanceSetupCallback (указывает на обратный вызов, который позволяет уведомлять драйверы минифильтров непосредственно перед тем, как они будут присоединены к тому), InstanceQueryTeardownStartCallback (содержит указатель на функцию, которая будет вызываться диспетчером фильтров перед процессом разрыва, что делает возможность для минифильтра отменить ожидающие операции и отменить или завершить запросы ввода-вывода) и так далее.

Что касается процесса отключения, экземпляр драйвера минифильтра отключается в следующих случаях: либо минифильтр выгружается, либо необходимо выполнить определенный запрос на отсоединение, либо том, к которому подключен экземпляр, отключен.

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

Другими ценными членами структуры FLT_REGISTRATION являются:

*ContextRegistration: представляет собой указатель на массив структур FLT_CONTEXT_REGISTRATION, по одной для каждого типа контекста (отформатированные данные, используемые драйвером, если это необходимо), которые может использовать минифильтр.
*OperationRegistration: он представляет собой указатель на массив структур FLT_OPERATION, по одной для каждого типа ввода-вывода, для которого минифильтр регистрирует подпрограммы обратного вызова перед операцией и после операции. Как упоминалось ранее, в этой структуре есть элементы, которые также определяют основные функции, такие как IRP_MJ_CREATE, IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_FILE_SYSTEM_CONTROL, IRP_MJ_DIRECTORY_CONTROL и так далее.

Если читатели спрашивают об определении обратных вызовов, они могут интерпретировать обратные вызовы как своего рода «современные перехваты». На самом деле методы обратного вызова позволяют нам регистрировать подпрограммы, которые будут запускаться и выполняться при возникновении определенных событий в системе. Существует ряд функций обратного вызова ядра, которые будут прокомментированы позже, и обратные вызовы, связанные с драйверами ядра и драйверами мини-фильтров, некоторые из которых будут упомянуты ниже.

Существует список указателей на различные обратные вызовы, которые можно зарегистрировать, и небольшое количество этих наиболее часто используемых процедур обратного вызова:

*FilterUnloadCallback: содержит указатель на процедуру обратного вызова, которая будет вызываться для уведомления драйвера минифильтра о том, что диспетчер фильтров собирается выгрузить драйвер минифильтра. Этот обратный вызов определен и рассматривается как необязательный, хотя без него драйвер не может быть выгружен, поэтому происходит утечка ресурсов.
*InstanceSetupCallback: это указатель на процедуру обратного вызова, которая будет вызываться для уведомления драйвера минифильтра о том, что новый том смонтирован и доступен. Другими словами, диспетчер фильтров вызывает эту подпрограмму, чтобы уведомить драйвер минифильтра о необходимости в конечном итоге ответить на запрос автоматического или ручного присоединения к заданному тому. Как читатели могут понять, у него есть интересные практические применения.
*InstanceQueryTeardownCallback: это указатель на процедуру обратного вызова, которая будет вызываться, чтобы позволить драйверу минифильтра ответить на запрос ручного отсоединения, исходящий от любого компонента режима ядра, вызывающего FltDetachVolume, или даже от приложения пользовательского режима, вызывающего функцию FilterDetach.
*InstanceTeardownStartCallback: он содержит указатель на процедуру обратного вызова, которая будет вызываться, когда диспетчер фильтров начнет удалять экземпляр драйвера минифильтра, чтобы позволить ему завершить любую ожидающую операцию, такую как закрытие открытых файлов и прекращение постановки в очередь новых рабочих элементов, а также сохранить информацию. С определенной точки зрения, эту процедуру обратного вызова можно интерпретировать как первый этап подготовки к процедуре очистки.
*InstanceTeardownCompleteCallback: представляет собой указатель на процедуру обратного вызова, которая будет вызываться после завершения процесса удаления, чтобы позволить драйверу минифильтра закрыть возможные открытые файлы и выполнить любой другой процесс очистки.
*GenerateFileNameCallback: он содержит указатель на процедуру обратного вызова, которая позволяет драйверу минифильтра перехватывать запросы имени файла от других драйверов минифильтра над ним в стеке минифильтра (весьма важно помнить о концепции стека драйверов). При вызове этой подпрограммы обратного вызова драйвер минифильтра может генерировать собственную информацию об имени файла на основе информации об имени файла, которая могла быть получена с помощью FltGetFileNameInformation().

Диспетчер фильтров выполняет свою работу и упрощает все, потому что он обрабатывает обычные задачи IRP, такие как копирование параметров в следующее местоположение стека, а также предоставляет возможность драйверам минифильтров регистрировать только те операции ввода-вывода, которые им действительно интересны (это имеет смысл для продуктов безопасности), например, и это основная причина того, что драйверы минифильтров | драйверы файловой системы интерпретируются как необязательные драйверы) или должны обрабатываться через массив структуры FLT_OPERATION_REGISTRATION:

1687108855090.png


Параметр MajorFunction указывает тип операций ввода-вывода, которые задаются объединением FLT_PARAMETERS, и некоторые из них показаны ниже:

1687108863874.png


1687108883203.png

Второй параметр — Flags, который указывает, когда вызывать подпрограммы обратного вызова до и после операции для кэшированных операций ввода-вывода или страничного ввода-вывода, но сейчас он не совсем актуален для нас.

PreOperation и PostOperation являются указателями на подпрограммы PFLT_PRE_OPERATION_CALLBACK и PFLT_POST_OPERATION_CALLBACK, которые, очевидно, зарегистрированы как предоперационные и послеоперационные подпрограммы обратного вызова соответственно.

Говоря грубыми словами, подпрограммы обратного вызова до операции выполняют задачи обработки, необходимые для завершения операции ввода-вывода, и контролируют, что должно быть сделано с запросами IRP и подпрограммами после операции. Подпрограммы обратного вызова после операции вызываются диспетчером фильтров поверх операции ввода-вывода, когда более низкие драйверы уже завершили обработку завершения.

Процедура PFLT_PRE_OPERATION_CALLBACK может возвращать различные значения, такие как:

*FLT_PREOP_COMPLETE: это значение означает, что драйвер минифильтра завершает операцию ввода-вывода, и драйвер фильтра не вызывает послеоперационные обратные вызовы любого минифильтра ниже вызывающего (помните о стеке драйвера) и не пересылает (пропускает вниз) любые запрос к драйверам минифильтра ниже вызывающего.
*FLT_PREOP_DISALLOW_FASTIO: это значение означает, что операция является быстрой операцией ввода-вывода и что драйвер минифильтра не позволяет использовать для этой операции быстрый путь ввода-вывода. Остальные характеристики, связанные с обратными вызовами после операции и запросами на пересылку, аналогичны FLT_PREOP_COMPLETE.
*FLT_PREOP_PENDING: это значение означает, что для предоставленного драйвера минифильтра операция все еще ожидает выполнения, и только после вызова FltCompletePendedPreOperation диспетчер фильтров продолжит операцию ввода-вывода.
*FLT_PREOP_SUCCESS_NO_CALLBACK: это значение означает, что драйвер минифильтра возвращает операцию ввода-вывода диспетчеру фильтров для дальнейшей обработки, но диспетчер фильтров не будет вызывать послеоперационный обратный вызов драйверов минифильтров после завершения ввода-вывода.
*FLT_PREOP_SUCCESS_WITH_CALLBACK: это значение означает, что драйвер минифильтра возвращает операцию ввода-вывода диспетчеру фильтров для дальнейшей обработки, которая вызывает обратный вызов после операции драйвера минифильтра по завершению ввода-вывода.
*FLT_PREOP_SYNCHRONIZE: это значение указывает на то, что драйвер минифильтра возвращает операцию ввода-вывода диспетчеру фильтров для дальнейшей обработки, но не завершает операцию. Кроме того, диспетчер фильтров вызовет обратный вызов мини-фильтра после операции в контексте текущего потока на уровне IRQL <= DISPATCH_LEVEL.
*FLT_PREOP_DISALLOW_FSFILTER_IO: это значение означает, что драйвер минифильтра запрещает быструю операцию QueryOpen и заставляет операцию выполняться по медленному пути.

Читатели поняли, что в последних абзацах появился новый термин: Fast I/O. В двух словах, Fast I/O — это дополнительный механизм, поддерживаемый драйверами минифильтров, для получения запросов. На самом деле драйвер файловой системы фильтрует запросы ввода-вывода, поступающие в виде IRP (пакета запроса ввода-вывода) или запросов быстрого ввода-вывода. Как и запросы IRP, запросы быстрого ввода-вывода также имеют методы обратного вызова.

Справедливо сказать, что запросы IRP в некотором роде эквивалентны запросам Fast I/O, но это не одно и то же, и IRP могут обрабатывать гораздо больше типов ввода-вывода, чем Fast I/O. Кроме того, подпрограмма DriverEntry может регистрировать подпрограммы отправки IRP, а также подпрограммы обратного вызова быстрого ввода-вывода, но только набор этих подпрограмм может быть зарегистрирован для данного драйвера фильтра.

Кстати, в чем разница между IRP и Fast I/O? Охват IRP шире, и его можно использовать для синхронных/асинхронных операций, и не имеет значения, кэшированный это или некэшированный ввод-вывод. В случае быстрого ввода-вывода он подходит для синхронных операций ввода-вывода с кэшированными файлами.
Таким образом, общие требования и практическое использование драйверов фильтров сосредоточены на запросах IRP, хотя даже в этих сценариях драйверы фильтров должны определять процедуру быстрого ввода-вывода, возвращающую «ложное» значение.

Возвращаясь к основной теме, процедура PFLT_POS_OPERATION_CALLBACK может возвращать различные значения, такие как:

*FLT_POSTOP_FINISHED_PROCESSING: это значение означает, что драйвер минифильтра уже завершил обработку завершения, и диспетчер фильтров продолжит обработку завершения операции ввода-вывода.
*FLT_POSTOP_MORE_PROCESSING_REQUIRED: это значение означает, что драйвер минифильтра приостановил выполнение, не вернет управление диспетчеру фильтров и не будет выполнять какие-либо задачи после операции, если обратный вызов после операции не отправил операцию ввода-вывода в рабочую очередь или рабочую процедуру для вызова функции FltCompletePendedPostOperation, чтобы вернуть управление операцией диспетчеру фильтров.
*FLT_POSTOP_DISALLOW_FSFILTER_IO: это значение означает, что драйверу минифильтра запрещена быстрая операция QueryOpen и принудительно выполняется операция по медленному пути.

Здесь следует упомянуть важный факт: пост-операции вызываются в контексте произвольного потока с IRQL <= DISPATCH_LEVEL. Кроме того, обработка завершения ввода-вывода с IRQL < DISPATCH_LEVEL не может выполняться в подпрограмме обратного вызова после операции и должна быть поставлена в очередь в рабочую очередь посредством вызова подпрограмм FltDoCompletionProcessingWhenSafe или FltQueueDeferredIoWorkItem. Исключениями из этого правила являются случаи, когда перед операцией драйвера мини-фильтра возвращается значение FLT_PREOP_SYNCHRONIZE, или даже существует уверенность в том, что процедура обратного вызова после создания будет вызываться на уровне IRQL_PASSIVE_LEVEL.

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

В общем, список возможностей, предоставляемых минифильтрами, довольно длинный, и одна из них — возможность изменения таких параметров, как адреса буферов, MDL и целевые файловые объекты, связанные с операциями ввода-вывода, и даже замена буферов. Эти операции можно эффективно выполнять с помощью обратных вызовов перед операцией, и они могут быть полезны в различных контекстах. После изменения параметра вызывается FltSetcallbackDataDirty для уведомления о том, что параметры были изменены. Кроме того, драйверы минифильтров также могут изменять статус ввода-вывода для данной операции. Чтобы завершить и выполнить необходимую очистку, авторы драйвера минифильтра должны освободить любой выделенный буфер.

Поскольку мы быстро обсудили возможность изменения параметров, читатели должны знать, что существует структура с именем FLT_CALLBACK_DATA, которая представляет операцию ввода-вывода и, конечно же, используется минифильтрами и собственным диспетчером фильтров над вводом-выводом:

1687108903512.png


Основными членами этой структуры являются:

*Flags: этот член представляет собой битовую маску флагов, описывающих операции ввода-вывода, и для минифильтров может быть указан только FLTFL_CALLBACK_DATA_DIRTY, который указывает, что содержимое структуры данных обратного вызова было изменено. Если эта структура инициализируется диспетчером фильтров, то можно использовать другие флаги, такие как FLTFL_CALLBACK_DATA_FAST_IO_OPERATION (структура данных обратного вызова представляет собой операцию быстрого ввода-вывода), FLTFL_CALLBACK_DATA_FS_FILTER_OPERATION (структура данных обратного вызова представляет операцию обратного вызова мини-фильтра файловой системы), FLTFL_CALLBACK_DATA_IRP_OPERATION ( структура данных обратного вызова представляет операцию на основе IRP). Читатели должны искать дополнительные флаги, используемые для инициализации структуры данных обратного вызова, а также во время обработки завершения.
*Iobp: этот член содержит указатель на структуру FLT_IO_PARAMETER_BLOCK, которая содержит параметры операции ввода-вывода.
*IoStatus: этот элемент содержит указатель на структуру IO_STATUS_BLOCK, которая содержит статус и информацию для операции ввода-вывода, и, как упоминалось ранее, ее содержимое может быть изменено обратным вызовом перед операцией или даже обратным вызовом после операции.

FLT_IO_PARAMETER_BLOCK, на который указывает параметр Iobp, имеет следующий состав:

1687108914720.png


Конечно, читатели лучше знакомы с большинством членов, входящих в эту структуру, и,в конце концов, мне не нужно объяснять их по одному, хотя объяснение есть в MSDN (Microsoft Learn): https://learn.microsoft.com/en-us/w...fltkernel/ns-fltkernel-flt_io_parameter_block. Кроме того, обратите внимание, что последним членом является Parameters, который задается гигантским объединением FLT_PARAMETERS, описанным по адресу: https://learn.microsoft.com/en-us/w...rs/ddi/fltkernel/ns-fltkernel-_flt_parameters.

Минифильтры участвуют в довольно обширном списке действий, а также могут генерировать и отправлять IRP-запросы, поэтому при реверс-инжиниринге этих типов драйверов мы можем увидеть подпрограммы, связанные с открытием, чтением, записью и даже созданием файлов (FltReadFile, FltWriteFile, FltCreateFile и так далее).

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

На самом деле коммуникационные порты не буферизуются, поэтому они быстрые и используются двунаправленным каналом связи. Кроме того, они создаются драйверами мини-фильтров, которые продолжают прослушивать любые входящие сообщения, и, как только приложение пользовательского режима пытается подключиться к этому порту, диспетчер фильтров вызывает процедуру ConnectNotifyCallback из драйвера мини-фильтра для обработки соединения, которое принимается только в том случае, если приложение пользовательского режима имеет необходимые и минимальные права, описанные дескриптором безопасности. Кроме того, диспетчер фильтров предлагает множество подпрограмм, которые связаны с коммуникационными портами, такими как FltSendMessage, FltCreateCommunicationPort, FltCloseClientPort, а также подпрограммы, доступные для использования приложением пользовательского режима, такие как FilterConnectCommunicationPort, FilterSendMessage, FilterGetMessage, FilterSendMessage и т. д. Наконец, для полноты уместно подчеркнуть, что приложение пользовательского режима может взаимодействовать с драйверами мини-фильтров с помощью обширной серии процедур для загрузки/выгрузки драйверов мини-фильтров (FltLoad, FltUnload), перечисления фильтров (FilterFindFirst, FilterFindNext, …), запроса информации. (FilterGetInformation, FilterGetInstanceInformation,…) и так далее.

К сожалению, установка драйвера минифильтра не так проста, как установка драйвера ядра, и необходимо создать INF-файл, что выходит за рамки данной статьи.

В системе Windows мы можем найти ряд драйверов минифильтров, выполнив следующие команды:

1687108929313.png


Конечно, читатели могут проверить высоту драйвера, проверив соответствующую запись в реестре. Например, для SysmonDrv у нас есть:

->Get-ChildItem -Path HKLM:\SYSTEM\CurrentControlSet\Services\SysmonDrv\Instances
Эта команда может делать гораздо больше, чем просто вывод списка драйверов мини-фильтра, например, их загрузку и выгрузку (как и ожидалось, выгрузка драйвера мини-фильтра вызывает процедуру FilterUnloadCallback):

1687108936769.png


В WinDbg драйверы минифильтров могут быть перечислены с помощью расширения отладчика (fltkd) WinDbg, которое предлагает ряд опций, таких как перечисление подробной информации о данном минифильтре, получение списка минифильтров, перечисление томов и кадров диспетчера фильтров, . Прежде чем продолжить, и поскольку я не знаю, привыкли ли читатели к этому, в этой среде я использую две виртуальные машины (на VMware): первая под управлением Windows 11 (хост) и вторая под управлением Windows 11 (целевой ). В моем случае в обеих системах установлен Windows SDK.

На цели:

1687109095882.png


На хосте:

1687109103625.png


Если все в порядке, вы должны увидеть приглашение WinDbg и можете выполнить следующее:

1687109124963.png


Мы также можем использовать команду расширения !fltkd.filters (она точно такая же). Как и в статье от Microsoft, связанной с обнаружением Защитника Windows, о которой упоминалось ранее в начале этого текста, фильтр Защитника Windows (WdFilter.sys) является желательным выбором. Мы также можем перечислить соответствующие коммуникационные порты, используя то же расширение fltkd. Получение адреса объекта из приведенного выше вывода (FLT_FILTER: ffff880f8ae9c4d0 "WdFilter" "328010") с помощью следующей команды:

1687109146214.png


Как показано на рис. 37, с минифильтром WdFilter связано только пять коммуникационных портов драйвера минифильтра. Если нам нужно собрать дополнительные сведения о самом драйвере минифильтра, выполните:

1687109157046.png


Вывод показывает нам ценную информацию о драйверах минифильтра, включая список коммуникационных портов. Если у читателей есть какие-либо проблемы с символами, проверьте, правильно ли настроен путь к символам, и принудительно загрузите их: команда .reload /f.

Если мы обратим внимание на детали, мы сможем реализовать другие термины, которые мы еще не прокомментировали:

* volume: драйвер фильтра файловой системы, соответствующий модели минифильтра или устаревшей модели фильтра файловой системы), также может выполнять операции ввода-вывода на одном или нескольких томах файловой системы, такие как ведение журнала, фильтрация ввода-вывода, изменение или мониторинг (как объяснялось ранее) , и на основе определения из Microsoft MSDN). Необходимо создать объект фильтрующего устройства (функция IoCreateDevice) и присоединить его к стеку драйвера фильтра, вызвав функцию IoAttachDeviceToStackSafe.
*context: это структура, которая может быть связана с объектом диспетчера фильтров и использоваться для сохранения и передачи информации (контекста) об объекте. Эта структура определяется самим драйвером минифильтра, и могут быть контексты, связанные с томами, файлами, экземплярами, транзакциями, дескрипторами потоков (файловыми объектами) и потоками. Читателям может быть интересно узнать, что такие функции, как FltAllocateContext (для создания контекстов), FltRegisterFilter (регистрация контекстов), FltSetFileContext | FltSetInstanceContext | FltSetStreamContext | FltSetVolumeContext | FltSetTransactionContext (установка контекстов) и другие, связанные с манипулированием контекстом. Кроме того, есть интересный пример (код), демонстрирующий, как это сделать, который доступен по адресу: https://github.com/Microsoft/Windows-driver-samples/tree/main/filesys/miniFilter/ctx.

Чтобы получить список томов и соответствующих подключенных к ним драйверов фильтров (обратите внимание на драйвер WdFilter), вы можете выполнить следующую команду:

1687109185143.png


Чтобы просмотреть информацию о конкретном томе (структура FLT_VOLUME), выполните: !fltkd.volume ffff880f8a92f010 (это второй том, указанный ранее)

1687109206183.png


Чтобы вывести конкретную информацию о данном экземпляре (вложение в структуру FLT_VOLUME), выполните:

1687109216674.png


Конечно, мы можем проникнуть внутрь структур и узнать гораздо больше информации. Например, мы можем получить информацию от драйвера WdFilter, наложив на его адрес структуру _FLT_FILTER:

1687109228987.png


Здесь присутствуют все функции, понятия и термины, о которых мы упоминали ранее: высота, функция FilterUnload (вызывается при выгрузке драйвера минифильтра), InstanceQueryTearDown, контексты, указатель на массив структур FLT_OPERATION_REGISTRATION (содержит обратные вызовы операций) и так далее. С другой стороны, концепцию DriverObject мы уже сейчас используем, и вскоре мы рассмотрим типичный вывод с ее использованием.

Хотя я не объяснял ранее, каждый фрейм диспетчера фильтров работает как заполнитель в стеке драйвера ввода-вывода, и к этому фрейму присоединяются мини-фильтры. Например, в стеке драйверов ввода-вывода могут существовать два кадра фильтра, а в середине — устаревший драйвер фильтра. В этом случае мы могли бы выбрать, будет ли драйвер минифильтра присоединен к фрейму фильтра до устаревшего драйвера фильтра или после устаревшего драйвера фильтра.

В любом случае, мы можем перечислить предоперации и постоперации (адреса и, если возможно, соответствующие имена), связанные с драйвером. Например, мы можем перечислить первые десять операций, выполнив следующую команду:

1687109242422.png


Изображение на выходе маленькое, и в данном конкретном случае мы не получили соответствующих имён. Если вы попробуете ту же команду, но без опции «-c», вы получите построчный вывод (длиннее, но лучше). Аналогичный вывод, но из драйвера WoF (Windows Overlay Filter), показан ниже для случая, когда отображаются имена подпрограмм (извините за маленький размер):

1687109251049.png


Возвращаясь к драйверу минифильтра WdFilter, мы можем получить информацию обратного вызова, связанную с данным экземпляром:

1687109258502.png


Все узлы обратного вызова имеют связанные имена, такие как ACQUIRE_FOR_SECTION_SYNC, CREATE, READ, WRITE, SET_INFORMATION, QUERY_EA, SET_EA, DIRECTORY_CONTROL, FILE_SYSTEM_CONTROL и CLEANUP.

Существует несколько MUP (Multiple UNC Provider), причем MUP является компонентом ядра, отвечающим за направление доступа к удаленной файловой системе через UNC к сетевому перенаправителю, и он связан с каждым узлом обратного вызова (см. рисунок выше).

Точно так же, как мы сделали со структурой _FLT_FILTER, мы можем выбрать один из узлов обратного вызова и получить информацию, наложив его на структуру _CALLBACK_NODE, как показано ниже:

1687109271528.png


Есть несколько деталей, чтобы прокомментировать вывод:

* У нас есть двусвязный список структур CALLBACK_NODE.
* Мы видим ссылку на обратные вызовы PreOperation и PostOperation.
* Все ссылки на имена «пустые», но мы уже знаем, что этого не происходит с другими драйверами минифильтров, такими как WoF (Windows Overlay Filter).

Так как минифильтру необходимо передавать контексты для сохранения и передачи информации об объекте, ему потребовался такой механизм, как контексты минифильтра (CONTEXT_NODE), и, как и ожидалось, с экземпляром также связан контекст:

1687109282642.png


Проверяя четвертую строку вывода, мы видим ссылку на NonPagedPool. За исключением контекстов томов, которые должны быть выделены из NonPagedPool, все остальные контексты (экземпляры, потоки, файлы, дескрипторы транзакций и потоков) могут быть выделены из PagedPool или NonPagedPool.

В любом случае, если читатели захотят, можно исследовать структуру _CONTEXT_NODE, используя ту же технику, которая использовалась до сих пор, и выбрать один из узлов контекста, как показано на следующей странице:

1687109462480.png


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

1687109477348.png


Возвращаясь к теме коммуникационных портов, пришло время изучить один из этих портов:

1687109485721.png


Как мы узнали ранее, коммуникационный порт (созданный функцией FltCreateCommunicationPort) важен для поддержания связи между драйвером минифильтра и приложением, и, как и ожидалось, существует ряд функций, связанных с коммуникационными задачами, и немногие из этих функций — это FilterConnectCommunicationPort, FltSendMessage, FilterSendMessage, FilterReplyMessage и так далее.

Кроме того, драйверы используют механизмы для обмена сообщениями (его заголовок представлен структурой FILTER_MESSAGE_HEADER), для сигнализации об ожидании сообщений (очередь сообщений, представленная структурой _FLT_MESSAGE_WAITER_QUEUE), обратного вызова для уведомления о доступности сообщения (подпрограмма MessageNotifyCallback, которая вызывается при IRQL=PASSIVE_LEVEL диспетчером фильтров) и PortCookie, который используется для уникальной идентификации клиентского порта или порта сервера, в зависимости от стороны связи.

На всякий случай, если читателям интересно, есть модуль PowerShell под названием NtObjectManager, написанный Джеймсом Форшоу (https://www.powershellgallery.com/packages/NtObjectManager/1.1.33), который легко предоставляет вам коммуникационные порты:

1687109499441.png


1687109510866.png

Возвращаясь к структуре _FLT_PORT_OBJECT, член MegQ является, как мы уже объяснили, указателем на структуру _FLT_MESSAGE_WAITER_QUEUE, которую можно применить к адресу и, выполнив следующую последовательность команд, мы имеем:

1687109522388.png


Как мы можем понять, из заданной структуры очереди сообщений мы получили структуры _ETHREAD и _IO_STACK_LOCATION.

Исследуя четвертую команду, мы имеем:

*dx Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY *)0xffff880f905cd1c8, "nt!_IRP", "Tail.Overlay.ListEntry")

Читатели, конечно, могут спросить, откуда берутся компоненты этой команды. Эта команда WinDbg использует LINQ (Language-Integrated Query), который хорошо известен из программирования на C#, а синтаксис этой команды взят из документации WinDbg на MSDN. В двух словах, эта команда анализирует структуру nt!_LIST_ENTRY, и ее состав прост:

*0xffff880f905cd1c8: указатель Flink
*nt!_IRP: ссылка на структуру.
*Tail.Overlay.ListEntry: поле из структуры _IRP, на которое ссылается указатель Flink.

Остается вопрос: откуда мне знать, что этот список указывает на структуру nt!IRP и, в частности, на Tail.Overlay. Поле ListEntry? Откройте файл fltmgr.sys в IDA Pro, и даже не выполняя никакой обработки кода, вы можете легко заметить, что функция FltpAddMessageWaiter() получает три аргумента: указатель на структуру _IO_Csq, указатель на структуру IRP и третий аргумент, связанный с контекст:

1687109541625.png


В строке 6 у нас есть ссылка на p_ListEntry = &Irp->Tail.Overlay.ListEntry, а в строках 14 и 15 читатели могут проверить настройку двусвязного списка. В любом случае, когда читатели достигают структуры _ETHREAD, можно получить значение любого поля.

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

Конечно, есть еще подробности, и пора двигаться дальше.

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

*DriverEntry: это та же процедура, что и для драйверов ядра, и таким же образом она запрашивается для всех драйверов фильтров. Кроме того, эта подпрограмма служит отправной точкой для ключевых действий, и, например, здесь драйвер минифильтра может зарегистрировать (через подпрограмму FltRegisterFilter) один предоперационный обратный вызов и один послеоперационный обратный вызов (присутствие обоих не обязательно) для каждый из различных типов ввода/вывода обрабатывается и фильтруется минифильтром.
*FltRegisterFilter: эта процедура используется драйверами минифильтров для регистрации, чтобы предоставить список процедур обратного вызова диспетчеру фильтров и, в то же время, зарегистрироваться в списке драйверов минифильтров.
*FltStartFiltering: эта процедура уведомляет диспетчер фильтров о том, что он готов и может начать фильтровать запросы, присоединяясь к томам.
*FltCreateCommunicationPort: эта процедура открывает порт сервера связи ядра.
*FltCloseCommunicationPort: эта процедура закрывает порт сервера связи ядра.
*FilterUnloadCallback: это процедура, отвечающая за выгрузку драйвера минифильтра. Это необязательная процедура.
*FltUnregisterFilter: эта процедура отменяет регистрацию драйвера минифильтра.

Очень важно понимать концепцию обратного вызова перед операцией, потому что каждый драйвер минифильтра может иметь свой собственный, и каждый связанный обратный вызов перед операцией для каждого зарегистрированного минифильтра будет вызываться из драйвера минифильтра, который удерживает более высокую высоту, до самой низкой для этого конкретного типа Операция ввода/вывода. Кроме того, важен параметр Register из подпрограммы FltRegister, поскольку он содержит указатель на структуру FLT_REGISTRATION. Эта структура содержит поле/член, который на самом деле является массивом структур FLT_OPERATION_REGISTRATION, каждая из которых представляет тип операции, управляемой и фильтруемой драйвером минифильтра. Конечно, это может показаться запутанным, потому что здесь есть три уровня перенаправления, но это не так уж редко встречается с драйверами ядра и минифильтра. Однако это еще не конец, и, поскольку существует две модели драйверов фильтров файловой системы, драйверы минифильтров сначала получают операцию ввода-вывода, а затем ее получают для обработки устаревшие драйверы фильтров файловой системы. После этого соответствующая файловая система получает операцию ввода-вывода для дальнейшей обработки. На стороне порядка подпрограммы после операции (каждый драйвер минифильтра, зарегистрированный для обработки этого типа операции ввода-вывода, может иметь или не иметь обратный вызов после операции) начинают свою работу в обратном порядке, заканчивают обработку операции ввода-вывода, возвращают менеджерам фильтров, которые передают его следующему драйверу минифильтра на верхнем уровне. На этом этапе нетрудно понять, что минифильтр файловой системы, скорее всего, будет использовать множество процедур обратного вызова перед операцией для управления и фильтрации операций ввода-вывода, и эти обратные вызовы перед операцией могут возвращать диспетчеру фильтров такие значения, как FLT_PREOP_SYNCHRONIZE (для операций на основе IRP), тип которого может быть подтвержден макросом FLT_IS_IRP_OPERATION, и послеоперационная подпрограмма будет вызываться на этапе завершения ввода-вывода), FLT_PROP_SUCCESS_NO_CALLBACK (во время фазы завершения ввода-вывода не будут вызываться подпрограммы обратного вызова после операции) и FLT_PREOP_SUCCESS_WITH_CALLBACK (процедуры обратного вызова после операции будет вызываться на этапе завершения ввода-вывода), например, как уже упоминалось ранее в этой статье. Конечно, таким же образом драйвер минифильтра может иметь более одной процедуры обратного вызова после операции, которые могут выполняться на уровне IRQL ниже или равном DISPATCH_LEVEL, и в связи с этим структуры данных должны размещаться в невыгружаемом пуле. В любом случае, послеоперационные подпрограммы вызываются в произвольном контексте. Драйверы минифильтров также передают информацию (данные) между приложениями, работающими в пользовательском режиме, и другими драйверами минифильтров, работающими на более низких уровнях, которые могут достигать драйверов устройств, и поскольку эти операции передачи данных также используют своего рода буфер.

Нет никаких новостей, связанных с буферами данных, и драйверы минифильтров файловой системы используют те же методы, что и драйверы ядра, для доступа к буферам, которые являются буферизованным вводом-выводом (в основном используются в операциях IRP, таких как Например, IRP_MJ_CREATE и IRP_MJ_QUERY_INFORMATION), прямой ввод-вывод и ни один ввод-вывод (он может использоваться такими операциями, как IRP_MJ_SYSTEM_CONTROL и IRP_MJ_QUERY_SECURITY). Кроме того, важные и обычные операции, такие как IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_DEVICE_CONTROL и IRP_MJ_QUERY_OPERATION (упомянутый выше) можно настроить как операции быстрого ввода-вывода или операции на основе IRP.

Как уже поняли читатели, одинаковые основные коды IRP-операций ввода-вывода действительны для драйверов минифильтров, и вы можете проверить их с помощью известной команды WinDbg:
 
1687191415852.png


Драйвер фильтра Windows Cloud Files (cldflt.sys) — это драйвер минифильтра файловой системы, связанный, например, с OneDrive. GsDriverEntry() представляет собой подпрограмму, сгенерированную автоматически при сборке драйвера, которая выполняет короткую инициализацию и вскоре после завершения инициализации вызывает реальный DriverEntry(), который был реализован.

Двигаясь вперед, я хотел бы прокомментировать ECP (дополнительные параметры создания), которые представляют собой структуры, содержащие информацию, используемую во время создания файла, и которые могут быть присоединены к операциям ввода-вывода с помощью структуры ECP_LIST. Например, драйвер фильтра файловой системы может манипулировать ECP (дополнительными параметрами создания) для обработки операций IRP_MJ_CREATE, и именно эти ECP используются для различения вызовов NtCreateUserProcess() и NtCreateProcessEx(), которые также упоминались в статье Microsoft по адресу начало этого текста. ECP могут быть одного из двух доступных типов: определяемые системой ECP, которые используются ОС для присоединения дополнительной информации к IRP_MJ_CREATE, упомянутой ранее, и определяемые пользователем ECP, которые используются драйверами ядра для обработки и добавления дополнительной информации к операции IRP_MJ_CREATE. Читатели, вероятно, узнают манипуляции с ECP, когда найдут такие подпрограммы, как FltAllocateExtraCreateParameterList (для выделения памяти для структуры ECP_LIST), FltFreeExtraCreateParameterList (для освобождения памяти, используемой структурой ECP_LIST), FltAllocateExtraCreateParameter (для выделения пула выгружаемой памяти для структуры контекста ECP, возвращающей указатель на it), FltInsertExtraCreateParameter (для вставки структур контекста ECP в структуру ECP_LIST), IoInitializeDriverCreateContext (для инициации IO_DRIVER_CREATE_CONTEXT_STRUCTURE) и, наконец, IoCreateFileEx|FltCreateFileEx2 (для присоединения ECP к заданному IRP_MJ_CREATE_CONTEXT).

Конечно, существует обширный список подпрограмм для обработки и управления ECP, таких как FltGetEcpListFromCallbackData (возвращает указатель на список ECP, связанный с объектом данных обратного вызова операции создания), FltFindExtraCreateParameter (ищет в предоставленном списке ECP структуру контекста ECP) и FltIsEcpFromUserMode (проверяет, исходит ли ECP из пользовательского режима). Пример использования этих подпрограмм показан ниже:

1687191432660.png


Возвращаясь еще раз к статье Microsoft, GUID_ECP_CREATE_USER_PROCESS и соответствующий контекст CREATE_USER_PROCESS_ECP_CONTEXT, который содержит токен создаваемого процесса, используются ядром, когда оно открывает исполняемый файл процесса. Таким образом, в то время как NtCreateUserProcess добавляет ECP для создания процесса, NtCreateProcessEx этого не делает, поскольку использует уже созданный (существующий) дескриптор раздела. Это упрощает определение того, когда используется та или иная функция.

Конечно, ECP — не единственная интересная тема, потому что в Windows 11 появился новый механизм под названием BypassIO, который запрашивается для дескриптора файла и делает доступ к вводу-выводу для чтения файлов лучше и быстрее благодаря меньше накладных расходов, и это используется драйверами минифильтров.
Большим преимуществом использования BypassIO является то, что запрос ввода-вывода не проходит через весь стек драйвера, а направляется непосредственно в файловую систему NTFS (минуя том и стек файловой системы, а последний может состоять из Volume Device Object (VDO) или Control Device Object (CDO) в дополнение к обычным объектам устройств минифильтра), а оттуда — к базовым томам и дискам. Кроме того, вызовы таких функций, как подпрограмма FltFsControlFile (или собственные эквиваленты) с управляющим кодом FSCTL_MANAGE_BYPASS_IO, являются обычными при запросе и выполнении операций BypassIO.

Читатели увидят управляющие коды FSCTL_MANAGE_BYPASS_IO и IOCTL_STORAGE_MANAGE_BYPASS_IO, связанные с драйверами минифильтров, использующими BypassIO, для чего на некоторое время требуется файловая система NTFS на устройстве хранения NVMe в Windows 11. Вы также должны обратить внимание на такие запросы, как FS_BPIO_OP_ENABLE, FS_BPIO_OP_DISABLE, FS_BPIO_OP_QUERY, FS_BPIO_OP_GET_INFO и другие подобные, в основном потому, что они связаны с предоперационными обратными вызовами.

Мы можем легко проверить поддержку функции BypassIO, выполнив следующую команду:

1687191446901.png


Возвращаясь к CDO (объект устройства управления) и VDO (объект устройства тома), упомянутым выше, которые необязательно создаются драйверами минифильтра файловой системы (файловые системы должны создавать CDO, но это необязательно для драйвера минифильтра файловой системы, хотя он обычно используется). Уместно подчеркнуть, что CDO работает как представление драйвера минифильтра для приложения пользовательского режима, и, конечно, помимо системы. Позже FDO (объект драйвера фильтра) будет выполнять все связанные задачи фильтрации в данной файловой системе или томе. Эта схема и состав не зависят от драйвера, обрабатывающего IRP или Fast I/O. Как объяснялось ранее, IRP используются в общих операциях (синхронных или асинхронных), в то время как быстрый ввод-вывод используется по сравнению с синхронными операциями, что дает преимущество в ускорении передачи между прикладным/пользовательским буфером и системным кешем, таким образом минуя возможную файловую систему и объемный стек в середине пути. Кроме того, мы также должны помнить, что файловая система минифильтра должна реализовывать подпрограммы быстрого ввода-вывода, даже если они их не поддерживают (и, как рекомендуется, возвращают FALSE).

До сих пор мы объясняли WDM (модель драйвера Windows), включая ряд концепций, связанных с драйверами ядра и драйверами минифильтров, потому что все эти концепции являются основой современных драйверов. Однако много лет назад Microsoft представила другую структуру для разработки драйверов под названием Windows Driver Frameworks (WDF), которая предлагает своего рода абстракцию, упрощающую разработку драйверов, и, конечно же, рано или поздно читатели обратят и проанализируют образец в своих повседневных задачах.

6. Обзор Windows Driver Frameworks (WDF)


Первые факты о WDF таковы:

*Они включают две важные структуры: KMDF (структура драйверов режима ядра) и UMDF (инфраструктура драйверов пользовательского режима).
*Microsoft предлагает соответствующий исходный код, доступный по адресу: https://github.com/Microsoft/Windows-Driver-Frameworks.
*Microsoft Visual Studio, как и ожидалось, предлагает набор шаблонов для разработки драйверов KMDF и UMDF.

Эти фреймворки (KMDF и UMDF) предлагают абстракцию от WDM (читатели могут согласиться с тем, что это действительно сложно) и выполняют важные функции, такие как Plug-and-Play и управление питанием, и все сделано для того, чтобы предложить дружественный интерфейс для разработчиков. Мы не видели ни одной из этих деталей в наших предыдущих обсуждениях, потому что мы сосредоточены на программном драйвере, не взаимодействуя непосредственно с оборудованием. В любом случае, несмотря на то, что модель отличается, цель одна и та же: управление связью между пользовательскими приложениями и устройствами или другими драйверами. В этой статье я буду ориентироваться на KMDF, но драйверы UMDF должны быть выделены, потому что они предлагают невероятно привлекательные функции, такие как обработка только памяти, связанной с процессом, более простое взаимодействие со средой, ограниченный доступ к системным файлам и даже данным от пользователей, и ряд других преимуществ, которые, в конечном счете, могут соответствовать требованиям проекта.

В общем, WDF (Windows Driver Frameworks) состоит из центральной подпрограммы DriverEntry, которая отвечает за вызов подпрограммы WfdDriverCreate (эта подпрограмма создает объект драйвера, представляющий драйвер), и ряда функций обратного вызова события, которые, наконец, вызывают методы объекта экспортируется собственной структурой. Другими словами, программирование ориентировано на события, поэтому объекты поддерживают одно или несколько из этих возможных событий, которые включаются в соответствии с системными изменениями или даже из-за новых запросов ввода-вывода. Самое приятное то, что структура драйвера предлагает процедуры по умолчанию для всех возможных событий. Драйвер не обязан управлять каким-либо из них, и, если драйвер хочет переопределить какую-либо из подпрограмм по умолчанию для обработки соответствующего события, драйверу необходимо зарегистрировать новый обратный вызов (вызывается при возникновении события) и уведомить драйвер о том, что произошло такое событие, которое дает водителю возможность выполнять дальнейшую обработку и задачи. Если у читателей есть какие-либо проблемы с пониманием этой концепции обратного вызова, подумайте об этом как о сообщении, сигнализирующем о том, что произошло что-то важное (событие), и драйвер может заинтересоваться обработкой. Модель WDF следует предлагаемому стеку драйверов:

*приложение → ядро → объект фильтрующего устройства (драйвер фильтра) → объект функционального устройства (функциональный драйвер) → объект фильтрующего устройства (драйвер фильтра) → объект физического устройства (драйвер шины)

Поскольку большинство общих понятий схожи, нам приходится адаптировать наши знания к новым именам функций и, в конечном счете, к понятиям. Как мы узнали ранее, драйверы могут реализовывать методы обратного вызова в соответствии с ожидаемыми событиями, а затем регистрировать эти обратные вызовы в фреймворке. Соглашение об именах для функций обратного вызова — EvtObjectEvent, где часть Object представляет упомянутый объект структуры, а часть Event представляет предоставленное событие. KMDF также следует правильно сформированному синтаксису своих методов: Wdf[Object][Operation], где Object относится к объекту, участвующему в операции, а Operation относится к цели метода.

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

Один из аспектов номенклатуры, который читатели уже поняли, заключается в том, что большинство (не все) объектов и подпрограмм имеют префикс строки «Wdf» (верхний регистр, нижний регистр или смешанное обозначение). Кроме того, вы увидите имена таких объектов, как WDFDEVICE (устройство), WDFDPC (dpc), WDFFILEOBJECT (файл), WDFINTERRUPT (прерывание), WDFSPINLOCK (спин-блокировка), WDFQUEUE (очередь), а также подпрограммы, такие как WdfDriverCreate, WdfDeviceCreate, WdmDeviceCreateSymbolicLink, Вдфобжектреференс,WdfDeviceCreateDeviceInterface, WdfRequestRetrieveInputBuffer, WdfRequestRetrieveOutputBuffer, WdfRequestRetrieveInputWdmMdl, WdfRequestRetrieveOutputWdmMdl, WdfAllocateContext (размещены в невыгружаемом пуле и взяты как часть объекта, что имеет эквивалентное значение WDM расширение устройства), WdfIoQueueCreate и так далее. Такие объекты имеют такие свойства, как ParentObject, Size,ContextTypeInfo и т. д., которые хранятся в структуре WDF_OBJECT_ATTRIBUTES и инициализируются функцией WDF_OBJECT_ATTRIBUTES_INIT. Между прочим, существуют структуры конфигурации, связанные с объектами, которые содержат такую информацию, как указатели на обратные вызовы событий, и номенклатура таких структур WDF_<object>_CONFIG, которые обычно инициализируются функциями/макросами, которые также следуют WDF_<объект>_CONFIG_INIT в качестве номенклатуры. Поэтому при создании драйвера KMDF читатели будут следовать обычному порядку объявления и инициализации структур конфигурации, затем
инициализации атрибутов и, наконец, созданию объекта.

Точно так же, как мы видели для WDM, модель WDF состоит из запросов ввода-вывода, очередей, областей памяти и устройств, конечно. Благодаря этому механизму, когда операционная система отправляет запрос ввода-вывода драйверу WDF, платформа отвечает за обработку операции отправки, постановки в очередь и завершения запроса. Кроме того, поскольку большинство приложений будут взаимодействовать с драйверами для чтения, записи или даже управления устройствами, такие подпрограммы, как подпрограмма WdfIoQueueCreate, будут использоваться для создания объекта очереди, представляющего соответствующую очередь ввода-вывода (как обычно, все сводится к управлению вводом-выводом). Здесь уместно подчеркнуть, что общая иерархия WDF задается объектом драйвера → объект устройства → объект очереди → объект запроса. Драйверы WDF также обрабатывают прерывания, вызывая такие подпрограммы, как подпрограмма WdfInterruptCreate, и, как вы можете себе представить, они будут создавать объекты прерывания для каждого заданного прерывания и регистрировать функции обратного вызова, которые мне не нужно повторять в том же объяснении. Кстати, обратные вызовы обычно имеют суффикс Evt string, поэтому есть EvtCleanupCallback, EvtDestroyCallback,
EvtDeviceAdd, EvtIoRead, EvtIoWrite и так далее.

KMDF, конечно, обширная тема и имеет свои особенности, но она близка к разработке WDM, так что этой пары страниц достаточно, чтобы ознакомиться с основами KMDF.

7. Дополнительная информация о обратных вызовах


Возвращаясь к теме обратного вызова, Windows предлагает серию API-интерфейсов обратного вызова ядра, которые экспортируются ядром. (NtosKrnl.exe + wdm.h) и какие драйверы могут использовать для регистрации своих подпрограмм обратного вызова, которые в конечном итоге будут вызываться для определенных событий и условий компонентов ядра.

Поскольку мы обсуждаем драйверы ядра и драйверы фильтров, было бы полезно оставить несколько слов по этой теме. Если читатели пишут драйвер ядра, они могут использовать объект обратного вызова из других драйверов и зарегистрировать подпрограмму (InitializeObjectAttributes() + ExCreateCallback() + ExRegisterCallback()), которая будет вызываться при срабатывании определенного обратного вызова (наступлении заданного условия).

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

Список обратных вызовов ядра (иногда называемых системными обратными вызовами) действительно велик, и я
приведу здесь только определения и концепции некоторых из них:

*CmRegisterCallbackEx(): эта функция регистрирует подпрограмму RegistryCallback, которая используется драйверами фильтров для отслеживания и изменения любых операций с реестром, таких как удаление ключа, переименование, изменение значения ключа, перечисление, создание и т. д. Например, вредоносное ПО может использовать этот обратный вызов для восстановления вредоносного содержимого (например, вредоносной записи, используемой для сохраняемости) вскоре после того, как системный администратор удалил запись, связанную с сохраняемостью. Как мы рассмотрели ранее, параметр Altitude (второй параметр, показанный ниже) определяет положение драйвера минифильтра по сравнению с другими минифильтрами в стеке ввода-вывода. Наконец, следует обратить внимание на то, что первый параметр (Function) — это указатель на процедуру RegistryCallback, которую нужно зарегистрировать, а третий параметр (Driver) — это указатель на традиционную структуру DRIVER_OBJECT, представляющую сам драйвер.

1687191490927.png


*FsRtlRegisterFileSystemFilterCallbacks(): Драйверы файловой системы вызывают эту функцию для регистрации процедур обратного вызова уведомлений, которые будут вызываться, когда файловая система выполняет определенные операции. Его второй параметр указывает на структуру FS_FILTER_CALLBACKS, которая содержит указатель входа процедур обратного вызова уведомлений, предоставленных вызывающей стороной. В конце выполнения обычно возвращается значение STATUS_SUCCESS или STATUS_FSFILTER_OP_COMPLETED_SUCCESSFULLY, но последнее означает, что операция FsFilter завершена.
*IoRegisterBootDriverCallback(): эта функция регистрирует подпрограмму BOOT_DRIVER_CALLBACK_FUNCTION, которая будет вызываться на этапе инициализации драйверов начальной загрузки и роль которой состоит в отслеживании событий начальной загрузки и возврате данных ядру. Например, драйвер ELAM (Early Launch Anti-Malware), представляющий собой механизм, который может использоваться средствами защиты, такими как антивирусные программы, может регистрировать методы обратного вызова с помощью этой функции для проверки проблем, связанных с отсутствием целостности других загрузочных драйверов или даже записи реестра, которые также можно отслеживать с помощью процедуры CmRegisterCallbackEx, как упоминалось ранее. Даже не обращая внимания на наше внимание, вы можете проверить WdBoot.sys (драйвер ELAM) с помощью IDA Pro + WinDbg (в конфигурации удаленной установки), если хотите. В качестве короткого примера, чтобы помочь вам начать:
* Откройте драйвер WdBoot.sys (из папки C:\Windows\system32\drivers) из удаленной системы Windows (позже мы выполним отладку) в IDA Pro.
* Найдите подпрограмму DriverEntry (она вызывается подпрограммой GsDriverEntry)
* Изучите драйвер WdBoot.sys на PEBear. Запишите базу образа.
* Через удаленный сеанс WinDbg (шаги я объяснял ранее) установите точку останова на удаленном (целевом) компьютере, чтобы остановить выполнение при загрузке драйвера путем выполнения sxe ld WdBoot.sys и перезагрузить систему. Если вы хотите увидеть все сообщения от отладчика, выполните ed nt!Kd_DEFAULT_MASK 0xFFFFFFFF
* После того, как система перезагрузилась и остановилась при загрузке WdBoot.sys, установите точку останова на WdBoot!DriverEntry (помните, что у нас нет символов), выполнив команду bp WdBoot + 0x1C000B000 — 0x1C0000000 (эффективно WdBoot + 0xB000).
* Введите g, чтобы возобновить работу системы.

1687191504029.png


1687191511354.png


1687191520382.png


С этого момента можно выполнять все обычные исследования с помощью WinDbg. В любом случае, часть драйвера, использующая подпрограммы IoRegisterBootDriverCallback (и соответствующие IoUnRegisterBootDriverCallback), выглядит следующим образом:

1687191535297.png


Поскольку подпрограмма MmGetSystemRoutineAddress отвечает за возврат указателя на заданную функцию, указанную параметром SystemRoutine, который содержит указатель на строку «IoRegisterBootDriverCallback», то адрес обратного вызова эффективно разрешается.

Похоже, что после разрешения обратных вызовов Защитник Windows загрузит свои подписи в соответствии со строкой 133 выше. Пройдя немного дальше, мы узнаем другую процедуру, связанную с обратным вызовом, о котором мы уже упоминали ранее (CmRegisterCallback), и даже API (ExFreePoolWithTag), ответственный за освобождение области пула памяти, связанной с предоставленным тегом (в данном случае EBsg). Наконец, мы видим, что IoRegisterBootDriverCallback (помните, что его указатель был сохранен в переменной SystemRoutineAddress), используемый для регистрации обратного вызова с именем MbEbBootDriverCallback, как показано в строке 221:

1687191548350.png


Процедура BOOT_DRIVER_CALLBACK_FUNCTION отвечает за мониторинг запуска данного драйвера и соответствует
первому параметру процедуры IoRegisterBootDriverCallback, как показано ниже:

1687191556390.png


Это достаточно о подпрограмме IoRegisterBootDriverCallback, и пришло время вернуться и прокомментировать другие системные обратные вызовы.

*IoRegisterFsRegistrationChangeEx(): эта процедура регистрирует процедуру уведомления (процедуру обратного вызова) фильтра файловой системы, которая вызывается, когда файловая система регистрируется или отменяет регистрацию. Большинство EDR активно следят за этой процедурой. Первый параметр — это указатель на объект драйвера для драйвера фильтра файловой системы, а второй параметр — это указатель на подпрограмму PDRIVER_FS_NOTIFICATION, которая вызывается файловой системой всегда, когда она регистрирует или даже отменяет регистрацию, вызывая такие функции, как IoRegisterFileSystem() и IoUnregisterFileSystem() соответственно.
*IoRegisterFsRegistrationChangeMountAware(): эта функция предназначена для регистрации подпрограмм уведомлений (методов обратного вызова) драйверов фильтра файловой системы, и, как и ожидалось, второй аргумент указывает на подпрограмму PSDRIVER_FS_NOTIFICATION, которая вызывается, когда файловая система монтируется (активна) или размонтируется (неактивный). Первый параметр, как обычно, является указателем на объект драйвера для драйверов файловой системы.
*ExAllocateTimer(): эта функция отвечает за выделение и инициализацию объекта таймера с помощью процедуры обратного вызова ExTimerCallback, которую Windows вызывает, когда истекает временной интервал таймера
(представленный объектом таймера EX_TIMER).

1687191564004.png


Несколько руткитов использовали этот обратный вызов для создания объекта таймера в непроизвольном контексте угрозы для планирования операций, которые будут выполняться периодически. Например, профессионалы, занимающиеся поиском таймеров, могут использовать расширение WinDbg !timer для отображения всех ожидающих таймеров в системе:

1687191570734.png


В качестве простого примера использования подпрограммы ExAllocateTimer мы можем проверить любой драйвер фильтра как WoF.sys (Windows Overlay Filter), который инициализирует объект таймера, связанный с обратным вызовом с именем TlgAggregateInternalFlushTimerCallbackKernelMode. Обратную работу подпрограммы, показанной ниже, можно значительно улучшить, но пока этого достаточно, потому что мы хотим выделить только использование одной подпрограммы:

1687191581382.png


*IoSetCompletionRoutineEx(): Хотя мы уже комментировали эту подпрограмму в первый момент на странице 25, уместно заметить, что эта подпрограмма регистрирует подпрограмму IoCompletion, которая обычно вызывается, когда драйвер следующего уровня (нижний драйвер) завершает выполнение задачи запрошенной операции, связанная с предоставленным IRP. Процедура завершения, которая выполняется из произвольного потока или даже из контекста DPC (вызовы отложенных процедур), отвечает за определение того, требуется ли какая-либо дополнительная обработка для данного IRP. В качестве дополнительной информации, подпрограмма DPC (DpcForIsr()), связанная с объектом DPC, ставится в очередь ISR (подпрограмма обслуживания прерываний — ее выполнение должно быть коротким и быстрым) и выполняется в более поздний момент с более низким IRQL. (IRQL_DISPATCH_LEVEL), чем высокий уровень ISR, и, в двух словах, он отвечает за выполнение тяжелой работы, которая не выполнялась ISR. Любая оставшаяся работа, которая не была завершена подпрограммой DpcForIsr(), может быть выполнена подпрограммами CustomDpc(), которые являются дополнительными DPC. Структура DEVICE_OBJECT содержит член структуры KDPC (поле Dpc), как показано ниже, который используется для запроса упомянутой процедуры DPC в пределах ISR. Поэтому, как только мы получим какой-либо ожидающий DPC (их можно перечислить, используя расширение !dpcs), мы можем получить его соответствующий адрес и выполнить наложение на структуру _KDPC, чтобы лучше понять дальнейшие детали:

1687191592179.png


Прежде чем продолжить, небольшое примечание: в конечном итоге ваша тестовая система ничего не ожидает в точное время, когда вы выполняете этот тест, потому что это зависит от текущей активности.

Чтобы получить дополнительную информацию о предоставленном KDPC, выполните:

1687191607020.png


1687191615887.png


Примечание: адрес KPCR (0x0xffffff806205434c0) взят из вывода расширения !pcr (не показано)

*KeInitializeDpc(): эта процедура дополняет описанную выше тему, поскольку ее роль заключается в инициализации объекта DPC и регистрации процедуры CustomDpc для такого объекта. Как и ожидалось, второй аргумент — это указатель на функцию обратного вызова KDEFERRED_ROUTINE, которая выполняется после ISR (процедуры обслуживания прерываний). Кроме того, подпрограмма CustomTimerDpc выполняется после истечения временного интервала данного объекта таймера, и, конечно же, читатели могут установить связь с функциями таймера, упомянутыми ранее в этой статье.

1687191625320.png


*KeInitializeApc(): эта процедура используется для инициализации объекта APC (асинхронный вызов процедур). Как читатели могли уже знать, APC — это своего рода механизм ядра, который используется для постановки задачи в очередь, которая будет выполняться в контексте данного потока. Кроме того, APC использовались, например, для внедрения кода в пользовательский процесс (в состоянии предупреждения) из драйвера ядра. Существуют различные типы APC (UserAPC, Special User APC и Kernel APC), первые два случая которых связаны с такими API, как QueueUserAPC() и NtQueueApcThreadEx2() соответственно. Kernel APC немного отличается, работает в режиме ядра на IRQL = PASSIVE_LEVEL (APC Special Kernel работает на IRQL = APC_LEVEL), он может запрашивать любой код пользовательского режима, работающий на IRQL = PASSIVE_LEVEL, и одной из его основных структур является _KAPC ( на самом деле эта структура является частью двусвязной структуры внутри структуры _KAPC_STATE, которая является частью структуры KTHREAD в ядре), которая должна быть выделена из памяти NonPagedPool. В конце концов, Kernel APC работает как прерывание, потому что это может произойти практически в любое время.

*PsSetLoadImageNotifyRoutine(): это хорошо известная процедура в Windows, и она регистрирует процедуру обратного вызова (предоставленную параметром NotifyRoutine в качестве указателя и имеющую тип PLOAD_IMAGE_NOTIFY_ROUTINE), которая будет уведомляться всякий раз, когда загружается образ. На самом деле, эта подпрограмма дополняется другими подобными подпрограммами, такими как PsSetCreateProcessNotifyRoutine (она работает аналогичным образом, но добавляет подпрограмму обратного вызова, которая будет вызываться всякий раз, когда процессы должны быть вызваны или завершены) и PsSetCreateThreadNotifyRoutine (тот же принцип работы, но связанный с созданием и прекращением потока). Про регистрацию обратного вызова для уведомления о создании и завершении процесса, интересно вспомнить и о PsSetCreateProcessNotifyRoutineEx и PsSetCreateProcessNotifyRoutineEx2. В качестве простого примера, драйверы Windows, такие как mssecflt.sys (драйвер фильтра файловой системы Microsoft Security Events Component), в котором за последние месяцы было несколько исправлений, используют PsSetCreateProcessNotifyRoutineEx, PsSetLoadImageNotifyRoutine, PsSetCreateThreadNotifyRoutine активно :

1687191635821.png


*KeRegisterBugCheckCallback(): эта процедура отвечает за регистрацию процедуры BugCheckCallback (KBUGCHECK_CALLBACK_ROUTINE), которая выполняется, когда Windows выполняет проверку на наличие ошибок.
Много лет назад я мог находить угрозы вредоносного ПО, используя этот обратный вызов, чтобы помешать инструментам цифровой криминалистики создать дамп образа памяти, а также помешать исследователям анализировать память.

*ObRegisterCallbacks(): эта подпрограмма является одной из самых интересных, поскольку она регистрирует список (предоставленный структурой OB_CALLBACK_REGISTRATION) подпрограмм обратного вызова для обработки потока, процесса и рабочего стола. Кроме того, существует процедура ObUnregisterCallbacks для отмены регистрации всех обратных вызовов. Помимо очевидного использования вредоносными программами (в том числе руткитами), я также видел его использование в анти-читах, и, конечно же, драйверы Microsoft, конечно же, также используют его. Например, в приведенном ниже фрагменте кода, который также взят из mssecflt.sys (это функция SecObAddCallback), читатели могут ясно увидеть вызов процедуры ObRegisterCallbacks, ее параметры и даже ссылку на настройку PreOperationCallback несколькими строками выше :

1687191644813.png


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

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


1687191696801.png


Оба расширения устарели, и не все команды работают должным образом в последних версиях Windows, но они по-прежнему полезны. В обоих случаях вы должны клонировать проект с помощью команды git clone и собирать их. Лично я всегда копирую свои расширения в соответствующую папку расширений WinDbg (в данном случае это C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\winext), но вы можете хранить расширения где угодно, а потом передача полного пути (без двойных кавычек и пробелов) при выполнении команды расширения !load. В любом случае, вы должны убедиться, что используете правильную версию WinDbg (x64) с правильным расширением. Простое выполнение получения обратных вызовов с использованием SwishDbgExt выглядит
следующим образом:

1687191745800.png


Конечно, читатели могли получить указанный список вручную. Например, получите список PsCreateProcessNotifyRoutines, выполнив следующую команду:

1687191755076.png


Мы заметили, что со всеми указанными выше адресами не связаны символы, но причина в том, что я тестировал команду в Windows Inside Preview, и у меня не было времени загрузить соответствующие символы. Повторяя ту же процедуру на Windows 11, мы имеем:

1687191764904.png


Другой способ получить тот же результат — выполнить следующую последовательность команд:

1687191774648.png


Как видите, сначала я получил количество функций обратного вызова, а затем сделал простой цикл для получения ответа. Конечно, читатели могут спросить, почему я использую PspCreateProcessNotifyRoutine (с дополнительной буквой «p» в имени), а не PsCreateProcessNotifyRoutine (имя функции, отвечающей за регистрацию процедур обратного вызова). Бывает, что PspCreateProcessNotifyRoutine (с лишней буквой «p» в имени) представляет собой массив, в котором хранится до 64 подпрограмм обратного вызова.

Если читатели хотят повторить процедуру с помощью wdbgark, то предлагаю следующие команды:
1687191796473.png


Вывод обширен, поэтому я не буду включать его сюда, но он понравится читателям, потому что он очень полный.
Наконец, если вы хотите протестировать, вы можете использовать Volatility для получения обратных вызовов из Windows. Чтобы установить Volatility 3 в Linux (моя среда — Ubuntu 22.10), выполните следующие шаги:

1687191805109.png


Получите память целевой системы, используя один из доступных:

1687191811810.png


Вы можете перечислить все включенные обратные вызовы. Поскольку вывод длинный, я использовал команду grep для фильтрации только одного типа обратного вызова, а также запускаю команду на другой Windows 11 с 4 ГБ (а не 64 ГБ), чтобы ускорить тест:

1687191824300.png


Имея адреса каждого обратного вызова, мы можем провести дальнейшее исследование. Читатели могут изучить другие обратные вызовы в зависимости от контекста.

Как я уже упоминал ранее, этот раздел является лишь кратким обзором, и в нем содержится больше подробностей о предмете, но, в конце концов, этого пока достаточно.
 
8. Реверсинг и платформа фильтрации Windows (WFP)

Как я уже описывал, программирование и обработка событий ядра — это другой подход, и, как и ожидалось, природа этих механизмов также различна, начиная с организации памяти, где на кучу ссылаются пулы ядра, а эти представлены разными характеристики. На самом деле, в последних версиях Windows 10 и 11 ядро использует Segment Heap вместо старой схемы пула, но концепции те же. Проверьте наличие следующих структур:

а. _EX_POOL_HEAP_MANAGER_STATE: https://www.vergiliusproject.com/ke...2H2 (2022 Update)/_EX_POOL_HEAP_MANAGER_STATE
б. _EX_HEAP_POOL_NODE: https://www.vergiliusproject.com/kernels/x64/Windows 11/22H2 (2022 Update)/_EX_HEAP_POOL_NODE).

Куча может быть NonPagedEx (не выгружаемая и не исполняемая), NonPaged (не выгружаемая), Paged, Session и Special, хотя здесь мы будем использовать первые три типа. Невыгружаемая куча (или пул) относится к страницам памяти, которые не могут быть отправлены (выгружены) на диск и, конечно, в случае выгружаемой кучи (или пула) такие страницы памяти могут быть отправлены на диск. Современные механизмы, такие как Segment Heap, также привносят другие различные концепции с точки зрения его организации, такие как Low Fragmentation Heap (используется для выделений менее 512 байт, и теперь любое распределение там полностью рандомизировано с точки зрения адреса местоположения), Variable Size (для выделений между 512 байт и 128 КБ), Backend (для выделений от 128 КБ до 512 КБ) и, наконец, Large Block (для выделений более 512 КБ).

К сожалению (для исследователей), многие средства защиты были введены или улучшены, и основными средствами защиты являются подпись кода в режиме ядра (KMCS), которая обеспечивается ci.dll и требует, чтобы любой загруженный драйвер был подписан, kASRL, целостность кода гипервизора (HVCI), которая основана на VBS и защищает ядро от эксплуатации, одновременно предотвращая привилегии исполняемого файла и записи (W ^ X) для выделения страницы в ядре, таким образом предотвращая выполнение любого вредоносного ПО и шелл-кода. Кроме того, любое выделение должно исходить от подписанного драйвера и поддерживаться безопасным ядром (работающим на VTL 1). В последние годы использование уязвимостей драйвера ядра стало сложнее. Без сомнения, эта тема невероятно привлекательна и могла бы заполнить десятки страниц, но этих вводных абзацев нам достаточно, и я рекомендую читателям искать подробности в книгах, статьях и страницах MSDN от Microsoft.

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

Без сомнения, все концепции, которые я упомянул в этой статье, важны, как и все упомянутые процедуры, которые почти наверняка читатели найдут, открыв ее в IDA Pro. Например, DriverEntry() — первый и очевидный выбор, поскольку он работает как подпрограмма для вызова других важных подпрограмм при определенных условиях. Однако я хочу прокомментировать и другие аспекты предмета, которые будут вам полезны.

Как мы узнали, приложения отправляют запросы другим драйверам, вызывая такие подпрограммы, как DeviceIoControl, используя элементы управления вводом-выводом устройства (которые также известны как IOCTL), что заставляет диспетчер ввода-вывода создавать и отправлять IRP.
Точно так же даже другие драйверы могут отправлять запросы целевому драйверу, используя известные функции, такие как IoCallDriver (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-iocalldriver) и IoBuildDeviceIoControlRequest (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm- iobuilddeviceiocontrolrequest), макрос и подпрограмма которого связаны с основным кодом IRP_MJ_INTERNAL_DEVICE_CONTROL. Поскольку у драйверов есть объект устройства с помощью процедуры IoCreateDevice (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-iocreatedevice), а ссылка на такой объект устройства и соответствующее имя устройства задаются символической ссылкой, созданной подпрограммой IoCreateSymbolicLink (https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf- wdm- iocreatesymboliclink).

Вероятно, читатели уже заметили, что на данный момент следующим наиболее важным фрагментом кода является инициализация подпрограмм диспетчеризации и, в частности, массива указателей на функции, который содержится в поле-члене MajorFunction, составляющем часть структуры _DRIVER_OBJECT. Как и ожидалось, существует несколько подпрограмм диспетчеризации, и иногда бывает трудно изучить их все, поэтому, возможно, хорошим подходом будет начать с наиболее часто используемой, такой как DispatchRead (код IRP_MJ_READ), DispatchWrite (код IRP_MJ_WRITE), DispatchCreate ( код IRP_MJ_CREATE) и DeviceIoControl| IoBuildDeviceIoControlRequest (коды IRP_MJ_DEVICE_CONTROL | IRP_MJ_INTERNAL_DEVICE_CONTROL). Последнее является следствием вызова DeviceIoControl IoCallDriver (упомянутые выше), и он отвечает за отправку управляющего кода (IOCTL) целевому драйверу. Таким образом, он становится для нас наиболее важным, поскольку показывает поток сообщений между приложением и драйвером или даже между текущим драйвером и другими вспомогательными драйверами. Хотя в заголовочных файлах SDK определен список кодов управления вводом-выводом, большинство этих кодов IOCTL являются частными и определяются драйверами, что может немного усложнить анализ. Без сомнения, изучение этих кодов управления вводом-выводом с помощью возможной задачи обратного проектирования действительно полезно для лучшего понимания драйвера ядра.

Если читателям нужен список стандартных и хорошо известных кодов управления вводом-выводом, некоторые из них доступны в Интернете: http://www.ioctls.net/

На данный момент у нас есть следующие ключевые моменты, которые следует учитывать в первый момент анализа драйвера:

* Поиск подпрограммы DriverEntry.
* Обратите внимание на основные процедуры, вызываемые из процедуры DriverEntry в качестве процедур обратного вызова для чтения, записи и отправки управляющих кодов в драйвер устройства.
*Поиск символической ссылки, связанной с объектом устройства.
* Поиск имени устройства (DeviceName).
*Анализ кодов управления вводом-выводом, объекта устройства и буферов, используемых такими подпрограммами, как DeviceIoControl и IoBuildDeviceIoControlRequest.

Конечно, эти предметы являются лишь отправной точкой. Если читателям интересно, как коды IOCTL используются с запросами IRP_MJ_DEVICE_CONTROL (созданными путем вызова DeviceIoControl() для связи между приложением пользовательского режима и драйвером ядра) или запросами IRP_MJ_INTERNAL_DEVICE_CONTROL (созданными с помощью вызова IoBuildDeviceIoControlRequest для связи между двумя драйверами ядра), здесь
представляет собой макрос, как показано ниже:

1687278851970.png


Определение IOCTL (это 32-битное значение) задается четырьмя компонентами:

*DeviceType: определяет тип устройства.
*FunctionCode: указывает на функцию, которую должен выполнить драйвер.
*TransferType: определяет, как данные будут передаваться между вызывающей стороной (приложение пользовательского режима или другой драйвер) и целевым драйвером, отвечающим за обработку IRP. Возможные значения: METHOD_BUFFERED, METHOD_IN_DIRECT или METHOD_OUT_DIRECT,METHOD_NEITHER.
*RequiredAccess: этот параметр определяет тип доступа, запрошенный вызывающей стороной для открытия файлового объекта, представляющего устройство. Возможные значения: FILE_ANY_ACCESS, FILE_READ_DATA, FILE_READ_DATA и FILE_WRITE_DATA.

Я думаю, что я уже представил достаточно концепций для этой и следующих статей.

Я не собираюсь анализировать вредоносный драйвер (руткит) в этой статье, но я проведу быстрый анализ одного известного образца под названием Netfilter (также известного как Retliften), который работает как троян (x64) и который на прошлое, было подписано (в то время) Microsoft по ошибке. Чтобы загрузить его с Malware Bazaar, выполните:

1687278867269.png


Если читатели хотят получить список и загрузить другие потенциальные вредоносные драйверы, эту задачу можно выполнить, выполнив следующую команду:

1687278889476.png


Следующий шаг — открыть его в IDA Pro и наблюдать несколько фактов.

После запуска IDA Pro и перед переходом к процедуре DriverEntry не забудьте несколько основных шагов:

* Принудительно декомпилируйте весь драйвер, выбрав «Файл» → «Создать файл» → «Создать файл C».
* Перейдите в «Правка» → «Плагины» → «Декомпилятор Hex-Rays» → «Параметры» и измените значение системы счисления по умолчанию на 16.
* Поскольку мы работаем с драйвером x64, откройте представление библиотек типов (SHIFT+F11) и добавьте (клавиша INSERT) две библиотеки: ntddk64_win10 и netapi64_win10.
* Откройте представление подписей (SHIFT+F5) и проверьте наличие следующих: ms64wdk, v64seh и vc64ucrt. Если их нет, добавьте их.
* Нажмите CTRL+E, чтобы перейти к точке входа (DriverEntry).

1687278927621.png


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

*DriverEntry: точка входа.
*DriverObject: переменная типа DRIVER_OBJECT, представляющая образ загруженного драйвера.
*DriverUnload: процедура, используемая для выгрузки драйвера.

Однако есть две подпрограммы, которые мы пока не комментируем:

*RtlCopyUnicodeString: как вы уже поняли, эта процедура копирует строку в целевой буфер. Помните, что RTL означает библиотеку реального времени.
*WdfVersionBind: эта процедура привязывает драйвер к определенной версии библиотеки WDF.

Я мог найти определение этой функции (а также WdfVersionUnbind) на https://github.com/microsoft/Windows-Driver- Frameworks/blob/main/src/framework/shared/inc/private/common/fxldr.h, который имеют следующие прототипы:

1687278966799.png


Читатели уже заметили, что есть два типа, о которых мы ничего не знаем, такие как PWDF_BIND_INFO и PWDF_COMPONENT_GLOBALS. Обычно я использовал два подхода к поиску этой информации:

*Клонирование репозитория (git clone https://github.com/microsoft/Windows-Driver-Framework) и рекурсивный поиск структур с помощью: findstr /S <string> *.
* Поиск определений структур на превосходных веб-сайтах, таких как https://github.com/winsiderss/systeminformer и https://doxygen.reactos.org/.

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

Если вы хотите улучшить определение WdfVersionBind в idb IDA (здесь это не особо необходимо), то необходимо будет добавить все определения структуры в локальные типы (SHIFT+F1):

1687278980904.png


Несколько записей будут созданы отдельно в представлении «Локальные типы», поэтому щелкните их правой кнопкой мыши и выберите параметр «Синхронизировать с idb».

1687278989803.png


В коде для этого конкретного случая не будет поразительного эффекта, но эта процедура по-прежнему полезна, чтобы объяснить читателям, как действовать в подобных случаях. В любом случае, перейдя к sub_140003C20 → sub_14000395C, читатели легко определят имя устройства, связанное с драйвером:

1687279005237.png


Перейдя к подпрограмме sub_140005284 (не показано на последнем изображении, а только три инструкции ниже), мы найдем следующее содержимое:

1687279013963.png


Из последней страницы мы узнали, что этот вредоносный драйвер с именем NET_FILTER, вероятно, контролирует (отслеживает или даже изменяет) поведение сетевой фильтрации через сетевое соединение. Хотя я не объяснял этот материал ранее, API для взаимодействия с сетевым стеком в Windows предлагает WFP (платформа фильтрации Windows). С точки зрения номенклатуры, архитектура WFP предлагает сетевой стек, состоящий из уровней (их около сотни, и каждый из них имеет связанный GUID), каждый уровень которого может состоять из нуля или более фильтров и нуля или более связанных драйверов вызовов , которые отвечают за выполнение путем обработки данных. Да, я знаю, что концепции здесь могут быть трудными для понимания, и, в конце концов, читатели к ним не привыкнут, поэтому краткое введение может быть полезным на этом этапе.

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

WFP (платформа фильтрации Windows) состоит из следующих крупных компонентов:

* Механизм фильтрации: компонент отвечает за выполнение задачи фильтрации, вызов выносок на основе классификации и, в конце концов, разрешает или запрещает определенный трафик.
* Базовый механизм фильтрации: этот компонент является макрокомпонентом в WFP и объединяет фильтры, отчеты, статистику, модель безопасности и конфигурацию.
*Shims: этот компонент представляет компоненты режима ядра, которые фактически принимают решение о фильтрации на основе классификации.
*Callout: этот компонент, как мы уже узнали, представляет собой функцию, которая эффективно разрешает, блокирует, модифицирует и даже повторно вводит сетевой трафик. Как и ожидалось, они должны быть зарегистрированы в слоях WFP.

Короче говоря, мы можем прямо или косвенно взаимодействовать с несколькими компонентами и подкомпонентами WFP, такими как:

*Фильтры: они участвуют в классификации, затем их можно интерпретировать как правила для приема или блокировки сетевого трафика. Фильтры организованы в подслои, и порядок задается весом, который подобен высоте для драйверов минифильтра.
* Слои: они работают как организация фильтра внутри механизма фильтрации и не могут быть удалены.
*Подуровни: они являются частью слоев и обычно обрабатывают исключения в правилах или конкретном сценарии. Их можно добавлять или удалять, и есть набор подслоев, которые наследуются слоями.
*Callout: это набор функций, активно участвующих в процессе классификации как разрешающих или блокирующих сетевые данные. Callout могут быть добавлены или удалены.
*Shims: это компонент режима ядра, который отвечает за принятие классификационных решений по фильтрам определенного уровня. Другими словами, компонент shim начинает классификацию, которая состоит из применения фильтров, чтобы в конце решить, следует ли блокировать или разрешать сетевой трафик.

Последовательность компонентов, участвующих в обработке, следующая: network packet → network stack → shim →
filters (from a layer) → callouts → shims (actually performing and following the filtering decision). Решения могут быть упрощены как разрешающие (FWPM_ACTION0.type=разрешить) или блокирующие (FWPM_ACTION0.type=блокировать), но есть несколько нюансов:

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

Возвращаясь к коду, читатели видят ряд вызываемых функций, и в нескольких словах их значение следующее:

*FwpmEngineOpen0: он открывает сеанс для механизма фильтрации и, как и ожидалось, возвращает ему дескриптор.
*FwpmTransactionBegin0: запускает транзакцию с текущим сеансом и для выполнения этой задачи использует дескриптор открытого сеанса, возвращаемый подпрограммой FwpmEngineOpen0.
*В подпрограмме sub_140004F2C у нас есть функция FwpsCalloutRegister1, которая отвечает за регистрацию выноски. Эта функция получает указатель на объект устройства, указатель на структуру выноски (с типом FWPS_CALLOUT1_) и возвращает calloutId, который используется для идентификации выноски в механизме фильтрации. Подпрограмма sub_140004F2C, функция FwpsCalloutRegister1 и структура FWPS_CALLOUT1_ показаны ниже:

1687279060009.png


1687279067162.png


Интерпретация элементов структуры (FWPS_CALLOUT1_) прямая:

* первый элемент (calloutKey) содержит идентификатор GUID (0BABE0A0B870EFD9A4854F0780CF72951h);
* второй член представляет флаги (ноль);
* третий элемент (classifyFn) содержит указатель на функцию, которая работает как уведомление (триггер) для вызова вызова всякий раз, когда есть сетевые данные;
* четвертый элемент (notifyFn) — это указатель на функцию, которая будет вызываться при добавлении или удалении любого фильтра, использующего эту выноску, а также при возникновении связанных с выноской событий.
* пятый параметр (flowDeleteFn) содержит указатель на функцию, которая будет вызываться, когда поток данных, обрабатываемый вызовом, завершится.

sub_140004FB8 пока самая важная процедура:

1687279084939.png


Как показано в коде, вызываются три ключевые подпрограммы:

*FwpmCalloutAdd0: эта подпрограмма отвечает за добавление новой выноски в систему, и ее прототипом является DWORD FwpmCalloutAdd0([in] HANDLE engineHandle, const FWPM_CALLOUT0 *callout, PSECURITY_DESCRIPTOR sd, [out, optional] UINT32 *id). Первый параметр — это дескриптор открытого сеанса для механизма фильтрации, второй параметр — это указатель на объект выноски (структура FWPM_CALLOUT0), а последний параметр представляет выходные данные, которые являются идентификатором времени выполнения.
1687279096610.png


*FwpmSubLayerAdd0: эта процедура добавляет в систему подуровень, и ее прототипом является DWORD FwpmSubLayerAdd0([in] HANDLE engineHandle, [in] const FWPM_SUBLAYER0 *subLayer, [in, необязательно] PSECURITY_DESCRIPTOR sd). Второй аргумент представляет добавляемый подуровень.

1687279105838.png


*FwpmFilterAdd0: эта подпрограмма добавляет в систему новый объект фильтра, и ее прототипом является DWORD FwpmFilterAdd0([in] HANDLEengineHandle, [in] const FWPM_FILTER0 *filter, [in, необязательный] PSECURITY_DESCRIPTOR sd, [out, необязательный] UINT64 *id ), второй параметр которого является указателем на добавляемый объект фильтра, а четвертый параметр, аналогичный FwpmCalloutAdd0, представляет выходные данные в виде идентификатора среды выполнения.

Строка 7 на последнем рисунке имеет ссылку на xmmword_140007680. На самом деле, если мы будем следовать этой ссылке на данные, мы увидим большое шестнадцатеричное число. Нажав «горячую клавишу U» (или даже «горячую клавишу A»), мы увидим юникодную строку, но без соответствующего представления (на самом деле горячие клавиши U или A нажимать не обязательно, и я показываю это, чтобы доказать, что это строка Юникода). Выделите все строки, содержащие символы, и перейдите в «Правка» → «Строки» → «Юникод», и появится строка «redirectCalloutV4». В этой подпрограмме псевдокод использует и другие строки Unicode, поэтому читатели могут повторить для них тот же подход. После обработки строк и переименования переменных у нас есть следующий псевдокод:

1687279121575.png


1687279129598.png


Из псевдокода имеем следующее:

* callout отображается как «redirectCalloutV4».
* Описание callout: «callout IPv4 для перенаправления».
* Помните, что объект callout представлен структурой FWPM_CALLOUT0.
* Поле displayData из структуры FWPM_CALLOUT0_ представлено структурой FWPM_DISPLAY_DATA0, состоящей из указателей wchar_t, которые являются полями имени и описания (проверьте строки 11 и 12).
* В строке 6 флаги (из структуры FWPM_CALLOUT0_) равны нулю, но это могут быть значения FWPM_CALLOUT_FLAG_PERSISTENT (0x00010000), FWPM_CALLOUT_FLAG_PERSISTENT (0x00020000) и FWPM_CALLOUT_FLAG_REGISTERED (0x00040000).
* CalloutKey идентифицирует сеанс, а applyLayer указывает, на каком уровне будет использоваться такаой callout, поэтому это поле указывает, что только фильтры из этого предоставленного слоя могут вызывать callout.
* Описание подуровня — «Подуровень для перенаправления», а его отображаемое имя — «перенаправление для подуровня» (строки 27 и 30).
*Подуровень, с которым связана структура FWPM_SUBLAYER0, также идентифицируется по GUID в ключе subLayerKey. Конечно, есть список встроенных подслоев, но в этом конкретном случае есть предоставленный ключ (проверьте строку 24). Если мы будем следовать ключевой ссылке, мы найдем следующую информацию:

1687279149933.png


*Для форматирования этого GUID я использовал следующий простой сценарий IDC:

1687279156371.png


*В командной строке IDA Pro запустите этот макрос, подтверждающий адрес начала GUID: Guid(0x00000001400084F8) == {AE1E820A-C60A-42A8-B4A2-9ACFB050387F}.
*Вес подуровня равен 0xFFFF (строка 29), что означает, что он вызывается первым.
*Количество условий фильтрации (numFilterConditions) равно нулю.
Таким образом, нет никакого установленного условия для вызова фильтра.
*Название фильтра на дисплее — redirectFilterV4, а его соответствующее описание — «Фильтр IPv4 для перенаправления» (строки 42 и 45).
* Тип действия фильтра — FWP_ACTION_CALLOUT_TERMINATING, что в основном заставляет вызывать вызов, который всегда возвращает блокировку или разрешение. Чтобы показать это строковое представление, я искал макрос (горячая клавиша M).
* FWPM_FILTER0_.weight.type, равный FWP_UINT64 (строка 48), означает, что механизм базовой фильтрации будет использовать предоставленное значение в качестве веса, то есть 0xFFFFFFFFFFFFFFFF (строки 37 и 50).
* В строке 53 calloutKey — это GUID для callout, допустимой в слое (строка 16), а layerKey (строка 64) содержит GUID, на котором размещен фильтр, и он соответствует строке 17.
* В строке 55, наконец, код добавляет объект фильтра в систему, вызывая подпрограмму FwpmFilterAdd0, которая использовала объект фильтра, созданный в предыдущих строках.

Читатели уже заметили, что WFP — это, по сути, набор ловушек внутри сетевого стека, а также механизм фильтрации, которые позволяют нам взаимодействовать, отслеживать и, в конечном итоге, контролировать информацию о сетевых данных. Кстати, если вас интересует значение FWPM, это Filtering Windows Platform Management, что является подходящим названием для фреймворка. Поэтому, по-видимому, вредоносное ПО добавляет новый подуровень, фильтр и связанный с ним вызов для обработки связи IPv4, который в данном случае работает как перенаправитель IPv4 на другой IP-адрес, но делать выводы рано. Мы также упомянули «произвольный GUID», и здесь нет ничего нового, потому что, поскольку callout является общим драйвером ядра, Visual Studio может сгенерировать любой GUID, и, вероятно, это сделал автор вредоносного ПО.

Я намеренно кратко прокомментировал подпрограмму sub_140004F2C (рис. 82), но мы должны помнить, что именно эта подпрограмма отвечает за регистрацию выноски в механизме фильтрации. Кроме того, его члены, такие как classifyFn (указывает на функцию, которая будет вызываться при наличии данных для обработки) и notifyFn (указывает на функцию, которая вызывается при завершении обрабатываемого потока данных) из структуры FWPS_CALLOUT1_, имеют значение.

1687279214448.png


Этот обратный вызов имеет следующие параметры:

*inFixedValues: содержит указатель на структуру FWPS_INCOMING_VALUES0, которая содержит значения для каждого из полей данных в фильтруемом слое.
*inMetaValues: содержит указатель на структуру FWPS_INCOMING_METADATA_VALUES0, которая содержит значения каждого поля метаданных в фильтруемом слое.
*layerData: содержит указатель на структуру, описывающую фильтруемые данные.
*classifyContext: содержит указатель на данные контекста.
*filter: содержит указатель на структуру FWPS_FILTER1.
*flowContext: содержит контекст, связанный с потоком данных.
*classifyOut: это указатель на структуру FWPS_CLASSIFY_OUT0, которая получает результат, возвращаемый функцией classifyFn1 вызывающей стороне.

Из рисунка 82 мы знаем, что:

*sub_1400053A0 — это callout classifyFn.
*sub_140005520 — это callout notifyFn.

Перемещаясь внутрь подпрограммы sub_1400053A0 (callout classifyFn), мы, к сожалению, не увидим
дружественного аспекта (см. рис. 93). Таким образом, я выполнил следующие шаги:

*Я переименовал (клавиша N) все его параметры в соответствии с прототипом, описанным на https://learn.microsoft.com/en-us/w.../ddi/fwpsk/nc-fwpsk-fwps_callout_classify_fn1.
*Я добавил (SHIFT + F9 → INS → Добавить стандартную структуру) все недостающие структуры:FWPS_INCOMING_VALUES0, FWPS_INCOMING_METADATA_VALUES0, FWPS_FILTER1, FWPS_CLASSIFY_OUT0_ и FWPS_INCOMING_METADATA_VALUES0_.
*Я изменил тип всех аргументов (горячая клавиша Y) в соответствии с сигнатурой функции.
*Я переименовал переменные поверх кода и применил два макроса (горячая клавиша M).
Результат на Рисунке 94 далек от совершенства, но уже можно иметь представление получше:

1687279234521.png



Анализируя полученную функцию, мы можем сделать следующие наблюдения:

* Структура FWPS_CALLOUT_ (как показано на рис. 86 и применено на рис. 82), которая используется и связана с подпрограммой FwpsCalloutRegister, была нашей отправной точкой для достижения этой точки анализа, поскольку она включает три соответствующих выноски, такие как classifyFn, notitfyFN и flowDeleteFn, и в данный момент мы анализируем classifyFn. Маршрут до этой точки: sub_140004F2C → sub_1400053A0.
*Поэтому в строке 18 (Рисунок 94) поле layerId, определяющее уровень фильтрации времени выполнения, тестируется и проверяется на соответствие FWPS_LAYER_ALE_CONNECT_REDIRECT_V4 (TCP-трафик – компонент отправитель|клиент). Этот уровень фильтрации допускает любую модификацию удаленного адреса и порта исходящих соединений, поэтому он связан с перенаправлением.
* Строка «ALE» означает Application Later Enforcement и, как и ожидалось, состоит из нескольких слоев фильтрации.
*Иногда читатели найдут типы данных FWPM (Фильтрация управления платформой Windows), которые связаны с задачами управления (callout и добавление фильтров), а иногда увидят данные FWPS, которые связаны с типами данных выноски (фактическая фильтрация). Аналоги есть с обеих сторон, хотя типы данных FWPS обычно меньше, чем типы данных FWPM. По этой причине мы видим, что поле layerId сравнивается с FWPS_LAYER_ALE_CONNECT_REDIRECT_V4 (0x42 — представлено 16 битами), в то время как для слоев фильтрации FWPM эти идентификаторы GUID имеют 16 байтов. Кроме того, есть и другие тонкие различия, которые не будут здесь комментироваться.
* В строке 24, если layerId не равен FWPS_LAYER_ALE_CONNECT_REDIRECT_V4, решение FWP_ACTION_PERMIT (загружается в поле actionType), что означает, что сетевой фильтр разрешает передачу или получение сетевых данных. Было бы полезно знать, что classifyOut, который является членом выноски FwpsCalloutClassifyFn1, является указателем на структуру FWPS_CLASSIFY_OUT0 и получает решение, возвращаемое функцией callout classifyFn. Возможные значения: FWP_ACTION_PERMIT (наш случай), FWP_ACTION_BLOCK и FWP_ACTION_CONTINUE. FWP_ACTION_NONE. Таким образом, в конце концов, окончательное решение принимает функция вызова classifyFn.
*Подпрограмма FwpsAcquireClassifyHandle0 отвечает за создание дескриптора классификации, который будет использоваться для асинхронной классификации и, что наиболее важно, для модификации данных в других функциях, таких как функции FwpsApplyModifiedLayerData0, FwpsAcquireWritableLayerDataPointer0, FwpsAcquireWritableLayerDataPointer0 и FwpsReleaseClassifyHandle0. Все эти подпрограммы присутствуют в подпрограмме sub_140005524 (строка 74).

Прежде чем продолжить, помните: FWPM относится к объектам пользовательского режима WFP, идентифицируемым с помощью GUID, а FPWS относится к объектам режима ядра WFP, идентифицируемым с помощью LUID (локально уникальный идентификатор). И снова потоки выполнения переходят к другой подпрограмме, sub_140005524, которая состоит из серии вызовов, прямо или косвенно связанных с callout. Как обычно, интересно показать код перед какой-либо обработкой, как показано на следующей странице:

1687279270308.png


Вызывается несколько подпрограмм WFP, поэтому краткая информация о них следующая:

*FwpsAcquireWritableLayerDataPointer0: эта функция возвращает данные, относящиеся к слою, которые можно проверить или даже изменить. Второй параметр (filterId) аналогичен параметру фильтра подпрограммы classifyFn, а его внутренняя организация задается структурой FWPS_FILTER1_, которая среди других полей устанавливает subLayerWeight, numFilterConditions, action и filterCondition.
*FwpsReleaseClassifyHandle0: эта процедура освобождает ранее полученный дескриптор классификации путем FwpsAcquireClassifyHandle0 (см. стр. 87).
*FwpsApplyModifiedLayerData0: эта функция применяет изменения, произведенные подпрограммой FwpsAcquireWritableLayerDataPointer0.
*FwpsCompleteClassify0: эта процедура завершает ожидающий запрос на классификацию.

Таким образом, после выполнения быстрого анализа и небольшого реверсирования, улучшенная версия
sub_140005524 выглядит ниже:

1687279303884.png


Без сомнения, представление кода лучше, чем в оригинальной версии, и я сделал следующее:

* Я переименовал a1 в arg_1 и a2 в arg_2 (горячая клавиша N).
*Поскольку arg_1 явно был структурой, я создал ее, щелкнув ее правой кнопкой мыши и выбрав Создать новый тип структуры.
* Я использовал прототип подпрограммы FwpsAcquireWritableLayerDataPointer0 для переименования аргументов.
* Я применил макросы, такие как FWP_ACTION_PERMIT и FWPS_RIGHT_ACTION_WRITE. Наличие права FWPS_RIGHT_ACTION_WRITE позволяет драйверу выноски записывать элемент actionType этой структуры и изменять его по назначению. Если бы здесь этого не было, он мог бы написать в actionType, если ему нужно было заблокировать предыдущее решение FWP_ACTION_PERMIT, принятое фильтром с большим весом (помните: вес представляет то же самое понятие высоты в драйверах мини-фильтра).
* Я добавил перечисление MACRO_FWPS, чтобы иметь возможность применять FWPS_CLASSIFY_FLAG_REAUTHORIZE_IF_MODIFIED_BY_OTHERS. Для этого была необходима информация, предоставленная FwpsApplyModifiedLayerData0 в MSDN о его прототипе (https://learn.microsoft.com/en-us/w...di/fwpsk/nf-fwpsk-fwpsapplymodifiedlayerdata0)
* Прототип FwpsAcquireWritableLayerDataPointer0 (https://learn.microsoft.com/en-us/w...nf-fwpsk-fwpsacquirewritablelayerdatapointer0) предоставил еще один полезный совет. При описании writableLayerData, который является выходным аргументом, в описании говорится, что это пустой указатель, который позже будет приведен к соответствующему типу структуры. Однако в разделе «Примечания» MSDN сообщает нам, что может быть только две возможные структуры: FWPS_BIND_REQUEST0 и FWPS_CONNECT_REQUEST0. Изучив их, стало ясно, что код относится ко второму, потому что «определяет изменяемые данные для слоев FWPM_LAYER_ALE_AUTH_CONNECT_REDIRECT_V4 и FWPM_LAYER_ALE_AUTH_CONNEC T_REDIRECT_V6». (проверьте: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/fwpsk/ns-fwpsk-_fwps_connect_request0). То же самое относится и к writableLayerData_1, потому что они одинаковы.
*В структуре _FWPS_CONNECT_REQUEST0 мало интересных полей, но первые два из них на данный момент более привлекательны. Поскольку они относятся к типу SOCKADDR_STORAGE, я изменил их типы (горячая клавиша Y) на sockaddr_in, основываясь на своем предыдущем опыте. Как только поля стали более понятными, я просто переименовал другие поля arg_1 в соответствии с контекстом.
* Я добавил как минимум одно перечисление, начинающееся с «AF_», «SOCK_» и «IPPROTO» (помните: добавление одного значения перечисления заставляет IDA Pro вставить все связанное перечисление), перейдя на вкладку «Перечисление», нажав клавишу INS и выбрав «Добавить». стандартное перечисление по имени символа. Впоследствии я использовал эти значения для применения отсутствующих макросов.
* Другие переменные также были переименованы (горячая клавиша N) в соответствии с контекстом.

Конечно, улучшение предыдущего кода может показаться трудным, но как только читатели смогут понять мои объяснения выше, процесс станет проще, чем ожидалось. До сих пор пути нашего анализа были следующими:

*sub_14000395C → sub_140005284 → sub_140004F2C → sub_1400053A0 → sub_140005524
*sub_14000395C → sub_140005284 → sub_140004FB8

Возвращаясь к sub_140005284, у нас есть оставшиеся функции:

*FwpmTransactionCommit0: эта функция фиксирует открытую транзакцию.
*FwpsCalloutUnregisterById0: эта функция отменяет регистрацию выноски.

Теперь мы можем вернуться к подпрограмме sub_14000395C (рис. 80) и попытаться сделать выводы и получить дополнительные сведения из других подпрограмм, которые мы оставили позади. Важно подчеркнуть, что я сосредоточился только на небольшой части кода, связанной с объектом устройства и платформой фильтрации Windows (WFP), чтобы объяснить новые концепции, а не из-за самого вредоносного драйвера.
Вся подпрограмма sub_14000395C показана ниже:

1687279334729.png


Перемещаясь внутри подпрограммы sub_140002BBC → sub_1400031F8, мы находим следующий код:

1687279342701.png
 
Структура WSK_CLIENT_NPI используется при реализации интерфейса сетевого программирования (NPI). В двух словах, NPI определяет интерфейс между сетевыми модулями, который реализует функцию в сетевом стеке, который может быть присоединен и интегрирован друг с другом. Таким образом, структура WSK_CLIENT_NPI описывается и определяется, как показано ниже:

1687279393798.png


Член ClientContext — это указатель на контекст привязки приложения WSK (Winsock Kernel), а член Dispatch — это указатель на другую структуру с именем WSK_CLIENT_DISPATCH, которая предоставляет таблицу диспетчеризации для функций обратного вызова, связанных с событиями, не связанными с конкретным сокетом, и это будет доступно для вызова при необходимости. Его состав определяется следующим образом:

1687279400639.png


Его членами являются:

*Версия: указывает версию WSK NPI.
*Зарезервировано: должно быть равно нулю.
*WskClientEvent: указатель на функцию обратного вызова события WskClientEvent, которая будет уведомлять приложение WSK о событиях, не связанных с конкретным сокетом.

Функция обратного вызова WskClientEvent определяется как тип PFN_WSK_CLIENT_EVENT, как показано ниже:
1687279412406.png


Аргумент ClientContext является указателем на значение контекста, поступающее из подпрограммы WskRegister; Аргумент EventType будет конкретным событием для уведомления приложения WSK; Информационный аргумент, который используется для передачи дополнительной информации приложению WSK, в большинстве случаев имеет значение NULL; Параметр InformationLength предоставляет размер информации. Таким образом, возвращаясь к подпрограмме sub_1400031F8, мы видим две вызываемые подпрограммы: WskRegister() и WskCaptureProvideNPI().

Подпрограмма WskRegister регистрирует приложение WSK, которое предоставляется и реализуется приложением WSK (WskClientNpi), и указателем на ячейку памяти, идентифицирующую регистрационный экземпляр приложения WSK (WskRegistration), которое фактически инициализируется подпрограммой WskRegister в результате его обработки. После успешного возврата вызывается подпрограмма WskCaptureProviderNPI, которая в данном случае выполняется на уровне IRQL <= DISPATCH LEVEL, поскольку ее второй аргумент равен 0xFFFFFFFF (WSK_INFINITE_WAIT), и захватывает NPI поставщика, когда он становится доступным. Первый параметр (WskRegistration) был инициализирован подпрограммой WskRegister, а третий параметр содержит указатель на таблицу диспетчеризации провайдера WSK, которая обеспечивает обратные вызовы, которые приложение WSK сможет вызывать.

Вернитесь к подпрограмме sub_14000395C, пришло время быстро изучить подпрограмму sub_140004A10, как показано ниже:

1687279426123.png


Ранее в этой статье я говорил об API CmRegisterCallbackEx(), который отвечает за регистрацию подпрограммы, которая будет использоваться ядром и драйверами фильтра для мониторинга и, в конечном счете, изменения любых операций с реестром, таких как переименование, перечисление, удаление ключей, создание и так далее. Теперь у нас есть реальный пример, используемый здесь, и, как мы уже узнали, первый параметр — это функция обратного вызова (в данном случае заданная функцией), второй параметр — высота (320000, как читатели могут видеть в строке 8). , указатель на структуру DRIVER_OBJECT и ссылку Cookie, которая является указателем на структуру LARGE_INTEGER, которая получает определенное значение, идентифицирующее процедуру обратного вызова.

Я не буду показывать содержимое обратного вызова Function (предоставленного в качестве первого аргумента API CmRegisterCallbackEx()), но самая интересная информация — это два вызова процедуры CmCallbackGetKeyObjectID, которая извлекает идентификатор и соответствующее имя объекта, связанного с предоставленным объектом ключа реестра. Обратите внимание, что вторым параметром подпрограммы CmCallbackGetKeyObjectID является именно указатель, который подпрограмма RegistryCallback драйвера получает как ссылку на структуру REG_XXX_KEY_INFORMATION.

Возвращаясь еще раз к подпрограмме sub_14000395C, есть еще две подпрограммы, в которых есть что-то полезное. Первая — это подпрограмма sub_140006548, у которой вызывается только одна функция — PsCreateSystemThread(), которая создает системный поток, как показано ниже:

1687279437353.png


Наиболее важным параметром здесь является StartRoutine (шестой параметр), который является указателем на выполняемую процедуру (обратный вызов KSTART_ROUTINE). Мы видим, что это второй аргумент подпрограммы sub_140006548, и, согласно рисунку 97 (строка 33), это подпрограмма sub_140003A70, показанная ниже:

1687279447006.png


Код начинает вызывать подпрограмму KeEnterCriticalRegion в строке 05, которая отключает выполнение обычных APC ядра. Это обычное действие, когда ожидается, что угроза выполнит операцию ввода-вывода. APC ядра будут повторно включены только тогда, когда код вызовет KeLeaveCriticalRegion() в строке 34.

В строке 06 вызывается подпрограмма KeSetBasePriorityThread для установки приоритета времени выполнения текущей угрозы путем добавления 5 к базовому приоритету процесса, удерживающего поток.

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

Подпрограмма sub_140005678, которая вызывается пять раз с разными аргументами, имеет в качестве основного содержимого выделение невыгружаемого пула с помощью процедуры ExAllocatePoolWithTag (перейдите к sub_140005678 → sub_1400044FC). Тег, используемый подпрограммой ExAllocatePoolWithTag, — «TLXE». Конечно, мы уже знаем, что эта подпрограмма устарела и заменена на ExAllocatePool2(), но авторы вредоносных программ продолжают ее использовать. Кроме того, подпрограмма sub_140005678 получает указатель функции в качестве первого аргумента, и, как уже упоминалось, при каждом вызове ей предоставляется одна функция.

Подпрограмма sub_1400069A4 (sub_140003BF0 → sub_140004A7C → sub_140004B5C → sub_1400069A4) имеет интересные вызовы функций, как показано ниже:

1687279465308.png


В строке 11 вызывается sub_140006B74 со следующим кодом:

1687279473356.png


В коде подпрограммы sub_140006B74 подпрограмма sub_140006B2C вызывается в строке 13:

1687279480497.png


Мы должны провести анализ в обратном порядке, чтобы получить общее представление о коде. Подпрограмма sub_140006B2C (рис. 107) вызывается с ProcessID == 4 (проверьте строку 9 в подпрограмме sub_140006B74), которая знает, что это системный процесс. Внутри подпрограммы sub_140006B2C эти процессы ищутся функцией PsLookProcessByProcessId, и возвращается дескриптор структуры EPROCESS предоставленного процесса. С помощью этого дескриптора вызывается функция PsGetProcessImageFileName и возвращается указатель на файл образа (исполняемый файл), резервирующий процесс на диске. Наконец, вызывается функция ObDereferenceObject для уменьшения счетчика ссылок на структуру EPROCESS, и в конце подпрограммы тот же самый указатель на файл изображения возвращается подпрограмме sub_140006B74.

Возвращаясь к подпрограмме sub_140006B74, существует условие while(true), которое анализирует каждый процесс до заданного ограничения PID (0x10000) и ищет первое вхождение строки «explorer.exe». Как только он найден, он возвращается через вызов функции PsLookProcessByProcessId указателя на соответствующую структуру EPROCESS.

Теперь, переходя к подпрограмме sub_1400069A4 (рис. 105), которая вызывает подпрограмму sub_140006B74, мы знаем, что функция ObOpenObjectByPointer открывает объект, на который ссылается возвращенный указатель из подпрограммы sub_140006B74, и возвращает указатель на объект. Другими словами, он возвращает указатель на процесс, представленный структурой EPROCESS, в данном случае это explorer.exe. Обратите внимание на строку 20, которая подтверждает нашу интерпретацию того, что это указатель на процесс, потому что пятый параметр (ObjectType) — это именно PsProcessType, а AccessMode, заданный шестым параметром, — это KernelMode (ноль).

Имея дескриптор этого процесса, он открывается функцией ZwOpenProcessTokenEx, которая возвращает соответствующий TokenHandle в свой пятый параметр. В следующей строке ExAllocatePoolWithTag вызывается для выделения PagedPool (чтобы его содержимое можно было выгрузить) с тегом «WENE» и размером 0x1000 байт, а действительность этого выделенного пула проверяется вызовом функции MmIsAddressValid (хотя Microsoft не рекомендуем использовать эту функцию).

В строке 41 вызывается NtQueryInformationToken для получения информации о предоставленном токене доступа (первый параметр: TokenHandle), а второй параметр равен TokenUser, который представляет собой значение TOKEN_INFORMATION_CLASS, определяющее, что выделенный буфер получает структуру TOKEN_USER с учетной записью пользователя token , третий параметр — это указатель на выделенный выгружаемый пул, четвертый параметр указывает размер TokenInformationBuffer (0x1000) и, наконец, последний параметр (ReturnLength) — длину возвращаемой информации.

В конце структура SID_AND_ATTRIBUTES, которая является единственным членом структуры TOKEN_USER и представляет пользователя, связанного с токеном доступа, используется в качестве аргумента функции RtlConvertSidToUnicodeString (строка 53) для преобразования его в строковое представление Unicode SID. Другими словами, у нас есть SID учетной записи, связанной с процессом explorer.exe, который возвращается в структуре UNICODE_STRING:

1687279658284.png


Возвращаясь к подпрограмме sub_140004CB8 (sub_140003BF0 → sub_140003BF0 → sub_140004CB8), есть вызов подпрограммы sub_140006684, которая в основном обрабатывает ACL, ACE и права собственности, связанные с SID.

Подпрограмма sub_140006C90 (sub_140003BF0 → sub_140004A7C → sub_140006C90) очень похожа на sub_140005678,
использующую функцию ExAllocatePoolWithTag, но она выделяет выгружаемый пул вместо невыгружаемого пула, а тег отличается: «WENE». В этой же процедуре есть другие манипуляции с ключами реестра, включающие структуру OBJECT_ATTRIBUTES.

Читатели могут легко понять, что следующие подпрограммы обрабатывают конфигурацию ключа реестра, связанную с доступом в Интернет (прокси), а также манипулированием SID/ACL (в этих конкретных случаях это происходит в подпрограммах внутри следующих):
1687279674753.png


Несколько записей реестра, которые манипулируются:

1687279682782.png


Удивительно, но мы только что закончили рассмотрение одной (sub_140003BF0) из пяти подпрограмм, относящихся к подпрограмме sub_140005678 (рис. 104), внутри подпрограммы sub_140003A70. Следующие две процедуры, sub_140003C10 и sub_140003B90, проще и аналогичны sub_140003BF0 и выделяют пул памяти, управляют строками и ключами реестра.

Две другие подпрограммы (sub_140003B80 и sub_140003BD0) более интересны, но они вызывают несколько других подпрограмм, и наш анализ превратился бы в бесконечную процедуру. Конечно, читатели могут заинтересоваться их анализом, потому что, например, есть подпрограммы, взаимодействующие с IO_STACK_LOCATION и Completion Routines.

Мы не можем игнорировать четкую ссылку на прокси в строке 19 (рис. 104), предполагающую перенаправление сети через конфигурацию прокси: http://110.42.4.180:2080/u. Кроме того, читатели могут заинтересоваться обработкой хранилища сертификатов внутри подпрограммы sub_140005D5C («\\Registry\\Machine\\SOFTWARE\\Microsoft\\SystemCertificates\\ROOT\\Certificates\\»). Наконец, если мы вернули уровень выше sub_14000395C (рис. 97), мы обнаружим несколько процедур, отменяющих и освобождающих все: освобождение пулов, отмена регистрации обратных вызовов (подпрограмма CmUnRegisterCallback), освобождение экземпляра регистрации приложения WSK, освобождение интерфейса сетевого программирования (NPI), удаление объекта фильтра, удаление выноски и, в конце, закрытие сеанса для механизма фильтрации.

В любом случае, я уже сказал, что это будет лишь краткий обзор нескольких фрагментов кода этого вредоносного двоичного файла, но после анализа этих нескольких подпрограмм вредоносные драйверы, по-видимому, пытаются открыть своего рода исключение в правиле фильтрации и перенаправления сети. данные на определенный удаленный адрес и IP-порт. Фактически, его глобальный план состоит в том, чтобы выполнить эту задачу на стороне ядра и пользовательского режима.
Конечно, читатели могут самостоятельно продолжить изучение других подпрограмм.

9. Дополнительные сведения о движении задним ходом водителя


Анализ драйверов требует значительных усилий, поскольку они могут содержать несколько подпрограмм, и, как и ожидалось, это требует времени. Несомненно, при анализе системного драйвера в Windows у нас есть предложенный Microsoft публичные символы и имена функций уже указаны. Целью здесь является не анализ драйвера, а только взаимодействие с первыми процедурами, чтобы показать, что все, что мы узнали до сих пор в этой статье, присутствует, и читатели могут двигаться вперед самостоятельно без каких-либо серьезных проблем.

Я взял драйвер srv2.sys, который является драйвером сервера Smb2.0 (сетевой драйвер), который очень часто обновлялся в последние месяцы из-за проблем с безопасностью. Открыв его в IDA Pro и выполнив полную декомпиляцию (Файл → Создать файл → Создать файл C), подпрограмма, показанная в качестве точки входа, будет GsDriverEntry, которая автоматически генерируется при компиляции драйвера и инициализации файла cookie безопасности, вызывает DriverEntry:

1687279705640.png


Войдя внутрь DriverEntry(), мы имеем следующее:

1687279713978.png


В приведенной выше подпрограмме DriverEntry нет ничего действительно нового, но ниже приведены соображения:

* В строках с 11 по 30 драйвер обрабатывает аспекты WPP (препроцессор трассировки программного обеспечения Windows), чтобы установить трассировку (возможность ведения журнала, аналогичную службам регистрации событий Windows) операции, что действительно полезно во время сеансов отладки и, кроме того, он предлагает возможность публиковать события в ETW (отслеживание событий для Windows). Эта часть драйвера нас не интересует, поэтому мы можем ее пропустить.
* Начиная со строки 31 переменные были переименованы.
* Макросы (горячая клавиша M) были применены к подпрограмме IoCreateDevice, а также к основным функциям в строках 65–69.
* Объект устройства (сетевое устройство) был создан подпрограммой IoCreateDevice, и его имя — \Device\Srv2.
*Вызывается функция IoGetCurrentProcess, которая возвращает указатель на текущий процесс.
* Таблица диспетчеризации DriverObject содержит указатели на четыре процедуры диспетчеризации: очистку (Srv2Cleanup), закрытие (Srv2Close), создание (Srv2Create) и управление устройством (Srv2DeviceControl).
* Как обычно и рекомендуется, существует процедура DriverUnload для выгрузки драйвера.

Мы могли бы изучить драйверы и, как обычно, подпрограмма диспетчеризации DispatchDeviceControl (Srv2DeviceControl) всегда является хорошей отправной точкой. Я не буду делать это здесь, потому что целью статьи не является анализ какого-либо конкретного драйвера ядра или файловой системы, а помощь читателям узнать о них и соответствующих методах, используемых в процедуре.

К сожалению, при реверсе драйверов, у которых нет их символов, задача усложняется, и, как следствие, ее выполнение может занять длительное время. Читатели могут выбрать любой драйвер стороннего производителя из своей системы во время этого примера упражнения. Существует несколько приложений для получения списка драйверов и соответствующих сведений о работающей системе, и читатели могут использовать такие приложения, как driverquery (из Windows: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands /driverquery) и DriverView (от Nirsoft: https://www.nirsoft.net/utils/driverview.html), которые очень просты. В моем случае я взял драйвер veracrypt.sys только для того, чтобы показать значимую разницу между обоими примерами (с символами отладки и без них):

1687279733640.png


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

Чтобы не расширять эту статью, я буду использовать подключаемый модуль IDA Pro под названием DriverBuddyReloaded (https://github.com/VoidSec/DriverBuddyReloaded) для декодирования IOCTL:

1687279751735.png


Выходные данные DriverBuddyReloaded показывают декодирование каждого IOCTL, найденного в коде:

1687279761858.png


Обратите внимание на горячие клавиши, такие как CTRL+ALT+F, для декодирования всех IOCTL внутри функции; CTRL+ALT+A для запуска автоматического анализа и CTRL+ALT+D для декодирования одного кода IOCTL. Они могут вам очень помочь.

Я сделал быструю разметку первой подпрограммы (DriverEntry), создал структуру (строка 93), применил макросы (горячая клавиша M) и создал перечисление, содержащее все имена IOCTL и их соответствующие значения.

На этом этапе все вызовы функций могут быть нормально проанализированы, потому что это законный драйвер, не вредоносный, и он следует и использует те же концепции, которые я показал в этой статье. Тем не менее, это было бы не очень продуктивно и только увеличило бы объем статьи.

Я попытался предоставить необходимую базовую основу для драйверов ядра, драйверов минифильтров и WFP (платформы фильтрации Windows), не вдаваясь в слишком много деталей программирования. Это будет полезно читателям в моих следующих статьях.

10. Рекомендуемые блоги и веб-сайты

Есть отличные исследователи кибербезопасности и компании, которые ведут блоги и пишут действительно хорошие статьи о внутреннем устройстве операционной системы, реверс-инжиниринге, исследовании уязвимостей и разработке эксплойтов.

Ниже приводится список интересных веб-сайтов и соответствующих дескрипторов Twitter в алфавитном порядке:

1687279777370.png


11. Заключение


Эта статья, как я уже сказал в начале, представляет собой введение в сложную тему драйверов ядра и минифильтров. Цель состоит в том, чтобы помочь профессионалам получить минимальные знания об используемых концепциях и обеспечить необходимую основу для следующих статей.

Сегодня я работаю в другой области (реверс + разработка эксплойтов), но мне всегда нравится напоминать более близким исследователям, что у каждого человека свой уникальный взгляд на мир информационной безопасности, и ни один из них не ошибается. Следуй за своим сердцем. :)
 
Исправленная и дополненая версия перевода первой статьи из цикла Exploiting Reversing Series
Переведено для xss.pro.
Оригинальная статья: https://exploitreversing[.]com/2023/04/11/exploiting-reversing-er-series/
Автор статьи Alexandre Borges
Автор перевода handersen.

0. Цитата
1. Введение
2. Благодарности
3. Ссылки
4. Обзор драйверов ядра
5. Обзор драйверов–фильтров
6. Обзор Windows Driver Frameworks (WDF)
7. Дополнительная информация о функциях обратного вызова
8. Реверс-инжиниринг и Windows Filtering Platform (WFP)
9. Дополнительные сведения о реверс-инжиниринге драйверов
10. Рекомендуемые блоги и веб-сайты
11. Заключение
Послесловие

Скачать с DamageLib: http://**************************************************************/d/VwWT949F17iVR9jg51SxQ

По ссылке скачается: Exploiting_Reversing_Series_part_1_2024_ru.pdf

sha-256: 65492842272177d72b6288453b926029003f6a2f7473d8ca053999bba996e38c

Нашли опечатки, неточности в переводе или другие недоработки? Дайте знать.
 
Корректирующий релиз с исправлением порядка 50 опечаток и более правильным переводом отдельных терминов.

Скачать с DamageLib: http://**************************************************************/d/319Yd46FSWNBDZVgiUVtEH

По ссылке скачается: Exploiting_Reversing_Series_part_1_2024_ru_err_fixes.pdf

sha-256: 5d1c81d33909fd0bc2b9d6435a2ed6a14cbbf7fb5856aa19dba929c387370d43
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Пожалуйста, обратите внимание, что пользователь заблокирован
Перевод второй статьи из цикла Exploiting Reversing Series

Переведено для xss.pro.

Оригинальная статья: https://exploitreversing[.]com/2024/01/03/exploiting-reversing-er-series-article-02/

Автор статьи Alexandre Borges

Автор перевода handersen.

Скачать с DamageLib: http://**************************************************************/d/5MDLHQ7F76Fjlx1ovDMucC

По ссылке скачается: Exploiting_Reversing_Series_part_2_2025_ru.pdf

sha-256: 99671f9da8a7171fa06f1400a2f40d6af737197c9f8d84463c89e9febb9f7cfb


Конструктивная критика перевода - приветствуется.
 
Пожалуйста, обратите внимание, что пользователь заблокирован


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