1. Введение
Добро пожаловать в первую статью серии Exploiting Reversing (ER), в которой я рассмотрю концепции, методы и практические шаги, связанные с двоичными файлами, и, в конце концов, проанализирую уязвимости в целом. Если читатели не читали прошлые статьи о других моих сериях (MAS — Malware Analysis Series), то все они доступны по следующим ссылкам:
В различных случаях нам приходится анализировать драйверы ядра или драйверы мини-фильтров, чтобы понять уязвимость или даже вредоносный драйвер (известный как руткит), и эта тема обычно сложна и содержит много деталей, которые в конечном итоге заслуживают объяснения. Тем не менее, мне все еще нужна была лучшая мотивация, чтобы начать эту новую серию, и она возникла, когда я анализировал детали минифильтра 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).
Существенным моментом в отношении драйверов ядра является понимание того, что один драйвер не делает все в одиночку. На самом деле, когда приложение отправляет запрос ввода-вывода, вероятно, будут драйверы, организованные в стек, каждый из которых отвечает за получение запроса, выполнение каких-либо действий или бездействие и передачу запроса следующему драйверу. Таким образом, из этого пункта вытекают важные понятия. После загрузки драйверов каждый из них представляется объектом драйвера, который имеет следующую структуру:
Объект-драйвер содержит жизненно важную информацию, вот некоторые из них:
*DeviceObject: указатель на объекты устройств, созданные драйвером (IoCreateDevice()).
*DriverExtension: указатель на расширение драйвера, которое используется драйвером для сохранения подпрограммы AddDevice в поле DriverExtension → AddDevice.
*DriverInit: точка входа, настроенная диспетчером ввода-вывода, в подпрограмму DriverEntry.
*DriverUnload: точка входа в процедуру выгрузки.
*MajorFunction: указатель на таблицу диспетчеризации, содержащую массив указателей входа на подпрограммы драйвера.
Драйверы составляют стек драйверов, и каждый из них связан с объектом драйвера. Каждый объект драйвера содержит один или несколько объектов устройств, представленных структурой _DEVICE_OBJECT:
Соответствующие поля в этой структуре:
*Тип: значение 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, упомянутых в последних двух абзацах, у нас есть следующее:
Кратко о его параметрах:
*DriverObject: содержит указатель на объект драйвера, полученный в качестве параметра функции DriverEntry().
*DeviceExtensionSize: представляет количество байтов, зарезервированных для расширения устройства объекта драйвера. Расширение устройства может использоваться для хранения частной структуры данных, связанной с устройством, но обычно оно используется с драйверами устройств, а не с драйверами ядра.
*DeviceName: необязательно указывает на буфер, который содержит имя объекта устройства, как и ожидалось.
*DeviceType: определяет тип устройства, который задается константами FILE_DEVICE_*. Чтобы добавить их в IDA Pro как перечисление:
+Добавьте библиотеку типов с именем ntddk64_win10 (SHIFT+11 и горячие клавиши INS).
+ Перейдите на вкладку «Перечисления» (SHIFT+F10), вставьте новое перечисление, выберите «добавить стандартное перечисление по имени символа» и выберите FILE_DEVICE_DISK.
*DeviceCharacteristics: этот параметр указывает одну или несколько констант, но в контексте драйвера ядра в большинстве случаев он будет равен нулю (0) или FILE_DEVICE_SECURE_OPEN. Повторяя те же действия, что и для DeviceType, но на этот раз добавьте FILE_DEVICE_SECURE_OPEN.
*Exclusive: этот параметр определяет, представляет ли объект устройства монопольное устройство, которое контролирует и определяет, может ли несколько файловых объектов открывать устройство.
*DeviceObject: этот параметр содержит указатель на структуру DEVICE_OBJECT, размещенную в невыгружаемом пуле.
Исходя из объясненных понятий, имеем следующую схему:
*драйвер установлен → объект драйвера (_DRIVER_OBJECT) → один или несколько объектов устройства (_DEVICE_OBJECT).
До сих пор единственной упомянутой подпрограммой драйвера была DriverEntry, имеющая следующую сигнатуру:
Первый параметр — это указатель на DRIVER_OBJECT, а структура второго параметра — UNICODE_STRING, указывающий, что ключ Parameters-это указатель RegistryPath драйвера в реестре:
Помимо основных задач, выполняемых (на самом деле, вызываемых) в 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, как показано ниже:
Есть поля, которые предоставляют нам важный контекст и информацию о работе драйвера ядра, некоторые из них будут объяснены по мере необходимости и должны быть дополнены новыми понятиями, которые будут представлены позже. Даже если это не показано на предыдущем изображении, IRP имеет фиксированную часть, содержащую заголовок (идентификатор потока вызывающей стороны, адрес объекта устройства, блок состояния ввода-вывода и т. д.), который используется диспетчером ввода-вывода для управления IRP, и вторая часть, специфичная для каждого драйвера (местоположение стека ввода-вывода), которая содержит такие параметры, как код функции запрошенной операции и ее соответствующий контекст:
Мы собираемся сделать новые заметки на эту тему позже. Снова сосредоточившись на теме основных кодов 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() связаны с указанной целью:
Первые два параметра этой функции:
*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 передаются и сохраняются от верхнего драйвера ядра к нижнему?
Добро пожаловать в первую статью серии Exploiting Reversing (ER), в которой я рассмотрю концепции, методы и практические шаги, связанные с двоичными файлами, и, в конце концов, проанализирую уязвимости в целом. Если читатели не читали прошлые статьи о других моих сериях (MAS — Malware Analysis Series), то все они доступны по следующим ссылкам:
В различных случаях нам приходится анализировать драйверы ядра или драйверы мини-фильтров, чтобы понять уязвимость или даже вредоносный драйвер (известный как руткит), и эта тема обычно сложна и содержит много деталей, которые в конечном итоге заслуживают объяснения. Тем не менее, мне все еще нужна была лучшая мотивация, чтобы начать эту новую серию, и она возникла, когда я анализировал детали минифильтра 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).
Существенным моментом в отношении драйверов ядра является понимание того, что один драйвер не делает все в одиночку. На самом деле, когда приложение отправляет запрос ввода-вывода, вероятно, будут драйверы, организованные в стек, каждый из которых отвечает за получение запроса, выполнение каких-либо действий или бездействие и передачу запроса следующему драйверу. Таким образом, из этого пункта вытекают важные понятия. После загрузки драйверов каждый из них представляется объектом драйвера, который имеет следующую структуру:
Объект-драйвер содержит жизненно важную информацию, вот некоторые из них:
*DeviceObject: указатель на объекты устройств, созданные драйвером (IoCreateDevice()).
*DriverExtension: указатель на расширение драйвера, которое используется драйвером для сохранения подпрограммы AddDevice в поле DriverExtension → AddDevice.
*DriverInit: точка входа, настроенная диспетчером ввода-вывода, в подпрограмму DriverEntry.
*DriverUnload: точка входа в процедуру выгрузки.
*MajorFunction: указатель на таблицу диспетчеризации, содержащую массив указателей входа на подпрограммы драйвера.
Драйверы составляют стек драйверов, и каждый из них связан с объектом драйвера. Каждый объект драйвера содержит один или несколько объектов устройств, представленных структурой _DEVICE_OBJECT:
Соответствующие поля в этой структуре:
*Тип: значение 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, упомянутых в последних двух абзацах, у нас есть следующее:
Кратко о его параметрах:
*DriverObject: содержит указатель на объект драйвера, полученный в качестве параметра функции DriverEntry().
*DeviceExtensionSize: представляет количество байтов, зарезервированных для расширения устройства объекта драйвера. Расширение устройства может использоваться для хранения частной структуры данных, связанной с устройством, но обычно оно используется с драйверами устройств, а не с драйверами ядра.
*DeviceName: необязательно указывает на буфер, который содержит имя объекта устройства, как и ожидалось.
*DeviceType: определяет тип устройства, который задается константами FILE_DEVICE_*. Чтобы добавить их в IDA Pro как перечисление:
+Добавьте библиотеку типов с именем ntddk64_win10 (SHIFT+11 и горячие клавиши INS).
+ Перейдите на вкладку «Перечисления» (SHIFT+F10), вставьте новое перечисление, выберите «добавить стандартное перечисление по имени символа» и выберите FILE_DEVICE_DISK.
*DeviceCharacteristics: этот параметр указывает одну или несколько констант, но в контексте драйвера ядра в большинстве случаев он будет равен нулю (0) или FILE_DEVICE_SECURE_OPEN. Повторяя те же действия, что и для DeviceType, но на этот раз добавьте FILE_DEVICE_SECURE_OPEN.
*Exclusive: этот параметр определяет, представляет ли объект устройства монопольное устройство, которое контролирует и определяет, может ли несколько файловых объектов открывать устройство.
*DeviceObject: этот параметр содержит указатель на структуру DEVICE_OBJECT, размещенную в невыгружаемом пуле.
Исходя из объясненных понятий, имеем следующую схему:
*драйвер установлен → объект драйвера (_DRIVER_OBJECT) → один или несколько объектов устройства (_DEVICE_OBJECT).
До сих пор единственной упомянутой подпрограммой драйвера была DriverEntry, имеющая следующую сигнатуру:
Первый параметр — это указатель на DRIVER_OBJECT, а структура второго параметра — UNICODE_STRING, указывающий, что ключ Parameters-это указатель RegistryPath драйвера в реестре:
Помимо основных задач, выполняемых (на самом деле, вызываемых) в 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, как показано ниже:
Есть поля, которые предоставляют нам важный контекст и информацию о работе драйвера ядра, некоторые из них будут объяснены по мере необходимости и должны быть дополнены новыми понятиями, которые будут представлены позже. Даже если это не показано на предыдущем изображении, IRP имеет фиксированную часть, содержащую заголовок (идентификатор потока вызывающей стороны, адрес объекта устройства, блок состояния ввода-вывода и т. д.), который используется диспетчером ввода-вывода для управления IRP, и вторая часть, специфичная для каждого драйвера (местоположение стека ввода-вывода), которая содержит такие параметры, как код функции запрошенной операции и ее соответствующий контекст:
Мы собираемся сделать новые заметки на эту тему позже. Снова сосредоточившись на теме основных кодов 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() связаны с указанной целью:
Первые два параметра этой функции:
*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 передаются и сохраняются от верхнего драйвера ядра к нижнему?
Последнее редактирование: