Saltar a contenido

Clase 4 — Debugging en vivo del overflow KSppNpFr y reconstrucción de la estructura de pipes en npfs.sys

← Volver al Módulo 6 · ← Clase 3

Resumen: esta clase retoma el dispatch hasta CKSAutomationThunk::ThunkEnableEventIrp y lo recorre paso a paso en el kernel debugger, confirmando en runtime cada cosa que en clases anteriores se había deducido estáticamente: el Current Stack Location en IRP+0xB8, el integer overflow de OutputBufferLength, el chequeo roto del size, la allocation KSpp de 0x1000, el memmove que desborda hacia el chunk NpFr siguiente y el pisado de DataSize a 0x4000. Después se cambia a npfs.sys, se muestra cómo encontrar dónde alocan las pipes (PoolHitTag, breakpoints por proceso) y se reconstruye la estructura NP_DATA_QUEUE_ENTRY (header de 0x30 bytes) usando ReactOS, identificando que el campo objetivo es DataSize y que el IRP guardado se pone a cero antes de salir (por eso hace falta una "trampita" extra para leakear).


Tabla de Contenidos

  1. Contexto de la clase
  2. Reconstruir el dispatch y fijar la dirección de salto
  3. Argumentos del dispatch: todos punteros a cero
  4. Símbolos: ksthunk.sys completo vs npfs.sys pelado
  5. Las dos condiciones de entrada: 32 bits + RequestorMode User
  6. Current Stack Location en IRP+0xB8 y corrección de la union
  7. Los dos sizes de entrada/salida
  8. El integer overflow paso a paso
  9. El MasterIRP reusado y el caso METHOD_NEITHER
  10. ProbeForRead dentro del try/except
  11. El switch case del Type3InputBuffer
  12. El chequeo de size roto
  13. La allocation KSpp de 0x1000
  14. El output buffer en dirección fija con VirtualAlloc
  15. El memmove vulnerable y el crash controlado
  16. Confirmación con !pool: KSpp seguido de NpFr
  17. Heap estándar vs Low Fragmentation Heap
  18. Encontrar dónde aloca npfs.sys: PoolHitTag y breakpoints por proceso
  19. La allocation real ocurre en WriteFile, no en CreateNamedPipe
  20. Reconstruir NP_DATA_QUEUE_ENTRY con ReactOS
  21. El objetivo: pisar DataSize y el problema del IRP puesto a cero
  22. Detectar el pipe overflowdeado con PeekNamedPipe
  23. Plan para la próxima: doble debugging y NtFsControlFile
  24. Workflow mental completo
  25. Cheat sheet
  26. Glosario corto

1. Contexto de la clase

La clase arranca exactamente donde quedó la anterior: en el despacho del IOCTL, justo antes de entrar a la función vulnerable.

A diferencia de las clases más estáticas, acá el foco es verificar en vivo en el kernel debugger todo lo deducido antes. La frase guía del instructor:

Vamos a llegar hasta el memmove y chequear bien, debagueando,
a ver si más o menos estábamos en lo correcto.

La cadena ya conocida:

CKernelFilterDevice::DispatchIrp
  -> CKSThunkDevice::DispatchIoctl   (IRP_MJ_DEVICE_CONTROL)
  -> IOCTL 0x2F0007
  -> CKSAutomationThunk::ThunkEnableEventIrp  (vulnerable)
  -> integer overflow + ExAllocatePool2 (KSpp, 0x1000)
  -> memmove overflow -> chunk NpFr siguiente
  -> DataSize del pipe pasa a 0x4000

2. Reconstruir el dispatch y fijar la dirección de salto

Para no perder el hilo entre DispatchIrp y DispatchIoctl, se usa un truco del plugin: forzar la dirección de destino del call para poder saltar directo con el cursor.

Edit -> Plugins -> Change call address
   -> poner la dirección con prefijo 0x (si no, IDA no la acepta)

Así, desde el dispatch genérico se salta directamente a CKSThunkDevice::DispatchIoctl y se ven los argumentos que vienen heredados desde el DispatchIrpBridge.


3. Argumentos del dispatch: todos punteros a cero

Al llegar a DispatchIoctl, los argumentos son:

Argumento Valor Origen
filter_device puntero al device viene de la referencia del bridge
IRP puntero al IRP el IRP real
arg3 0 tercer argumento del bridge, era 0
status1 puntero a 0 status = status1 = 0

Detalle importante que se confirma siguiendo referencias:

status  = status1
status1 = 0
=> todos estos son punteros a cero al entrar

El return status puede cambiar después, pero al llegar acá todo es cero. Renombrarlos como 0/constante 0 evita confusión durante el trace.


4. Símbolos: ksthunk.sys completo vs npfs.sys pelado

Un punto pedagógico que se repite: la calidad de los símbolos no es uniforme en el kernel.

Driver Calidad de símbolos
ksthunk.sys bastante completos: clase, método y tipos de argumentos
npfs.sys sólo nombres de función, sin args ni tipos, "más triste que nada"
ksthunk.sys -> CKSAutomationThunk::ThunkEnableEventIrp(int, IRP*, ...)
npfs.sys    -> NpFooBar           (nombre y chau)

Esto anticipa por qué para npfs.sys habrá que apoyarse en ReactOS para reconstruir estructuras.


5. Las dos condiciones de entrada: 32 bits + RequestorMode User

Antes de llegar al IOCTL, el código chequea dos condiciones que tienen que cumplirse:

if (proceso es de 32 bits) && (RequestorMode == User)
    -> entra a evaluar los IOCTLs

Recordatorio de RequestorMode:

Valor Significado
1 UserMode
0 KernelMode

Sólo si se dan ambas (proceso WOW64 de 32 bits y request desde user) se enrutan los IOCTLs, y 0x2F0007 es el que lleva a la función vulnerable.


6. Current Stack Location en IRP+0xB8 y corrección de la union

Regla mnemotécnica que se repite en clase:

cuando ves B8, ya sabés que es el Current Stack Location

El campo en el IRP aparece primero como:

IRP->Tail.Overlay.CurrentStackLocation   (offset 0xB8)

En IDA sale primero como IRP_Tail. El truco para que aparezca bien:

clic derecho -> Force offset field -> volver a aplicar
   => ahora muestra CurrentStackLocation

Y como estamos en un IOCTL, la union del IO_STACK_LOCATION está mal resuelta por defecto. Hay que cambiarla a la variante correcta:

union -> Parameters.DeviceIoControl

IDA avisa que las unions no las puede resolver solo ("me dice que las uniones no las puede resolver, ya sabe eso"), por eso se corrige a mano.


7. Los dos sizes de entrada/salida

Una vez aplicada la union DeviceIoControl, aparecen los dos campos clave:

Campo Valor pasado por el PoC Nombre puesto en IDA
InputBufferLength 0x1000 size_input
OutputBufferLength 0xFFFFFFF0 size_output

Origen de esos valores en user-mode:

CreateFile -> ... (pipes/spray) ... -> Trigger
Trigger -> DeviceIoControl(hDevice, IOCTL=0x2F0007,
                           inputBuffer,  inputSize  = 0x1000,
                           outputBuffer, outputSize = 0xFFFFFFF0,
                           &returned)

El size_input = 0x1000 es correcto y benigno. El problema está en el size_output = 0xFFFFFFF0, que es el que va a provocar el integer overflow.


8. El integer overflow paso a paso

El cálculo que rompe todo:

size_output = 0xFFFFFFF0
tmp = size_output + 0x17     ; 0xFFFFFFF0 + 0x17 = 0x100000007 -> truncado a 32 bits = 0x7
size = tmp & 0xFFFFFFF8       ; 0x7 & 0xFFFFFFF8 = 0x0

Confirmado en runtime:

le suma 17  -> queda 7
AND 0xFFFFFFF8 -> queda 0
=> size = 0

Ese size = 0 es el tamaño "alineado" que después se usará para la allocation, y es justamente lo que hace que el chunk quede demasiado chico respecto a lo que el memmove va a copiar.


9. El MasterIRP reusado y el caso METHOD_NEITHER

Hay un chequeo temprano:

if (IRP->AssociatedIrp.MasterIrp != 0)
    -> sale

Por qué pasa el chequeo: el IOCTL es METHOD_NEITHER ("Nader"), y en ese método el campo MasterIrp no se usa, así que vale 0.

