Как злоумышленники используют подписанные драйверы для захвата инфраструктуры. Разбор техники BYOVD.
Введение в BYOVD
BYOVD (Bring Your Own Vulnerable Driver) – это продвинутая техника, позволяющая злоумышленникам использовать подписанные, легитимные драйверы для получения несанкционированного доступа к режиму ядра операционной системы. Если быть точнее, BYOVD – это собирательное название для группы техник, эксплуатирующих уязвимые примитивы в драйверах операционных систем.
При эксплуатации уязвимых драйверов, атакующие достигают следующие цели:
Эскалация привилегий Privilege Escalation: получение прав уровня NT AUTHORITY\SYSTEM.
Обход механизмов защиты Defense Evasion: нейтрализация или обход средств защиты информации.
Закрепление в системе Persistence: внедрение в систему скрытых механизмов сохранения присутствия.
Манипуляции с данными Impact: осуществление несанкционированных операций чтения, модификации или удаления критически важных данных в корпоративной информационной инфраструктуре.
Выполнение вредоносного кода Execution: запуск произвольного кода в режиме ядра (Ring 0), что предоставляет полный контроль над системой и позволяет осуществлять прямое манипулирование системными структурами, объектами и памятью.
Вместо поиска уязвимостей в самом ядре, атакующие ищут уязвимости в драйверах, разработанных сторонними производителями, которые уже имеют право на выполнение в режиме ядра. Это значительно упрощает задачу, поскольку драйверы уже подписаны и, следовательно, не вызывают подозрений у системы. BYOVD считается одной из самых сложных техник, поскольку она позволяет злоумышленнику получить полный контроль над системой, оставаясь практически незамеченным.
Ключевым моментом техники BYOVD является то, что атакующие «приносят свой собственный уязвимый драйвер» (bring your own vulnerable driver), загружают его в ядро (или используют уже загруженный) и затем эксплуатируют обнаруженные в нём уязвимые примитивы с помощью собственных эксплойтов.
В этой статье мы сфокусируемся на техниках манипуляций привилегиями и правами доступа процессов в контексте BYOVD. Ключевая идея данного материала заключается в том, чтобы дать понимание читателям о том, как устроены базовые механизмы безопасности в операционной системе Windows, и что может случиться, если запросы, передаваемые драйверу, будут недостаточно контролироваться кодом самого драйвера.
Дисклеймер
Данный материал имеет ознакомительный характер и предназначен для специалистов по информационной безопасности и системных программистов, интересующихся вопросами защиты информации в ядре операционных систем. ООО «ПАРАНОИД СЕКЬЮРИТИ» не несет ответственности за возможный вред, причиненный с применением изложенной информации. Любая деятельность по созданию, распространению или использованию компьютерных программ, либо иной компьютерной информации, заведомо предназначенных для несанкционированного уничтожения, блокирования, модификации, копирования компьютерной информации или нейтрализации средств защиты компьютерной информации влечет за собой ответственность в соответствии с законодательством Российской Федерации.
Краткий экскурс в подсистему безопасности Windows
Знание принципов работы подсистемы безопасности Windows критически важно для понимания техник манипуляции с привилегиями и правами доступами.
В операционных системах термин «программа» обозначает статический исполняемый файл, размещенный на запоминающем устройстве. В отличие от него, запущенная программа именуется как процесс. Процесс представляет собой абстракцию, создаваемую ядром операционной системы для управления исполняющимся кодом. Эта абстракция инкапсулирует выделенные ресурсы и включает в себя совокупность структур данных ядра, таких как таблицы виртуальной памяти, дескрипторы открытых файлов и планируемые потоки. Функция операционной системы заключается в изоляции, планировании и управлении множеством параллельных процессов, обеспечивая безопасное и эффективное разделение ресурсов вычислительной системы.
Таким образом, процесс – это объект ядра операционной системы, который представляет работающий экземпляр программы. Ключевой структурой, описывающей процесс, является _EPROCESS. Это очень большая структура, содержащая более 200 полей и указателей на другие вспомогательные объекты. В ней записана вся информация о процессе, включая его идентификатор, приоритет, состояние, используемую память и, что наиболее важно (в контексте данного материала) – указатель на токен доступа (Access Token или Token).

Каждый процесс в ОС Windows имеет ассоциированный с ним Access Token. Это своеобразный паспорт, который процесс предъявляет каждый раз при выполнении каких-либо операций с ресурсами (чтение файлов, доступ к другими процессами, запись ключей реестра и т.д.). Access Token содержит исчерпывающую информацию об учетной записи, от имени которой запущен процесс, группах, к которым он принадлежит, и привилегиях, которыми он обладает. Таким образом, можно сказать что каждый процесс выполняется в контексте какой-либо учетной записи.
Поле Token в _EPROCESS является указателем на структуру _TOKEN, которая содержит информацию о привилегиях и правах доступа процесса.

