Технология ELAM: как детектируются уязвимые драйверы Windows

от автора

Введение

На дворе 2026 год, технологии развиваются, а хакеры становятся сильнее и изобретают новые способы обхода средств безопасности. Один из таких методов, который уже стал классикой в 2026 году — это BYOVD‑техника (Bring Your Own Vulnerable Driver).

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

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

Используется данная техника потому, что современные антивирусы стали слишком сильными. Если посмотреть под капот любому актуальному хорошему защитнику, мы увидим там как минимум 3–4 собственных драйвера, которые контролируют процессы, следят за подключением разных физических устройств (USB), следят за тем, чтобы вредоносный процесс не смог проэксплуатировать какую‑то уязвимость (например, подмена токена у системного процесса на свой).

Исходя из всего, что я описал выше, возникает логичный вопрос: как мы можем пресекать способы эксплуатации системы через BYOVD‑драйверы? Ответ на этот вопрос прост — нам нужна технология ELAM (поправка, детектирует только при загрузке).

ELAM

ELAM — это технология, которая появилась в Windows 8, она предназначена для раннего запуска определенного загрузочного драйвера еще до полной инициализации большинства драйверов системы. Благодаря данной технологии антивирусное ПО получает возможность анализировать первые загружаемые драйверы практически в самом начале загрузки ОС. После того как ELAM‑драйвер получит информацию о первом загружаемом драйвере, он имеет право проверить такие данные как:

1. Хэш драйвера. ELAM‑драйвер сверяет хэш проверяемого компонента с базой хэшей уязвимых и вредоносных драйверов, которые находятся в черном списке, данный метод эффективен относительного второго, так как изменение имени файла или его метаданных не позволяет обойти блокировку — если хэш совпадает со списком запрещенных, драйвер не загрузится.

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

Исходя из вышеперечисленной информации, возникает вопрос: каким образом ELAM‑драйвер получает информацию о загрузке других драйверах системы? Ответ заключается в использовании механизма функций обратного вызова (каллбека).

В момент своей инициализации ELAM‑драйвер регистрирует функцию обратного вызова с помощью API‑функции IoRegisterBootDriverCallback, которая вызывается каждый раз, когда в систему загружаются новый драйвер. Данная функция инициализируется ELAM‑драйвером в первую очередь после непосредственно своей инициализации. Прототип данной функции выглядит следующим образом:

PVOID IoRegisterBootDriverCallback(  [in]           PBOOT_DRIVER_CALLBACK_FUNCTION CallbackFunction,  [in, optional] PVOID                          CallbackContext);
Вызов функции обратного вызова в настоящем ELAM-драйвере.

Вызов функции обратного вызова в настоящем ELAM‑драйвере.

Теперь ознакомимся с каждым параметром данной функции, а именно:

1. CallbackFunction — параметр является указателем на функцию обратного вызова типа BOOT_DRIVER_CALLBACK_FUNCTION

2. CallbackContext — параметр отвечает за определение контекста, который будет передан в функцию.

Структура же из первого параметра выглядит следующим образом:

BOOT_DRIVER_CALLBACK_FUNCTION BootDriverCallbackFunction;VOID BootDriverCallbackFunction(  PVOID CallbackContext,  BDCB_CALLBACK_TYPE Classification,  PBDCB_IMAGE_INFORMATION ImageInformation){...}

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

— Первый параметр представляет собой указатель на контекст, заданный разработчиком при регистрации.

— Второй параметр указывает на тип события, происходящего в системе на данном этапе загрузки. 

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

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

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

typedef struct _BDCB_IMAGE_INFORMATION {    BDCB_CLASSIFICATION_TYPE ClassificationType;    ULONG ImageFlags;    UNICODE_STRING ImageName;    UNICODE_STRING RegistryPath;    UNICODE_STRING CertificatePublisher;    UNICODE_STRING CertificateIssuer;    PVOID ImageHash;    ULONG ImageHashLength;    PVOID CertificateThumbprint;    ULONG CertificateThumbprintLength;} BDCB_IMAGE_INFORMATION, *PBDCB_IMAGE_INFORMATION;