En METHOD_BUFFERED ese campo apunta al system buffer (distinto de cero) y la función saldría; en METHOD_NEITHER queda en cero, por eso sigue.

Lo interesante: como el campo no se usa en METHOD_NEITHER, el driver lo reutiliza como variable temporal:

MasterIrp = puntero a la allocation KSpp   ; lo usa como scratch
MasterIrp->flags + 0x4 = case              ; guarda ahí el case del switch

Aclaración de la clase sobre por qué UserBuffer sirve de entrada y salida:

en METHOD_NEITHER:
  Type3InputBuffer -> buffer de entrada
  UserBuffer       -> buffer de salida (y también se lee de él)
  MasterIrp        -> campo no usado -> reutilizado como scratch

10. ProbeForRead dentro del try/except

El driver hace ProbeForRead sobre el buffer de entrada, pero hay un detalle clave en cómo funciona ProbeForRead:

ProbeForRead NO chequea todos los bytes.
Chequea que el inicio esté en user
y que (inicio + size) siga dentro de user.

Consecuencias:

  • Con size_input = 0x1000 sobre un buffer real de user, pasa sin problema.
  • Más adelante, al probar el output con un size gigante (0xFFFFFFF0), tampoco falla, porque al ser un proceso de 32 bits la suma no cruza al espacio de kernel de 64 bits. Sólo verifica que la dirección exista y sea accesible, no recorre el rango.

Todo esto vive dentro de un try/except:

__try {
    ... ProbeForRead ...
    ... memmove ...
} __except {
    -> raise / salir limpio, sin Blue Screen
}

Por eso, cuando el memmove se pase de rango, no hay BSoD: salta al except y la función retorna.


11. El switch case del Type3InputBuffer

El valor que decide el camino sale del buffer de entrada controlado por el atacante:

val = *(Type3InputBuffer + 5)   ; offset por interpretación del puntero
                                ; (toma como si fuera 8 bytes -> 5*8 = 0x28 -> input+0x10)
val == 4                        ; el case observado

Antes del switch hay un bit-test:

BTR eax, 0x1C    ; testea el bit 28 -> está en 0 -> no pasa nada (sólo setea ZF)

Y después la cadena de restas típica de un switch:

eax = 4
eax-1 = 3
eax-2 = ... -> llega al case que nos lleva al camino vulnerable

La conclusión: ese 4 es un valor que domina el atacante, y sólo decide por qué rama del switch se va. Conviene renombrarlo en IDA (no acepta case, sí acepta caso).


12. El chequeo de size roto

Acá está el filtro que debería atajar el overflow pero no lo hace:

R13 = size_output            ; 0xFFFFFFF0
cmp R13, 0x10
jb  ...                      ; si fuera menor saltaría -> no salta, pasa

eax = R13 + 0x10             ; 0xFFFFFFF0 + 0x10 = 0x100000000 -> truncado = 0x0
R15 = eax                    ; R15 = 0
cmp size_input(0x1000), R15  ; 0x1000 vs 0 -> no es menor -> pasa

El instructor lo resume sin vueltas:

el size real es 0xFFF..., pero le suma 0x10 y se hace chico (0),
después lo compara con 0 -> pasa. Es un filtro mal hecho. Una boludez.

Ese wraparound es el corazón del bug: el chequeo cree que el output es chiquito.


13. La allocation KSpp de 0x1000

Llega la allocation con ExAllocatePool2:

PoolType = 0x97              ; NonPagedPool + flags (suma de varias cosas)
Size     = 0 + size_input    ; 0 + 0x1000 = 0x1000
Tag      = 'KSpp'            ; 0x7070534B little-endian

El puntero resultante se guarda en el MasterIrp reusado. Confirmación en runtime:

copy value -> !pool <addr>
=> tag KSpp, size 0x1000

Este es el chunk que se va a desbordar: se aloca 0x1000 pero el memmove va a copiar mucho más.


14. El output buffer en dirección fija con VirtualAlloc

Del lado del PoC, el output buffer no es cualquier dirección: se reserva con VirtualAlloc en una dirección baja y fija.

outputBuffer = VirtualAlloc(dirección fija baja, ...)
output size  = 0x1060 + ...  -> alineado a 0x2000

La cuenta vista en clase:

0x1060 + (resto)  -> alinea -> 0x2000
=> el tamaño efectivo del output buffer es 0x2000

Por qué dirección fija y baja: para que dirección + size_gigante no cruce a kernel y ProbeForRead no falle. El buffer se llena con 0x41 ("está todo lleno de 41").

El pico del payload se arma al final del buffer:

override_data_ptr = outputBuffer + 0x1000 - 0x10   ; = outputBuffer + 0xFF0

Y ahí, en la zona que va a caer sobre el header del pipe siguiente, se escribe el valor objetivo:

OB size = 0x4000   ; este es el que va a pisar el DataSize del pipe vecino

15. El memmove vulnerable y el crash controlado

Hay más de un memmove en la función; hay que identificar el que produce el overflow mirando el destination.

memmove "inocuo":  dst = RDX (+0x10)   ; no es el overflow
memmove vulnerable: dst = RCX          ; chunk + 0x20, cae sobre el NpFr siguiente
                    src = UserBuffer
                    size = R8 (largo)

El RCX apunta a la región ...F020, es decir el chunk que está a continuación del KSpp — el pipe NpFr.

Al dar Run, el comportamiento esperado:

el memmove copia desde UserBuffer (source de 0x2000)
cuando se queda sin source (>0x2000) -> excepción
-> salta al except -> la función sale
-> NO hay Blue Screen

El instructor lo enfatiza: esto no se hace una sola vez, el PoC repite el trigger muchísimas veces porque hay un montón de llamadas que hacen exactamente este overflow controlado.


16. Confirmación con !pool: KSpp seguido de NpFr

Parados en el kernel, se confirma el layout:

!pool <chunk>
=> chunk actual:    tag KSpp, size 0x1000
   chunk + 0x1000:  tag NpFr, size 0x1000   ; el pipe

Modelo:

0x...6000  [KSpp, size 0x1000]   <- allocation vulnerable
0x...7000  [NpFr, size 0x1000]   <- pipe (data entry)

Esto valida toda la estrategia: el spray de pipes dejó un NpFr justo después del hueco donde cayó el KSpp, y el memmove desde KSpp+0x20 sale del chunk y pisa el header del NpFr.

Nota: el size del pipe también es 0x1000. Un spray de pipes de 0x1000 siempre cae en el mismo bucket de size, lo que hace el layout reproducible.


17. Heap estándar vs Low Fragmentation Heap

La clase explica por qué este grooming es predecible:

al entrar a la función que aloca:
  if (size < 0x200) -> probablemente Low Fragmentation Heap (LFH)
  if (size >= 0x200) -> Heap estándar
Tipo Tamaños Comportamiento
LFH chicos (~0xF0 y similares) random, buckets de N bytes, difícil de predecir
Heap estándar grandes (0x1000) secuencial, muy predecible

Como 0x1000 >= 0x200, las allocations van al heap estándar y son secuenciales:

1000, 2000, 3000, 4000, 5000, 6000, 7000...  (de 0x1000 en 0x1000)

Por eso la receta funciona:

1. armar un array de pipes (llenar)
2. liberar uno del medio -> queda un hueco predecible
3. el KSpp cae en ese hueco
4. el NpFr queda justo a continuación

Con LFH habría que llenar mucho, hacer muchos agujeros y meter el propio en alguno ("es como un queso"). Con heap estándar, un solo free deja un hueco predecible.


18. Encontrar dónde aloca npfs.sys: PoolHitTag y breakpoints por proceso

Problema real: querés parar cuando se aloca con un tag específico, pero las funciones de pool las usa medio kernel.

Por qué el breakpoint condicional global no sirve

bp condicional sobre ExAllocatePool2 global
-> millones de allocations
-> la máquina queda "boluda", evaluando constantemente, no te deja hacer nada

Opción A — PoolHitTag (variable global, no breakpoint)

ed nt!PoolHitTag '<tag al revés>'

Cómo funciona:

la función de pool, antes de alocar, chequea PoolHitTag:
  if (PoolHitTag == 0)        -> sigue normal
  if (PoolHitTag == tag)      -> dispara excepción controlada (dentro de try)
                                 -> parás ahí y ves quién lo llamó

Limitación: no se puede filtrar por proceso, para en todos. Truco práctico:

poner un MessageBox + Sleep en user-mode cerca de la zona que aloca,
hacer clic, y durante el Sleep setear el ed nt!PoolHitTag
-> para en la N-ésima allocation del tag

Opción B — bp /p por proceso

!process 0 0 console_application.exe   ; obtener el EPROCESS (DML)
bp /p <EPROCESS> nt!ExAllocatePool2 "..."

El /p limita el breakpoint a ese proceso. Aun así puede parar en allocations del mismo proceso que no son la nuestra (ej. cosas de Win32kbase), así que hay que repetir hasta caer en la llamada correcta (la que viene de WriteDataEntry).

Detalle del argumento: en ExAllocatePool2 el tag está en R8 (R8D). Y el tag en la condición va entre comillas simples: 'NpFr'. Si no para nunca, probar el tag al revés.


19. La allocation real ocurre en WriteFile, no en CreateNamedPipe

Punto clave de timing, igual que con archivos:

CreateNamedPipe / CreateSprayPipe -> sólo devuelve handles (como CreateFile)
WriteFile / WriteDataEntry        -> AQUÍ se hace la allocation del data entry

Analogía de la clase:

crear un archivo no aloca el contenido;
recién cuando escribís, el sistema sabe cuántos bytes guardar y aloca.
La pipe es igual: el WriteFile trae los datos, los aloca y los guarda.

Por eso el breakpoint útil va sobre la ruta de WriteDataEntryExAllocatePool2, no sobre la creación de la pipe.

Verificación con un breakpoint condicional dentro de npfs.sys (ahí sí se puede, porque es código propio y no lo usa todo el kernel):

print "%x" % cpu.Rcx   # o el registro con el size

Y se observa la secuencia predecible:

1000, 2000, 3000, 4000, 5000, 6000, 7000...   (heap estándar, secuencial)

20. Reconstruir NP_DATA_QUEUE_ENTRY con ReactOS

Como npfs.sys no tiene tipos, se recurre a ReactOS, que tiene la función y las estructuras reverseadas (no idénticas, pero muchísimo mejor que nada).

función relevante: NtAddDataQueueEntry  (agrega el "data entry")

Estructura reconstruida (header de 0x30 bytes + data):

typedef struct _NP_DATA_QUEUE_ENTRY {
    LIST_ENTRY QueueEntry;   // Flink / Blink
    PIRP       Irp;          // <- guardado, pero puesto a 0 antes de salir
    ULONG      DataEntryType;
    ULONG      QuotaInEntry;
    ULONG      DataSize;      // <- CAMPO OBJETIVO
    // UCHAR   Data[];        // lo que vos mandás desde user (lo que copia)
} NP_DATA_QUEUE_ENTRY;

Interpretación:

los primeros ~0x30 bytes = header del data entry
después = Data[] = exactamente lo que escribiste desde user-mode

La idea de explotación que esto habilita: antes de desbordar, aplicar esta estructura sobre el chunk que está a continuación para ver con claridad qué campo se pisa, en vez de mirar bytes sueltos.


21. El objetivo: pisar DataSize y el problema del IRP puesto a cero

El objetivo concreto del primer overflow:

el overflow pasa los 0x1000 bytes del primer bloque (KSpp)
y entra en los ~0x28/0x30 primeros bytes del header del NpFr
-> pisa DataSize
-> de ~0xFC0/0x1000 lo cambia a 0x4000
Estado DataSize
Pipe normal ~0xFC0
Pipe corrupto 0x4000

La "trampita" del IRP

El header guarda un puntero IRP que sería oro para leakear, pero:

el driver guarda el IRP en el header,
y antes de salir lo pone a CERO.
=> así nomás no se puede leakear el IRP
=> hay que hacer una "trampita" más (etapa posterior)

Esto explica por qué la explotación no termina con un simple over-read: hace falta una segunda manipulación para recuperar/inyectar punteros vivos.

Por qué no se puede cerrar el pipe corrupto

si cerrás (free) un pipe con el header pisado -> crashea ("frita")
=> hay que SEGUIR y "acomodar" todo (armar un fake header coherente)
   antes de liberar, para no romper.

22. Detectar el pipe overflowdeado con PeekNamedPipe

Después del trigger, el PoC necesita saber cuál de los ~1000 pipes quedó corrupto. Lo hace con PeekNamedPipe (mirar sin consumir):

for cada pipe en el array:
    PeekNamedPipe(pipe)
    if (bytesDisponibles > 0x4000 - 0x100):   // OB size = 0x4000
        overflowedIndex = i
        break

Lógica: sólo el pipe pisado reporta un size anormal (~0x4000), porque su DataSize fue inflado. El resto sigue en ~0xFC0.

overflowed[X] == Y  ->  break   ; encontró el índice corrupto

Estructura del array de pipes

pipes[i] para i en [0, 1000)        ; ~1000 pipes
cada pipe = un objeto con DOS handles:
    PHW (CreateFile)        -> handle de escritura -> usado por WriteDataEntry
    PHR (CreateNamedPipe)   -> handle de lectura   -> usado por ReadFile

El mismo objeto-pipe expone un handle de lectura y uno de escritura; el PoC trabaja con los handles guardados en arrays, no con el nombre.


23. Plan para la próxima: doble debugging y NtFsControlFile

La clase cierra dejando planteado el siguiente paso: doble debugging simultáneo user + kernel para ver cómo se modifican las pipes en vivo.

Debugger Rol
IDA kernel debugger ver ksthunk.sys/npfs.sys, pool, KSpp, NpFr, memmove
IDA remote Win32 debugger tracear el PoC de 32 bits (ConsoleApplication15.exe)

Setup mencionado:

correr el server remoto de IDA como administrador
configurar IP + puerto + path local/remoto del EXE

Y se adelanta una segunda vía de lectura usada por el PoC:

NtFsControlFile
  -> es otra forma de llamar a "read" del driver de pipes (vía IOCTL interno)
  -> termina yendo a la misma rutina read,
     pero puede leer cosas que el read normal no lee

El objetivo final, otra vez, es la cadena clásica:

leak thread -> token -> System process -> current process
-> escribir el token de System sobre el proceso actual

24. Workflow mental completo

1. Saltar desde DispatchIrp a DispatchIoctl (Change call address)
2. Confirmar args del dispatch: filter_device, IRP, 0, puntero a 0
3. Verificar condiciones: proceso 32 bits + RequestorMode == User
4. IRP+0xB8 -> Current Stack Location (Force offset field)
5. Corregir union -> Parameters.DeviceIoControl
6. Leer size_input=0x1000 y size_output=0xFFFFFFF0
7. Integer overflow: (0xFFFFFFF0 + 0x17) & 0xFFFFFFF8 = 0
8. Chequeo MasterIRP == 0 (METHOD_NEITHER) -> pasa
9. ProbeForRead input (ok) dentro de try/except
10. switch case del Type3InputBuffer (val == 4)
11. Chequeo de size roto: (0xFFFFFFF0 + 0x10) = 0 -> pasa
12. ExAllocatePool2: NonPagedPool, size 0x1000, tag KSpp
13. Guardar puntero en MasterIRP reusado
14. ProbeForRead output (no falla: 32 bits, no cruza a kernel)
15. UserBuffer/output via VirtualAlloc dir fija, alineado a 0x2000, lleno de 0x41
16. override_data_ptr = output + 0xFF0; OB size = 0x4000
17. memmove vulnerable (dst RCX = KSpp+0x20 -> NpFr): overflow
18. Run -> se queda sin source (>0x2000) -> except -> sale sin BSoD
19. !pool: KSpp(0x1000) seguido de NpFr(0x1000)
20. (npfs.sys) PoolHitTag / bp /p para hallar la allocation de la pipe
21. Recordar: la pipe aloca en WriteFile, no en CreateNamedPipe
22. Reconstruir NP_DATA_QUEUE_ENTRY (ReactOS): header 0x30, DataSize objetivo
23. DataSize: ~0xFC0 -> 0x4000
24. PeekNamedPipe para encontrar el pipe overflowdeado
25. (próxima) doble debugging + fake IRP + NtFsControlFile -> token stealing

25. Cheat sheet

Valores y offsets

Valor Uso
0xB8 offset del Current Stack Location en el IRP
0x1000 InputBufferLength y tamaño del chunk KSpp/NpFr
0xFFFFFFF0 OutputBufferLength malicioso
0x17 / 0xFFFFFFF8 constantes del alineado que provoca el overflow
0x0 resultado del size alineado tras el wraparound
0x10 sumando del chequeo de size roto (0xFFFFFFF0 + 0x10 = 0)
0x18 size mínimo de input (< 0x18 -> invalid buffer size)
0x97 PoolType (NonPagedPool + flags)
0x2000 tamaño alineado del output buffer (VirtualAlloc)
0x1060 base del cálculo del output size
0xFF0 offset del override_data_ptr dentro del output (+0x1000-0x10)
0x4000 nuevo DataSize del pipe corrupto
0x30 tamaño del header NP_DATA_QUEUE_ENTRY
0xFC0 DataSize de un pipe normal
0x200 umbral LFH vs heap estándar
4 valor del switch case del Type3InputBuffer

Tags

Tag Valor Significado
KSpp 0x7070534B chunk vulnerable de ksthunk.sys
NpFr (little-endian) data entry de named pipe en npfs.sys

WinDbg / debugging

!pool <chunk>                          ; ver tag y size del chunk
!process 0 0 console_application.exe   ; obtener EPROCESS (DML)
bp /p <EPROCESS> nt!ExAllocatePool2    ; breakpoint por proceso (tag en R8)
ed nt!PoolHitTag '<tag>'               ; parar por tag (global, no por proceso)

IDA tricks

Acción Uso
Change call address saltar directo entre dispatch y handler
Force offset field resolver IRP+0xB8 como CurrentStackLocation
union -> DeviceIoControl corregir el IO_STACK_LOCATION en IOCTL
Manual Memory Regions poder navegar/aplicar structs sobre memoria runtime
Struct var aplicar NP_DATA_QUEUE_ENTRY al chunk NpFr
bp condicional dentro de npfs.sys trackear sizes (print "%x") sin colgar el kernel

Secuencia de corrupción

KSpp chunk (0x1000)          NpFr chunk siguiente (0x1000)
[ ....... 0x41 ....... ][Flink Blink IRP DataEntryType QuotaInEntry DataSize | Data ]
   memmove desde KSpp+0x20 ----------------------------------------> DataSize = 0x4000

26. Glosario corto

Término Descripción
CKSAutomationThunk::ThunkEnableEventIrp Handler vulnerable alcanzado por el IOCTL 0x2F0007.
Current Stack Location IO_STACK_LOCATION actual del IRP, en IRP+0xB8.
METHOD_NEITHER ("Nader") Método de IOCTL donde el driver maneja punteros user crudos; MasterIrp queda sin uso.
MasterIrp Campo de AssociatedIrp reutilizado como scratch en la ruta vulnerable.
Type3InputBuffer Buffer de entrada en METHOD_NEITHER; de él sale el case del switch.
UserBuffer Buffer de salida (y lectura) en METHOD_NEITHER.
ProbeForRead Valida inicio + (inicio+size) en user; no recorre todos los bytes.
ExAllocatePool2 API de allocation de pool; tag en R8.
KSpp Pool tag del chunk vulnerable de ksthunk.sys.
NpFr Pool tag del data entry de named pipe en npfs.sys.
NP_DATA_QUEUE_ENTRY Estructura del data entry de pipe (header 0x30 + Data[]).
DataSize Campo del data entry que dice cuántos bytes cree tener el pipe; objetivo del overflow.
PoolHitTag Variable global de nt que dispara excepción al alocar un tag dado.
PeekNamedPipe API que mira los bytes disponibles de un pipe sin consumirlos.
NtFsControlFile API nativa, vía alternativa de lectura hacia el driver de pipes.
LFH Low Fragmentation Heap; para sizes chicos (< 0x200), layout poco predecible.

Apunte basado en la transcripción clase-modulo6-4 del Módulo 6 y en los hallazgos ya confirmados en la IDB de ksthunk.sys. La clase es el recorrido en vivo del bug: del dispatch al memmove, validando en el kernel debugger cada paso del integer overflow, y arrancando el reversing de npfs.sys para entender la estructura NP_DATA_QUEUE_ENTRY cuyo campo DataSize es el primer objetivo de la explotación.