Статья Алгоритм поиска уязвимых драйверов

Драйверы открывают доступ в ядро ОС, вот только начиная с Win7-x64 Microsoft ввела принудительную их подпись по EV-сертификатам (Extended Validation). А всё потому, что с выходом платформы KMDF (Kernel Mode Driver Frameworks) программирование драйверов стало напоминать игру «Тетрис», в результате чего огромная армия пионеров дружненько заполнила рынок кривыми дровишками. Теперь-же, Windows примет на свой борт только те драйвера, которые прошли жёсткий фейс-контроль в штабе Microsoft, и имеют удостоверяющую личность цифровую печать.

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

На программном уровне, драйвер олицетворяет структура DRIVER_OBJECT, и она всегда привязывается к какому либо устройству DEVICE_OBJECT. Это устройство может быть как физическим (например порт или диск), так и логическим типа файловой системы FAT/NTFS. Один драйвер способен обслуживать запросы сразу от нескольких устройств одного типа, тогда будем иметь одну структуру DRIVER_OBJECT, и несколько связанных с ней DEVICE_OBJECT. Вот представленная отладчиком WinDbg топология драйвера шины PCI.SYS где видно, что в его команде аж 20 подчинённых устройств, и нужно сказать это не предел (у acpi.sys их 43):

Код:
0: kd> !drvobj pci
Driver Object (fffffa800443a980) is for:  \Driver\pci

Device Object list:
  fffffa80043d5060  fffffa80043cc040  fffffa80043cb660
  fffffa80043c7a10  fffffa80043c7060  fffffa80043c6a10
  fffffa80043c5a10  fffffa80043c5060  fffffa80043c4a10
  fffffa80043c3a10  fffffa80043c3060  fffffa80043c2a10
  fffffa80043c1a10  fffffa80043c1060  fffffa80043c0a10
  fffffa80043cbcb0  fffffa80043c4060  fffffa800443a5d0
  fffffa80043c6060  fffffa80043c2060

Один драйвер не может решать сразу все задачи, поэтому драйвера собираются в цепочку образуя «Стек». Выполнив свою работу, драйверы в стеке передают запрос следующему и так, пока запрос не дойдёт до драйвера самого низкого уровня. Обычно на самом низком уровне находятся драйверы физ.устройств PDO (Physical Device Object), а на более высоком уровне функциональные FDO. Ответ на запрос распространяется по стеку в обратном порядке.

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

Код:
0: kd> !devstack fffffa80043c6a10

  !DevObj           !DrvObj           !DevExt           ObjectName
  ----------------  ----------------  ----------------  -------------
  fffffa80043e92f0  \Driver\intelide  fffffa80043e9440  PciIde0
  fffffa80043c37f0  \Driver\ACPI      fffffa80039a82d0
> fffffa80043c6a10  \Driver\pci       fffffa80043c6b60  NTPNP_PCI0012

  ServiceName is "intelide"

1. Модель драйверов фильтра

Не возможно атаковать драйвера не зная принципы их функционирования. Чтобы построить абъюзивную схему отношений, здесь нужен вектор нападения. Драйверы основных устройств (таких шины, мосты и прочее) пишутся в закрытых офисах Microsoft, и следовательно они тщательно отшлифованы – найти в них дыры практически нереально. Другое дело фильтр-драйверы, которые инкапсулируются в стек-драйверов устройства (выше или ниже основного драйвера), расширяя тем-самым его базовый функционал. На этом фоне выделяются фильтры файловой системы NTFS. Перед тем-как записать данные на диск, этот фильтр может например зашифровать поток, а при чтении обратно расшифровать. Так работает модуль «BitLocker» в Windows.

Когда мы запрашиваем доступ к файлу вызовом NtCreateFile(), системный диспетчер ввода-вывода создаёт пакет запроса IRP (Interrupt Request Packet, содержит тип операции и её аргументы). Далее IRP попадает в стек, где может располагаться драйвер фильтра. Архитектурой WDM всего поддерживается 28 типа запросов с кодами IRP_MJ_xx, а на те, которые интересуют, драйвер-фильтра вешает свои функции обратного вызова «Callback», или как их называют в доках «Dispatch Routines» (процедуры обслуживания).