Структура _TOKEN имеет более 40 полей и вложенных структур. Практические каждое из полей этой структуры описывает контекст безопасности процесса в зависимости от защитного механизма ядра Windows. Мы рассмотрим только некоторые из них.
Поле Privileges – это указатель на структуру _SEP_TOKEN_PRIVILEGES, которая содержит все привилегии процесса, а также их состояния. Подробно мы ее рассмотрим в следующем разделе.
Поле SessionId говорит о том, что процесс запущен в пользовательской сессии.
UserAndGroups определяет уровень доступа в контексте групп, в которых состоит учетная запись. А поле UserAndGroupCount содержит количество таких групп.
TokenType описывает тип токена. Если данное поле имеет значение TokenPrimary, то это говорит об использовании основного токена, значение TokenImpersonation – об использовании токена доступа «чужого процесса» (такое явление называют имперсонализацией). Данный механизм применяется в многопользовательских сервисах, когда основной процесс (Web-сервер ил SMB-сервер) выполняется из-под служебной (сервисной) учетной записи, но для корректного разрешения прав доступа к ресурсам (Web-страницам, записям в БД или файлам), процесс порождает новый поток, выполняющийся от имени учетной записи пользователя, запросившего такой доступ.
Даже с одной учетной записью в системе может быть зарегистрировано несколько различных сеансов. Для того, чтобы их разграничивать существует поле LogonSession – это указатель на структуру _SEP_LOGON_SESSION_REFERENCES, которая содержит информацию о времени входа, типе входа, аутентифицирующем пакете.
Мы получили краткое представление о ключевых структурах ядра, описывающих процессы, их права доступа и привилегии процессов, далее мы рассмотрим, как атакующие могут манипулировать полями этих структур.
Пример эксплуатации техники BYOVD: манипуляции с привилегиями процесса
Ядерные манипуляция с токенами доступа позволяют атакующим получить полный контроль над всеми процессами, выполняющимися в системе, в том числе защищенными (Protected Process Light). Наличие привилегий у процесса позволяет ему выполнять набор конкретных системных (чувствительных) операций. Например, если у процесса имеются привилегии SeBackupPrivilege и SeRestorePrivilege, то это дает ему возможность читать и писать в произвольные файлы независимо от наличия прав доступа. Это значит, что если у пользователя (точнее учетной записи) имеется явный запрет на запись (или чтение) конкретного файла, то с помощью указанных привилегий он может обойти этот запрет. С помощью привилегии SeLoadDriverPrivilege пользователь может загружать (или выгружать) драйверы, а с помощью SeDebugPrivilege – отлаживать процесс и получать полный доступ к его памяти.
Как мы увидели ранее, за наличие (или отсутствие) привилегий у процесса отвечает структура _SEP_TOKEN_PRIVILEGES (поле Privileges структуры _TOKEN).

Эта структура состоит из трех полей:
Поле Present – битовая маска привилегий, которые существуют в токене. Если бит установлен в 1, то привилегия присутствует и ей, при необходимости, можно воспользоваться, если 0 – то привилегия отсутствует, и ей воспользоваться невозможно.
Наличие в токене процесса определенной привилегии еще не означает, что она «активирована». Перед тем, как запросить чувствительную операцию, привилегию необходимо включить. Это делается с помощью специальной функции AdjustTokenPrivileges. За текущее состояние привилегий в токене процесса отвечает поле Enabled. Это еще одна битовая маска структуры _SEP_TOKEN_PRIVILEGES, которая определяет включена или выключена конкретная привилегия.
EnabledByDefault – битовая маска состояния привилегий, которые включены по умолчанию при создании токена.
Посмотрим на то, как это работает для процесса, запущенного без административных привилегий (если быть точнее, то для процесса с Medium Integrity Level):

Видим, что у процесса есть 5 привилегий, которые указаны в поле Present (0x602880000). При этом, только одна из них находится во включенном состоянии – поле Enabled (значение 0x800000).

