Clase 4 — Debugging en vivo del overflow KSpp→NpFr 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::ThunkEnableEventIrpy lo recorre paso a paso en el kernel debugger, confirmando en runtime cada cosa que en clases anteriores se había deducido estáticamente: elCurrent Stack LocationenIRP+0xB8, el integer overflow deOutputBufferLength, el chequeo roto del size, la allocationKSppde0x1000, elmemmoveque desborda hacia el chunkNpFrsiguiente y el pisado deDataSizea0x4000. Después se cambia anpfs.sys, se muestra cómo encontrar dónde alocan las pipes (PoolHitTag, breakpoints por proceso) y se reconstruye la estructuraNP_DATA_QUEUE_ENTRY(header de0x30bytes) usando ReactOS, identificando que el campo objetivo esDataSizey que elIRPguardado se pone a cero antes de salir (por eso hace falta una "trampita" extra para leakear).
Tabla de Contenidos¶
- Contexto de la clase
- Reconstruir el dispatch y fijar la dirección de salto
- Argumentos del dispatch: todos punteros a cero
- Símbolos:
ksthunk.syscompleto vsnpfs.syspelado - Las dos condiciones de entrada: 32 bits + RequestorMode User
Current Stack LocationenIRP+0xB8y corrección de la union- Los dos sizes de entrada/salida
- El integer overflow paso a paso
- El
MasterIRPreusado y el casoMETHOD_NEITHER ProbeForReaddentro del try/except- El switch case del
Type3InputBuffer - El chequeo de size roto
- La allocation
KSppde0x1000 - El output buffer en dirección fija con
VirtualAlloc - El
memmovevulnerable y el crash controlado - Confirmación con
!pool:KSppseguido deNpFr - Heap estándar vs Low Fragmentation Heap
- Encontrar dónde aloca
npfs.sys:PoolHitTagy breakpoints por proceso - La allocation real ocurre en
WriteFile, no enCreateNamedPipe - Reconstruir
NP_DATA_QUEUE_ENTRYcon ReactOS - El objetivo: pisar
DataSizey el problema delIRPpuesto a cero - Detectar el pipe overflowdeado con
PeekNamedPipe - Plan para la próxima: doble debugging y
NtFsControlFile - Workflow mental completo
- Cheat sheet
- 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:
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:
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:
El campo en el IRP aparece primero como:
En IDA sale primero como IRP_Tail. El truco para que aparezca bien:
Y como estamos en un IOCTL, la union del IO_STACK_LOCATION está mal resuelta por defecto. Hay que cambiarla a la variante correcta:
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:
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:
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_BUFFEREDese campo apunta al system buffer (distinto de cero) y la función saldría; enMETHOD_NEITHERqueda 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 = 0x1000sobre 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:
Y después la cadena de restas típica de un switch:
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:
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:
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:
Y ahí, en la zona que va a caer sobre el header del pipe siguiente, se escribe el valor objetivo:
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 de0x1000siempre 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:
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)¶
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
ExAllocatePool2el tag está enR8(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 WriteDataEntry → ExAllocatePool2, 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):
Y se observa la secuencia predecible:
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).
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.
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.