Код:
Dispatch routines:
[00] IRP_MJ_CREATE
[01] IRP_MJ_CREATE_NAMED_PIPE
[02] IRP_MJ_CLOSE
[03] IRP_MJ_READ
[04] IRP_MJ_WRITE
[05] IRP_MJ_QUERY_INFORMATION
[06] IRP_MJ_SET_INFORMATION
[07] IRP_MJ_QUERY_EA
[08] IRP_MJ_SET_EA
[09] IRP_MJ_FLUSH_BUFFERS
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION
[0b] IRP_MJ_SET_VOLUME_INFORMATION
[0c] IRP_MJ_DIRECTORY_CONTROL
[0d] IRP_MJ_FILE_SYSTEM_CONTROL
[0e] IRP_MJ_DEVICE_CONTROL
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL
[10] IRP_MJ_SHUTDOWN
[11] IRP_MJ_LOCK_CONTROL
[12] IRP_MJ_CLEANUP
[13] IRP_MJ_CREATE_MAILSLOT
[14] IRP_MJ_QUERY_SECURITY
[15] IRP_MJ_SET_SECURITY
[16] IRP_MJ_POWER
[17] IRP_MJ_SYSTEM_CONTROL
[18] IRP_MJ_DEVICE_CHANGE
[19] IRP_MJ_QUERY_QUOTA
[1a] IRP_MJ_SET_QUOTA
[1b] IRP_MJ_PNP

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

1. Не трогая, передать пакет следущему драйверу в стеке.​
2. Изменить пакет IRP, и отдать его сл.драйверу.​
3. Завершить указанную в IRP операцию с успешным результатом.​
4. Завершить операцию IRP с ошибкой.​
5. Передав без изменения запрос сл.драйверу, изменить ответ на обратном пути.​
6. Передать пакет IRP в стек другого устройства.​

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


2. Менеджер фильтров и мини-фильтры

Реализовать драйвер фильтра с нуля довольно сложно – нужно будет обрабатывать каждый из 28-ми типов запросов на ввод-вывод IRP_MJ_xx, даже если они нам не нужны. Другая проблема – это позиция фильтра в стеке. Легко прикрепить его первым к вершине, но попытка загнать в середину может привести к катастрофе, т.к. порядок следования фильтров может отличаться от порядка загрузки их в память. Ядро это не юзер-спейс – оно не понимает таких шуток, и сразу падает в бсод.

Чтобы упростить написание фильтров, Win поставляется с их диспетчером FLTMGR.SYS, который отвечает за обработку запросов на ввод-вывод, и передачу пакетов IRP по стеку. Так появились драйверы «Мини-фильтра», вместо уже устаревших драйверов фильтра. На следующей схеме показано, как меняется архитектура ввода-вывода, когда в штате появился новый сотрудник в лице «диспетчера фильтров» (выделен синим). Здесь видно, что мини-фильтры не добавляют в стек свои собственные объекты устройств – вместо этого они регистрируются у диспетчера/менеджера, который вызывает мини-фильтры для обработки IRP. Если запрос не поддерживается ни одним из мини-фильтров, диспетчер сам передаёт пакет IRP сл.драйверу в стеке, сводя на нет всевозможного рода ошибки. Лог отладчика подтверждает, что менеджер в стеке располагается выше драйвера NTFS.SYS:

Код:
0: kd> !drvobj ntfs
Driver object (fffffa800445f9c0) is for: \FileSystem\Ntfs

Device object list:
fffffa80047e6030  fffffa8004bd8030  fffffa8004ba8030
fffffa8004b75030  fffffa80049ba030  fffffa8004bc3030

0: kd> !devstack fffffa80047e6030

  !DevObj           !DrvObj             !DevExt
  ----------------  ------------------  ----------------
  fffffa8003bfb060  \FileSystem\FltMgr  fffffa8003bfb1b0
> fffffa80047e6030  \FileSystem\Ntfs    fffffa80047e6180

MiniFlt.webp

Более того, диспетчер реализует механизм упорядочивания мини-фильтров по значению высоты «Altitudes» – чем оно выше, тем выше приоритет. Например при поступлении запроса IRP, мини-фильтр на высоте 420.000 будет вызываться перед фильтром на высоте 280.000, и т.д. Значения высоты решают ещё проблему того, что теперь порядок следования фильтров в стеке не повлияет на порядок загрузки их в память. Приоритеты раздаёт Microsoft в момент подписи драйверов – со списком зареганых на данный момент высот можно ознакомиться .