На изображении выше приведены битовые эквиваленты каждого из полей структуры _SEP_TOKEN_PRIVILEGES.
Проведем манипуляцию с полем Present и посмотрим на результат.
С помощью команды отладчика eq (edit qword) мы изменили значение поля Present, после чего количество привилегий было расширено. По аналогии можно манипулировать полем Enabled для изменения состояния привилегий.
Таким образом, если атакующему удастся получить возможность манипулировать структурой данных с помощью уязвимого драйвера, то у него автоматически появится возможность повысить свои привилегии, и выполнять административные операции.
Техники BYOVD предоставляют возможность злоумышленникам не только повысить свои привилегии, но и другую критически важную способность – манипулировать привилегиями других процессов.
Этот аспект особенно опасен в контексте обхода средств защиты информации, таких как антивирусы (AV) и решения для обнаружения и реагирования на конечных точках (EDR).
Используя уязвимый драйвер, злоумышленник может целенаправленно лишить процессы AV/EDR набора критически важных привилегий, которые необходимы для их штатного функционирования. В результате лишения этих привилегий, часть функций средств защиты перестанет корректно работать. Например, AV-процесс может потерять возможность проверять файлы сигнатурным движком, отправлять или читать события подсистемы ETW (Event Tracing for Windows).
Фактически, атакующий использует легитимный, подписанный драйвер как «троянского коня» для создания идеальных условий: сначала он нейтрализует безопасность («ослепив» AV/EDR), а затем беспрепятственно выполняет свои основные вредоносные действия уже под покровом системы, которая больше не способна себя защитить. Это превращает атаку с помощью техник BYOVD в элегантный и крайне эффективный способ получения абсолютного доминирования над скомпрометированной системой.
Пример эксплуатации техники BYOVD: кража токена доступа
Помимо манипуляций с полями структуры _SEP_TOKEN_PRIVILEGES, атакующие могут подменить собственный Access Token на токен другого, более привилегированного процесса. Для этого они ищут процессы, запущенные, например, от имени ученой записи System, читают их _EPROCESS ->Token и перезаписывают этим значением токен в собственной структуре _EPROCESS.
По аналогии с техникой манипуляции привилегиями посмотрим, как выглядит кража токена с помощью отладчика.

Путем несложных манипуляций в отладчике нам удалось присвоить процессу cmd.exe токен процесса System.
Для реализации данной схемы атаки достаточно иметь уязвимый драйвер, который имеет примитивы для произвольного чтения и записи данных по произвольному адресу. Атакующий сможет использовать эти примитивы для получения указателя на _EPROCESS другого процесса, например, антивируса или процесса System (примитив чтения). Затем он копирует указатель на Token в «собственный процесс» (примитив записи).
Коммуникация эксплойта с уязвимым драйвером
Как уже было сказано ранее, атаки типа «принеси свой уязвимый драйвер» (BYOVD) базируются на использовании легитимного, подписанного драйвера для выполнения вредоносного кода в режиме ядра. Ключевым этапом этой техники является установление коммуникационного канала между вредоносной программой, работающей в пользовательском режиме (эксплойтом), и уязвимым драйвером, работающим в режиме ядра.
Взаимодействие между эксплойтом и уязвимым драйвером в операционной системе Windows происходит по стандартизированному механизму ввода-вывода (I/O), который включает следующие этапы:
- Инициализация драйвера и создание коммуникационного устройства
Процесс взаимодействия начинается, когда уязвимый драйвер загружается в память ядра. В ходе своей инициализации функция DriverEntry создает коммуникационное устройство (device) и символическую ссылку.
Драйвер вызывает функцию IoCreateDevice для создания объекта устройства (device), который представляет собой коммуникационное устройство в пространстве ядра.
После создания объекта device драйвер вызывает функцию IoCreateSymbolicLink, чтобы сделать это устройство доступным пользовательскому коду. Эта ссылка, как правило, имеет имя вида \??\

Кроме того, функция DriverEntry заполняет структуру DRIVER_OBJECT, в том числе инициализирует массив процедур диспетчеризации (Dispatch Routines). Эти процедуры являются входными точками (функциями-обработчиками) для различных типов запросов ввода-вывода, таких как чтение, запись или, что наиболее важно для BYOVD – IRP_MJ_DEVICE_CONTROL.

- Инициирование запроса из пользовательского режима
После того как драйвер готов к работе, вредоносный эксплойт, запущенный в пользовательском режиме, получает дескриптор объекта коммуникационного устройства (device), готовит структуру данных, которая будет передана в ядро и отправляет эту структуру данных.
Эксплойт вызывает функцию Windows API, чаще всего CreateFile, используя ссылку, созданную драйвером (например, \.<device-name>). В результате эксплойт получает дескриптор объекта коммуникационного устройства в ядре.

Далее эксплойт подготавливает необходимые структуры данных (например, аргументы для эксплуатации уязвимости) и код управления вводом-выводом (IOCTL Code), после чего вызывает функцию Windows API DeviceIoControl. Эта функция является ключевой: она упаковывает запрос пользовательского режима, включая IOCTL и буферы с данными, и передает его в ядро диспетчеру ввода-вывода.