Разберем самые важные поля данной структуры, а именно:

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

BdCbClassificationKnownGood (0) — данный тип присваивается драйверу, в случае если он прошел все этапы верификации, обладает валидной подписью от Microsoft, а также отсутствует в базах антивирусного ПО и на LoLDrivers;
BdcbClassificationKnownBad (1) — данный тип присваивается драйверу, в случае если хэш‑сумма, имя или сертификат проверяемого драйвера совпадает сведениями из черного списка вредоносного или гарантированно уязвимого ПО;
BdcbClassificationKnownBadBootCritical (2) — данный тип присваивается драйверу, в случае если идентифицированному как уязвимый или поврежденный, но при этом помеченному в системе как критически важный для процесса загрузки, если полностью заблокировать такой драйвер, операционная система завершит работу с критической ошибкой (BSOD);
BdcbClassificationUnknown (3) — данный тип присваивается драйверам, о которых у защитного ПО полностью отсутствуют подтвержденные данные как в списках доверенных, так и в списках запрещенных компонентов.

2. ImageFlags — данное поли содержит битовые флаги, которые передаются ОС и описывают дополнительные характеристики загружаемого образа, данные флаги позволяют ELAM‑драйверу получить контекст о природе исполняемого файла еще до начала его анализа, например6 указывает ли система, что данный образ является полноценным драйвером ядра или же он относится к категории критических системных компонентов;

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

4. RegistryPath — это строковое поле типа UNICODE_STRING, указывающее точный путь к разделу реестра, через который данный драйвер был зарегистрирован в операционной системе (обычно это ветка внутри HKLM\SYSTEM\CurrentControlSet\Services). Анализ этого пути позволяет ELAM‑драйверу проверить легитимность конфигурации службы драйвера и убедиться, что параметры запуска не были подменены злоумышленниками на этапе подготовки к BYOVD‑атаке. Сверка параметров в реестре с реальными характеристиками файла на диске обеспечивает дополнительный уровень верификации загружаемого компонента;

5. CertificatePublisher и CertificateIssuer — данные поля содержат текстовую информацию об издателя и поставщике цифрового сертификата, которым подписан проверяемый драйвер;

6. ImageHash и ImageHashLength — связка из указателя на буфер данных и переменной, определяющей его длину, которые содержат чистый хэш‑сумма (цифровой отпечаток) проверяемого бинарного файла.

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

NTSTATUS IoUnregisterBootDriverCallback(    PVOID Handle);

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

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

VOID UnregisterCallback(){    if (g_CallbackHandle)    {        IoUnregisterBootDriverCallback(g_CallbackHandle);        g_CallbackHandle = NULL;    }}

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

ELAM‑драйвер на практике. Программируем на С++

Непосредственно пора бы уже переходить к практической реализации подобного драйвера. Для разработки нам понадобится фреймворк WDK (Windows Driver Kit) и среда разработки Visual Studio с установленным компонентом C++ WDK Desktop Tasks.

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

extern "C" NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath){    UNREFERENCED_PARAMETER(RegistryPath);      DriverObject->DriverUnload = DriverUnload;    KdPrint(("ELAM driver loaded!\n\n"));    g_CallbackRegistrationHandle = IoRegisterBootDriverCallback(HabrElam, NULL);    if (g_CallbackRegistrationHandle == NULL)    {        KdPrint(("ELAM driver start error...\n\n"));        g_CallbackRegistrationHandle = NULL;        return STATUS_UNSUCCESSFUL;    }    return STATUS_SUCCESS;}

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

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

1. Мы извлекаем структуру BDCB_IMAGE_INFORMATION и анализируем хэш‑сумму драйвера;
2. Сверяем его имя и цифровую подпись с локальными списками известных драйверов, после чего принудительно изменяем поле классификации для блокировки вирусных объектов.