Код:
420.000 – 430.000: Filter
400.000 – 410.000: FS_Filter Top
392.000 – 395.000: FS_Filter Security Monitor
360.000 – 390.000: FS_Filter Activity Monitor
340.000 – 350.000: FS_Filter Undelete
320.000 – 330.000: FS_Filter Anti-Virus
300.000 – 310.000: FS_Filter Replication
280.000 – 290.000: FS_Filter Backup
272.000 – 275.000: FS_Filter Security Content
260.000 – 270.000: FS_Filter Content Screener
240.000 – 250.000: FS_Filter Quota Management
220.000 – 230.000: FS_Filter System Recovery
200.000 – 210.000: FS_Filter Cluster File System
180.000 – 190.000: FS_Filter HSM
170.000 – 175.000: FS_Filter Imaging ZIP
160.000 – 170.000: FS_Filter Compression
140.000 – 150.000: FS_Filter Encryption
130.000 – 140.000: FS_Filter Virtualization
120.000 – 130.000: FS_Filter Physical Quota
100.000 – 110.000: FS_Filter Open File
 80.000 -  90.000: FS_Filter Security Enhancer
 60.000 -  70.000: FS_Filter Copy Protection
 52.000 -  55.000: FS_Filter Security Bottom
 40.000 -  50.000: FS_Filter Bottom
 20.000 -  30.000: FS_Filter System

3. Сбор информации о мини-фильтрах

Мини-фильтр сначала должен зарегистрировать своё присутствие функцией FltRegisterFilter() из драйвера FltMgr.sys. При этом в структуре «FLT_REGISTRATION» он перечисляет все колбеки IRP_MJ_xx, которые планирует перехватить. Теперь драйвер помещает свою тушку в стек мини-фильтров функцией FltStartFiltering(), а позже может отсоедениться вызовом FltUnregisterFilter(). Штатная утилита ком.строки fltmc.exe выводит паспорт всех зареганых в диспетчере фильтров – здесь видим высоту (приоритет в стеке) и кол-во его экземпляров:

fltmc_1.webp


На своём узле я обнаружил всего 2 мини-фильтра, и оба они хукают обращения к низлежащему драйверу NTFS. Первый LuaFV.sys это фильтр модуля UAC, который закрывает смертным юзерам доступ к системным файлам – в дословном переводе означает «Low User Account File Virtualization». Второй FileInfo.sys представляет драйвер, который отвечает за проверку файлов Win на целостность (см.утилиты sfc и dism.exe). Поскольку UAC имеет смысл только в контексте системного диска, фильтр luafv.sys контролирует обращения исключительно к разделу C:\, о чём свидетельствует число экземпляров(1). Просмотреть лист активных томов накопителя можно ключём «Volumes», и по желанию вручную прикреплять/отсоединять мини-фильтры к этим томам ключами «Attach/Detach» соответственно (см.хелп):

fltmc_2.webp