- Обработка запроса диспетчером ввода-вывода
После вызова DeviceIoControl управление переходит в ядро, где его принимает менеджер ввода-вывода (I/O Manager), который формирует IRP пакет.
Диспетчер ввода-вывода получает запрос на обработку пользовательских данных и формирует структуру IRP (I/O Request Packet). Этот пакет является стандартным способом представления всех запросов I/O в ядре. Он содержит всю информацию о запросе, включая: тип операции, указатели на буферы пользовательского режима (входящие и исходящие данные) и IOCTL-код, переданный процессом пользовательского режима.
Диспетчер ввода-вывода считывает из IRP тип операции (DeviceIoControl -> IRP_MJ_DEVICE_CONTROL) и использует его как индекс для вызова соответствующей процедуры диспетчеризации, адрес которой был ранее инициализирован драйвером в структуре DRIVER_OBJECT.
- Обработка IRP запроса драйвером
После вызова соответствующего обработчика, управление переходит к основному коду уязвимого драйвера. Код драйвера приступает к обработке IRP-запроса и первым делом считывает ключевые параметры из структуры IRP (и дочерних структур):
MajorFunction – тип процедуры диспетчеризации: получение/закрытие дескриптора (IRP_MJ_CREATE/IRP_MJ_CLOSE), операции чтения/записи (IRP_MJ_READ/IRP_MJ_WRITE), обработка запроса с специфической логикой (IRP_MJ_DEVICE_CONTROL).
IoControlCode – IOCTL-код, который указывает, какую конкретную внутреннюю функцию должен выполнить драйвер.
SystemBuffer – указатель на буфер, содержащий данные, подготовленные и переданные эксплойтом из пользовательского режима.
После получения параметров обработчик переходит к диспетчеризация внутренней функции. На основе считанного IoControlCode, код драйвера обычно использует конструкцию switch/case для вызова конкретной внутренней функции-обработчика. Именно здесь происходит эксплуатация уязвимого примитива (например, уязвимости чтения или записи ядерной памяти), используя специально подготовленные данные из SystemBuffer.

Таким образом, IOCTL-код действует как «команда», а SystemBuffer как «полезная нагрузка», позволяя эксплойту использовать легитимный канал I/O для запуска вредоносного кода с наивысшими привилегиями (привилегиями ядра).
Пример примитивов произвольного чтения и записи в память
В этом разделе мы рассмотрим несколько ядерных примитивов, позволяющих осуществить произвольное чтение из ядерной памяти, а также запись в память ядра. Все представленные примеры взяты из реальных драйверов с подтвержденными CVE. Однако из этичных соображений мы не будем раскрывать информацию об именовании этих драйверов и CVE.
Посмотрим на ядерную API MmMapIoSpace. Функция MmMapIoSpace выполняет критическую задачу: она отображает непрерывный блок физических адресов (как правило, принадлежащих аппаратному обеспечению) в виртуальное адресное пространство ядра. Это позволяет программистам драйверов взаимодействовать с регистрами или буферами различных периферийных устройств так, будто это обычная оперативная память, используя стандартные операции чтения и записи (например, memcpy).
Сама по себе функция MmMapIoSpace не наносит вреда и является легитимным API для работы с устройствами. Истинная опасность кроется в том, что за вызовом MmMapIoSpace в большинстве случаев следуют критические операции – чтение или запись в отображенную память.
Если уязвимый драйвер позволяет пользовательскому коду произвольно вызывать MmMapIoSpace (предоставляя атакующему контроль над физическим адресом и размером отображения), злоумышленник получает мощный примитив произвольного чтения/записи (Arbitrary Read/Write) в физическую память.
Используя этот примитив, атакующий может:
Найти физические адреса, выделенные для хранения важнейших структур (_EPROCESS -> Token) высокопривилегированных процессов.
Отобразить эти физические адреса в виртуальную память.
Записать (подменить) Access Token своего вредоносного процесса токеном высокопривилегированного процесса.
Посмотрим на то, как это может быть реализовано в уязвимом драйвере.

Вначале драйвер проверяет размеры входного (InputBufferLength) и выходного (OutputBufferLength) буферов, после чего вычисляет эффективный объем читаемых из памяти ядра данных (_bittest). Этот объем чтения строго ограничен – 1, 2, 4 или 8 байт. Далее, в зависимости от значения OutputBufferLength, драйвер читает данные из памяти ядра и записывает их в буфер SystemBuffer, завершает обработку IRP и передает назад менеджеру ввода-вывода, который, в свою очередь их передает приложению пользовательского режима.

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