Реализуем же эту функцию! Для начала начнём сверять хэши:

VOID HabrElam(_In_ PVOID CallbackContext, _In_ BDCB_CALLBACK_TYPE Classification, _Inout_ PBDCB_IMAGE_INFORMATION ImageInformation){    UNREFERENCED_PARAMETER(CallbackContext);    if (Classification == BdCbInitializeImage)    {        PBDCB_IMAGE_INFORMATION StatusData = (PBDCB_IMAGE_INFORMATION)ImageInformation;        ImageInformation->Classification = BdCbClassificationKnownGoodImage;        if (StatusData->ImageHash != NULL && StatusData->ImageHashLength == SHA256_HASH_SIZE)        {            for (ULONG i = 0; i < CERT_BLACK_LIST_COUNT; i++)            {                if (RtlCompareMemory(StatusData->ImageHash, CERT_BLACK_LIST[i], SHA256_HASH_SIZE) == SHA256_HASH_SIZE)                {                    KdPrint(("driver blocked by blacklist hash match at %lu\n", i));                    ImageInformation->Classification = BdCbClassificationKnownBadImage;                    return;                }            }        }}

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

Основной цикл верификации осуществляет последовательный перебор локального массива хэш‑сумм, содержащего цифровые отпечатки уязвимых драйверов (приводил пример известных скомпрометированных версий из базы данных LoLDrivers). В случае побитового совпадения данных с помощью стандартной функции RtlCompareMemory, драйвер немедленно классифицируется как гарантированно вредоносный путем присвоения статуса BdCbClassificationKnownBad, в результате этого выполнение функции прерывается, предотвращая дальнейшую инициализацию уязвимого драйвера. Сам же массив выглядит следующим образом:

const UCHAR CERT_BLACK_LIST[CERT_BLACK_LIST_COUNT][SHA256_HASH_SIZE] = {    // RTCore64.sys    { 0x01, 0x54, 0x0c, 0x8a, 0x3d, 0x5e, 0x3e, 0x93, 0x59, 0xb6, 0xcc, 0xd0, 0x51, 0x22, 0xc1, 0x73,      0x4e, 0x6e, 0xf6, 0x7b, 0x4a, 0x0a, 0x05, 0x50, 0x5c, 0x5b, 0x98, 0x1f, 0x48, 0x9b, 0xa1, 0xbc },      // gdrv.sys    { 0xdc, 0x2d, 0x75, 0x3b, 0xa5, 0xea, 0x50, 0xec, 0xb5, 0x4d, 0x60, 0x6e, 0x88, 0x9a, 0x24, 0x69,    0x1c, 0x8d, 0x75, 0xee, 0x54, 0x8c, 0x01, 0x90, 0x9c, 0xde, 0xda, 0x98, 0x20, 0x03, 0x90, 0x22 },        // procexp.sys    { 0x5c, 0x01, 0x5e, 0x18, 0xb2, 0xd3, 0xee, 0x49, 0xd0, 0x83, 0xcb, 0xee, 0xb0, 0xb5, 0xbc, 0x6b,    0xbb, 0xd8, 0xfc, 0xe4, 0xbf, 0x4c, 0x26, 0x30, 0x53, 0x47, 0xfd, 0x20, 0x10, 0x7a, 0x94, 0x8a }};

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

Верификация по имени файла реализуется через анализ строки, содержащейся в поле ImageName структуры BDCB_IMAGE_INFORMATION, которую мы рассматривали ранее. Для начала в header‑файле объявим массив имен драйверов, которые напрочь запрещено инициализировать в системе:

const ULONG DRIVER_BLACK_LIST_COUNT = 10;const UNICODE_STRING DRIVER_BLACK_LIST[DRIVER_BLACK_LIST_COUNT] = {    RTL_CONSTANT_STRING(L"RTCore64.sys"),       // чтение/запись физической памяти    RTL_CONSTANT_STRING(L"gdrv.sys"),           // выполнение кода в ring 0    RTL_CONSTANT_STRING(L"dbutil_2_3.sys"),     // раскрытие данных ядра    RTL_CONSTANT_STRING(L"procexp.sys"),        // уязвимые старые версии    RTL_CONSTANT_STRING(L"AsIO.sys"),           // прямой доступ к портам ввода-вывода    RTL_CONSTANT_STRING(L"ENETechIo64.sys"),    // эксплуатация уязвимостей в RGB-софте    RTL_CONSTANT_STRING(L"DirectIo.sys"),       // компонент сторонних утилит для прямого маппинга памяти    RTL_CONSTANT_STRING(L"360hvm64.sys"),       // уязвимый драйвер от Qihoo 360    RTL_CONSTANT_STRING(L"PhntDrv.sys"),        // уязвимые версии компонентов системного мониторинга    RTL_CONSTANT_STRING(L"mhyprot2.sys")        // Genshin Impact Anti-Cheat драйвер};

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

VOID HabrElam(_In_ PVOID CallbackContext, _In_ BDCB_CALLBACK_TYPE Classification, _Inout_ PBDCB_IMAGE_INFORMATION ImageInformation){    UNREFERENCED_PARAMETER(CallbackContext);    if (Classification == BdCbInitializeImage)    {        PBDCB_IMAGE_INFORMATION StatusData = (PBDCB_IMAGE_INFORMATION)ImageInformation;        ImageInformation->Classification = BdCbClassificationKnownGoodImage;        if (StatusData->ImageHash != NULL && StatusData->ImageHashLength == SHA256_HASH_SIZE)        {            for (ULONG i = 0; i < CERT_BLACK_LIST_COUNT; i++)            {                if (RtlCompareMemory(StatusData->ImageHash, CERT_BLACK_LIST[i], SHA256_HASH_SIZE) == SHA256_HASH_SIZE)                {                    KdPrint(("driver blocked by blacklist hash match at %lu\n", i));                    ImageInformation->Classification = BdCbClassificationKnownBadImage;                    return;                }            }        }        if (ImageInformation->ImageName.Buffer != NULL && ImageInformation->ImageName.Length > 0)        {            for (ULONG i = 0; i < DRIVER_BLACK_LIST_COUNT; i++)            {                if (RtlFindUnicodeStringString(&ImageInformation->ImageName, &DRIVER_BLACK_LIST[i]) != NULL)!= NULL)                {                    KdPrint(("driver blocked by blacklist name match  %wZ\n", &DRIVER_BLACK_LIST[i]));                    ImageInformation->Classification = BdCbClassificationKnownBadImage;                    return;                }            }        }    }}

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

FASM.

FASM.

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

VOID DriverUnload(_In_ PDRIVER_OBJECT DriverObject){    UNREFERENCED_PARAMETER(DriverObject);        IoUnregisterBootDriverCallback(g_CallbackRegistrationHandle);    g_CallbackRegistrationHandle = NULL;    KdPrint(("elam unregistred callback.\n"));}

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

IAT.

IAT.

Как подписываются ELAM‑драйверы?

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

1. ELAM‑драйвер должен пройти обязательную процедуру отправки в Microsft через портал оборудования, обычный сертификат подписи кода не подойдет, Microsoft должны самостоятельно подписать бинарный файл своим специальным корневым сертификатом раннего доступа.

2. Процедура получения такой подписи жестко привязана к участию разработчика в программе Microsoft Virus Initiative (MVI). Это означает, что независимый разработчик или обычная софтверная компания не могут просто так отправить любой драйвер на подпись как ELAM. Вендор обязан официально подтвердить статус разработчика антивирусного программного обеспечения, пройти аудит безопасности и доказать, что компонент действительно выполняет функции защиты системы, а не используется как потенциальный бэкдор, который, наоборот, борется с этими защитниками.

Заключение

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

ссылка на оригинал статьи https://habr.com/ru/articles/1056154/