Узнать, к каким именно разделам диска приатачен тот или иной фильтр можно ключём «Instances» – как видим теория выше верна, и под надзором luafv.sys находится раздел(C:\) с установленной системой. Его собрат FileInfo.sys не знает точно, где могут лежать системные файлы для проверки, а потому ведёт перекрёстный огонь сразу по всем томам, в т.ч. и сетевым дискам «Mup». Итого шесть лог.разделов на моих хардах (включая неактивную флешку G:\ под ником Vol#3), которые контролируют шесть указанных на первом скрине экземпляров фильтра:

fltmc_3.webp



4. Природа уязвимостей в мини-фильтрах

Чтобы найти баг, злоумышленник должен послать драйверу данные, на которые дров не рассчитывал. Что делает мини-фильтры привлекательными, так это множество каналов связи с ними. Например драйверу мини-фильтра не нужно создавать подчинённый DEVICE_OBJECT для своей жизнедеятельности – этим занимается диспетчер FltMgr.sys. Однако фильтр может создать девайс для своих собственных нужд. Типичный вектор атаки – это когда мы открываем дескриптор «объекта устройства», и отправляем ему управляющие коды FSCTL для анализа уязвимого поведения. Ясно, что это не единственно возможный вариант, а потому рассмотрим поверхностно остальные.

4.1. Порты коммуникации

Один из уникальных механизмов связи диспетчера с фильтром – это порт, который мини-фильтр создаёт функцией диспетчера FltCreateCommunicationPort(). Имя порта указывается в структуры OBJECT_ATTRIBUTES, и далее сохраняется в системном пространстве имён OMNS (Object Manager Name Space) или в корне каталога, или по пути \\FileSystem\Filters. При этом в той-же структуре фильтр должен указать адрес своей функи обратного вызова для обслуживания запросов, плюс дескриптор безопасности SD, чтобы доступ к порту имел только админ, хотя можно расшарить порт и для юзера.

WinObj.webp

После того-как порт связи с мини-фильтром создан, мы можем открыть его по имени, или вызовом из библиотеки FltLib.dll функции FilterConnectCommunicationPort() прямо из пользовательского режима. Если диспетчер даст нам добро на доступ к порту, то при подключении сработает обратный вызов «ConnectNotifyCallback» с уведомлением, что всё гуд. Основная проблема здесь в том, как программно найти имя порта, а потому придётся использовать набор сторонних инструментов, например расширение !fltkd.help отладчика WinDbg, или софт «SystemInformer» (в младенчестве ProcessHacker).

Диспетчер предоставляет сл.функции из своей тушки FltMgr.sys для фильтров, чтобы они могли взаимодействовать с приложениями юзер-моды:

Код:
FltCreateCommunicationPort()
FltCloseCommunicationPort()
FltSendMessage()
FltReceiveMessage()

Перечень функций для обратной коммуникации из юзера к мини-фильтрам выглядит так (см. FltLib.dll в папке System32):

Код:
FilterConnectCommunicationPort()
FilterGetMessage()
FilterSendMessage()
FilterReplyMessage()

За доставку сообщений отвечает вспомогательный драйвер диспетчера FltMgrMsg.sys, который мы видим выше в окне программы «Win Object Explorer» из пакета SysinternalsSuite.


4.2. Ntfs Reparse Point, или точки повторной обработки

«Reparse Point» представляет собой тип атрибута в файловых потоках NTFS. Если открыть спеку, то он числится в метаданных под номером 0xC0, и позволяет нашему приложению связать с произвольным файлом блок дополнительных своих данных. Когда диспетчер «Object Manager» обнаруживает данный атрибут в файле, он выполяет повторный поиск имени, чем собственно и обусловлено название «Reparse Point». По сути фишка бесполезна, но благодаря ей прогеры могут расширять функционал NTFS.

На данный момент предусмотрено 2 типа точек RP – это пользовательские, и определённые в Microsoft. Последние используются диспетчером объектов для создания символических ссылок на файлы, а драйвером Mountvol.sys для создания точек монтирования томов жёсткого диска. Далее нас будут интересовать исключительно пользовательские «Third-Party Reparse Point».

Ntfs_RP.webp

Пользовательскую точку определяет структура REPARSE_GUID_DATA_BUFFER с таким прототипом, которую нужно будет аргументом передать в функцию DeviceIoControl():

Код:
struct REPARSE_GUID_DATA_BUFFER
  ReparseTag         dd  0
  ReparseDataLength  dw  0
  Reserved           dw  0
  ReparseGuid        rb  16
  DataBuffer         db  ?
ends
----------------------------------
1. Обязательный тег (см.табл.ниже)
2. Размер данных в буфере
3. Резерв (выравнивание)
4. Уникальный GUID точки RP
5. Буфер с данными (макс. 16 КБ)

Интерес здесь представляет буфер с данными, которые скопируются в поток «Reparse Point» файла. В качестве данных можно использовать что-угодно (например шелл-код), лишь-бы они не превышали макс.размера в 16 КБ. В спеке на NTFS имеется такая табличка со-значением тегов, хотя для пользовательских точек старшие 2-байта можно сбросить в нуль, или выставить значение 0x4000 «Высокая задержка», что означает низкую скорость отклика. Для точек мелкомягких обязательным является значение 0х8000 (взведён ст.бит), и нужно передавать структуру REPARSE_DATA_BUFFER с немного иным содержимым, и без 16-байтного GUID.

Tag.webp


Если мы хотим просканировать диск на наличие файлов с потоками RP, можно вызвать FindNextFile() и проверить в выхлопе флаг FILE_ATTRIBUTE_REPARSE_POINT. Юзер-приложение может устанавливать, запрашивать и удалять точки повторной обратотки в файлах функцией DeviceIoControl() со-следующими кодами файловой системы.

Код:
FSCTL_SET_REPARSE_POINT    = 0x000900a4
FSCTL_GET_REPARSE_POINT    = 0x000900a8
FSCTL_DELETE_REPARSE_POINT = 0x000900ac

Со своей стороны диспетчер фильтров тоже имеет функции для работы с точками RP, которые может вызывать драйвер мини-фильтра:

Код:
FltAddOpenReparseEntry()
FltRemoveOpenReparseEntry()
FltTagFileEx()
FltUntagFile()

Если фильтр импортирует эти API в свою тушку, значит он реализует формат пользовательских точек RP – это может стать потенциальной уязвимостью, т.к. в буфере может притаиться хоть сам чёрт.

Для эксперимента создадим пустой файл с любым расширением (у меня TestFile.txt), и ещё один с произвольными данными, которые поместим в буфер потока RP (я записал строку HelloHackerLab). Теперь возьмём софтину «NTFS Stream Explorer», где можно быстро создать точку повторной обработки – по нашим данным она сама заполнит структуру REPARSE_GUID_DATA_BUFFER, и вызовет все нужные API. Вот скрин и галки, которые нужно в ней взвести:

NSE.webp

Что примечательно, система не может теперь получить доступ к новоиспечённому TestFile.txt – его нельзя ни открыть, ни удалить, ни скопировать. Единственно кому доступен файл, это утилита fsutil.exe из штатной поставки Win, которой необходимо передать ключ «reparsepoint» (принимает 2 аргумента: query и delete). Как видим, она исправно распарсила файл, а чтобы обратно получить доступ к нему, нужно просто удалить точку аргументом «delete»:

fsutil.webp


Ещё одна уязвимость состоит в том, что начиная с Win10 в составе ОС появился новый управляющий код FSCTL_SET_REPARSE_POINT_EX =0x0009040C, который старые мини-фильтры не обрабатывают, ожидая только кода без суффикса(EX). В некоторых случаях это позволит приложению добавлять/удалять теги с флагом Microsoft 0x8000, что приведёт к получению файла с системной меткой, для обхода таких сторожей как WinDefender.


4.3. Уязвимость в значении высоты мини-фильтра

Это класс ошибок, которые вызваны порядком следования мини-фильтров в стеке, на основе заданных им высот «Altitudes». Например, несоответствие высоты фильтра является причиной пропуска файловых событий шпионом «Process Monitor» из пакета SysinternalsSuite. Если запустить его и следом уже знакомую нам утилиту fltmc, то можно обнаружить, что драйвер мини-фильтра шпиона называется «ProcMon24.sys», и ему назначена высота 382.500, в то время как сторож LUAFV находится на более низком уровне 135.000. Как результат, фильтр LUAFV перехватывает все обращения шпиона ProcMon.exe к системным файлам, искажая логи последнего. Баг в том, что мы можем отсоединить от тома фильтр ProcMon24 командой «Detach», и снова присоединить его «Attach» на гораздо меньшей высоте чем LUAFV, например 20.000. Так шпион обойдёт сторожа, и в его логах мы получим больше интересной информации. Для справки по ключам введите fltmc attach.

ProcMon.webp



5. Эпилог

В узких кругах уязвимые драйвера называют LOL-драйверами от «Living Off The Land». Огромная их коллекция собрана здесь: . Поиск багов в них это целая наука, на постижение которой можно потратить всю оставшуюся жизнь. Нужно знать не только общую архитектуру драйверов, но и пятой точкой уметь предсказывать все последующие шаги драйвера, после того-как пошлём ему пакет IRP для обработки. Отличным полигоном на начальном этапе может послужить 32-битная WinXP, которая не требует подписи драйверов, а потому любезно принимает на свой борт даже заведомо кривые субстанции для практической реализации атак. Здесь главное руку набить, после чего можно переселиться и на драйвера более актуальных систем.

Ссылки по теме:

Руководство по поиску багов


Примеры мини-фильтров
Windows-driver-samples/filesys/miniFilter at main · microsoft/Windows-driver-samples

Разбор портов коммуникации


Справочник по кодам FSCTL


Дом.страница софта «NTFS Stream Explorer»
 
  • Нравится
Реакции: Edmon Dantes
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab