
Драйвер: реверс и эксплуатация
⚠️ Предупреждение: Этот проект носит строго образовательный и демонстрационный характер. Он не предназначен для использования в вредоносных целях. Цель — изучить методологию 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.