
Driver Reverse & Exploitation
⚠️ Предупреждение: Этот проект носит строго образовательный и демонстрационный характер. Он не предназначен для использования в вредоносных целях. Цель — изучить методологию reverse engineering и этапы эксплуатации Windows-драйвера.
Здесь я объясняю подход, который я использовал для решения упражнения, предложенного d1rk(SaadAhla) https://github.com/SaadAhla, а именно reverse engineering и эксплуатацию легитимного подписанного драйвера, отсутствующего в блок-листах (HVCI, LOLBIN...). Доступна программа на C, позволяющая завершить любой активный процесс в системе через этот драйвер режима ядра (Kernel-mode Driver); ниже я подробно описываю, как она работает.

📃 Использование: DriverKiller.exe <nom_processus.exe> [-d]
Опция -d: позволяет удалить службу и драйвер из системы после эксплуатации.
На целевой машине должен быть включён режим тестовой подписи (testsigning mode), так как сертификат драйвера истёк.
Часть 1 — Reverse engineering:
В упражнении предоставляется файл .sys, названный по его SHA-256 хешу.
Первый шаг — открыть этот файл в IDA.
IDA доступна бесплатно. Достаточно зайти на сайт Hex-Rays, чтобы сгенерировать лицензию и скачать программу.
Начнём с просмотра IAT (Import Address Table) драйвера и поищем вызов нужного API: ZwTerminateProcess.
Двойным щелчком по ZwTerminateProcess IDA перенаправляет нас к скомпилированному коду этой функции. Выбрав запись и открыв перекрёстные ссылки (cross-references), мы получаем список функций драйвера, которые её вызывают.
Мы видим, что именно функция sub_12EF4, по смещению 1CE, использует ZwTerminateProcess. После двойного щелчка IDA показывает её скомпилированный код.
Декомпилированный код показывает вызовы ZwOpenProcess (открывает дескриптор целевого процесса) и ZwTerminateProcess (завершает процесс через этот дескриптор).
Согласно документации ZwOpenProcess (https://learn.microsoft.com/fr-fr/windows-hardware/drivers/ddi/ntddk/nf-ntddk-zwopenprocess), параметр ClientID — это указатель, задающий PID целевого процесса.
В строке выше ClientId.UniqueProcess инициализируется переменной v22. Она определена чуть выше:
v22 = (void )((_QWORD *)i + 10);
Чтобы понять это присваивание, нужно определить переменную i и поле +10.
Выше в этой функции виден вызов ZwQuerySystemInformation с параметром SYSTEM_PROCESS_INFORMATION. Также становится понятно, что i — итератор по записям этой структуры вместе с переменной v6.
Согласно документации ZwQuerySystemInformation (https://learn.microsoft.com/en-us/windows/win32/sysinfo/zwquerysysteminformation), эта функция возвращает массив с одной записью на каждый активный процесс в системе.
Структура SYSTEM_PROCESS_INFORMATION описана здесь: https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
KPRIORITY BasePriority;
HANDLE UniqueProcessId;
PVOID Reserved2;
ULONG HandleCount;
ULONG SessionId;
PVOID Reserved3;
SIZE_T PeakVirtualSize;
SIZE_T VirtualSize;
ULONG Reserved4;
SIZE_T PeakWorkingSetSize;
SIZE_T WorkingSetSize;
PVOID Reserved5;
SIZE_T QuotaPagedPoolUsage;
PVOID Reserved6;
SIZE_T QuotaNonPagedPoolUsage;
SIZE_T PagefileUsage;
SIZE_T PeakPagefileUsage;
SIZE_T PrivatePageCount;
LARGE_INTEGER Reserved7[6];
} SYSTEM_PROCESS_INFORMATION;
Напоминание: размеры некоторых типов в Windows x64
typedef struct _UNICODE_STRING {
USHORT Length; -> 2
USHORT MaximumLength; -> + 2 = 4
PWSTR Buffer; -> + 8 = 12 (12 n'est pas un multiple de 8 donc padding de 4 ajouté en amont de Buffer) = 16
} UNICODE_STRING;Расчёт смещения UniqueProcessId:
ULONG NextEntryOffset; -> 4
ULONG NumberOfThreads; -> + 4 = 8
BYTE Reserved1[48]; -> + 48 = 56
UNICODE_STRING ImageName; -> + 16 = 72
KPRIORITY BasePriority; -> + 4 = 76 (76 n'est pas un multiple de 8 donc padding de 4 ajouté) = 80
HANDLE UniqueProcessId; -> + 8 = 88
Член UniqueProcessId находится по смещению 0x50 (80 в десятичной системе).
Глядя на присваивание переменной v22, видно, что i приводится к указателю QWORD (8 байт):
v22 = (void )((_QWORD *)i + 10);
v22 соответствует адресу i + 10 * 8 = 80 байт. Эта переменная действительно содержит PID, полученный из структуры SYSTEM_PROCESS_INFORMATION.
Чтобы узнать, какой PID будет передан в ZwTerminateProcess, нужно проанализировать условие, окружающее это присваивание.
Видно, что сначала извлекается имя образа процесса:
v9 = (wchar_t )((_QWORD *)i + 8);
v9 = адрес i + 8 × 8 = 64 байта. Это соответствует полю Buffer члена ImageName, поскольку этот член находится по смещению 56 + 2 (USHORT) + 2 (USHORT) + 4 (padding) = 64.
Судя по операциям и циклам ниже, можно предположить, что выполняется сравнение имени процесса, переданного аргументом (a2), с активными процессами системы v9/String.
sub_1C078(String, v9, (int)v13); v17 = strupr(a2); v18 = strupr(String);
Значит, параметр a2 должен содержать имя процесса, который нужно завершить через ZwTerminateProcess.
Заметим, что a2 — параметр функции sub_12EF4. Чтобы продвинуться дальше, нужно изучить ссылки на эту функцию (я переименовал её в ZwTerminateProcessCaller для лучшей читаемости).
Видно, что ZwTerminateProcessCaller вызывается функцией sub_13624 по смещению 61A.
Прежде чем анализировать этот декомпилированный код, я найду ссылки на функцию sub_13624 (переименованную в ZwTerminateProcessCallerCaller), чтобы убедиться, что этот код действительно используется после вызова API DeviceIoControl из UserMode.
Видно, что ZwTerminateProcessCallerCaller вызывается функцией sub_14130 (переименованной в ZwTerminateProcessCallerCallerCaller ...к счастью для нас, это последняя перед точкой входа 😅).
Видно, что ZwTerminateProcessCallerCallerCaller вызывается функцией sub_1A4A8 по смещению 306.
Здесь находится присваивание функции ZwTerminateProcessCallerCallerCaller:
memset64(DriverObject->MajorFunction, (unsigned __int64)ZwTerminateProcessCallerCallerCaller, 0x1Cu);
Прежде чем вернуться к функции sub_13624 (она же ZwTerminateProcessCallerCaller), получаем Symbolic Name и Device Name (здесь они совпадают): Viragtlt.
Возвращаясь к ZwTerminateProcessCallerCaller, замечаем, что её второй параметр (то есть a2) соответствует MasterIrp->AssociatedIrp.SystemBuffer.
Сразу над вызовом ZwTerminateProcessCaller находится IOCTL-код: -2106392528 (в шестнадцатеричном виде: 0x82730030).
Благодаря этой информации можно сделать вывод, что для эксплуатации этого драйвера нужно отправить драйверу вызов API DeviceIoControl с именем завершаемого процесса в SystemBuffer.
🔷 Информация, полученная в ходе reverse engineering:
0x82730030ViragtltViragtltЧасть 2 — Эксплуатация
Чтобы эксплуатировать этот драйвер (если он установлен и активен на целевой машине), необходимо открыть к нему дескриптор, а затем выполнить вызов API DeviceIoControl с буфером, содержащим имя процесса, который требуется завершить.
Для этого упражнения я разработал проект на C, который:
Я также добавил опцию -d, которая позволяет удалить службу и драйвер из системы после эксплуатации.
Вот поведение программы на C в полном цикле выполнения:
Обход AV/EDR
В данном случае DriverKiller.exe не обнаруживается Microsoft Defender — ни статически, ни динамически. Обход здесь не имеет особого смысла, потому что эксплуатируемый драйвер имеет истёкший сертификат, поэтому его использование в реальных условиях трудно представить. Но для лучшей скрытности можно было бы реализовать:
Обнаружение драйвера на 29/08/2025 (результат уже существовал, я ничего не отправлял на VirusTotal по очевидным причинам):
⚠️ Этот проект создан в учебных целях. Он может содержать неточности или ошибки. Любые предложения, исправления и обсуждения приветствуются! 😃 Спасибо d1rk(SaadAhla): https://github.com/SaadAhla !