Этот пример схож с предыдущим, за исключением того, что данные, переданные пользовательским приложением, записываются в память ядра. Таким образом, MmMapIoSpace выступает как ключ, который, будучи скомпрометирован, открывает полный доступ к физической памяти и открывает перед атакующим совершенно неограниченные возможности при манипуляции критическими структурами ядра, а также снижает вероятность быть обнаруженным в атакованной инфраструктуре.
Защита и противодействие
Для разработчиков программного обеспечения рекомендуем придерживаться следующих принципов:
Проверка входных данных – все данные, полученные от приложений пространства пользователя данные должны быть тщательно проверены, установлены их допустимые диапазоны, чтобы предотвратить их использование в злонамеренных целях.
Использование безопасных API – используйте безопасные версии API, которые предоставляют защиту от распространенных типов атак.
Проверка привилегий – драйвер должен иметь возможность аутентифицировать вызывающий процесс (например, по его цифровой подписи), а также проверить, имеет ли он право на выполнение запрошенных операций (например, с помощью проверки наличия определенных привилегий).
Регулярное тестирование на уязвимости – драйвер должен регулярно тестироваться на уязвимости внутренней командой безопасной разработки, чтобы выявить и устранить потенциальные проблемы.
Не стоит пренебрегать механизмами противодействия, разработанными Microsoft. Они направлены на блокировку эксплуатации техник BYOVD. Эти защитные меры создают многоуровневый барьер, затрудняющий как загрузку, так и использование уязвимых драйверов:
Основные механизмы противодействия BYOVD:
Kernel Mode Code Signing (KMCS) – проверка подписи всех драйверов, загружаемых в режим ядра. Этот механизм гарантирует, что только драйверы от доверенных разработчиков могут быть загружены. Атакующие вынуждены искать уязвимости только в подписанных, легитимных драйверах, что сужает вектор атаки.
Hypervisor-enforced Code Integrity (HVCI) изоляция и усиленная проверка целостности кода ядра, выполняемая гипервизором (Virtualization-Based Security, VBS). Данная мера повышает сложность эксплуатации, изолируя код ядра от потенциально скомпрометированных процессов. В сочетании с VBS может защищать от записи в критические области памяти ядра, делая примитивы произвольного чтения/записи (полученные через BYOVD) менее эффективными.
Kernel Patch Protection (KPP, или PatchGuard) – защита от несанкционированных изменений критических структур и кода ядра ОС Windows в реальном времени. PatchGuard блокирует попытки вредоносного кода (выполненного через уязвимый драйвер) напрямую изменить важные системные таблицы, хуки или код ядра, необходимые для повышения привилегий или обхода безопасности.
Code Integrity Policy (Windows Defender Application Control, WDAC) – динамический список, содержащий хэши или сертификаты заведомо уязвимых или вредоносных драйверов. Является прямым ответом на BYOVD. Позволяет администраторам превентивно заблокировать загрузку драйверов, уязвимости в которых стали известны и активно эксплуатируются, даже если они имеют действительную цифровую подпись.
В целом, защита от BYOVD сводится к двум ключевым стратегиям:
KMCS – это фильтр первого уровня, который отсеивает неподписанные или подделанные драйверы.
WDAC (Code Integrity Policy) – это «черный список» для уже идентифицированных плохих актеров, который аннулирует право легитимно подписанного, но уязвимого драйвера на загрузку. Это вынуждает атакующих постоянно искать новые, ещё не заблокированные драйверы. Стоит отметить, что использование данного механизма требует первоначальной настройки администраторами безопасности, а также регулярной поддержки политик в актуальном состоянии.
Даже если уязвимый драйвер успешно загружен и эксплойт смог получить примитив произвольного чтения/записи (например, через MmMapIoSpace), KPP и HVCI выступают как «последняя линия обороны». Они предотвращают использование этого примитива для внесения критических изменений в структуры ядра (например, для манипуляции токенами доступа), которые необходимы для финального захвата контроля над системой.
Таким образом, разработчики операционных систем помогают владельцам инфраструктур повысить свои шансы на обеспечение безопасности корпоративной информации, а от администраторов требуется лишь своевременно и правильно использовать данные механизмы.
Заключение
BYOVD – это серьезная угроза безопасности, которая требует комплексного подхода к защите. Понимание принципов работы этой техники, а также механизмов защиты ядра Windows, является ключевым для предотвращения атак. Разработчики драйверов должны придерживаться безопасных методов программирования, чтобы предотвратить создание новых уязвимостей. Постоянный мониторинг и обновление системы также важны для защиты от новых угроз.