Clase 3 — Debugging del PoC, overwrite de NpFr.DataSize y transición hacia fake IRP¶
← Volver al Módulo 6 · ← Clase 2¶
Resumen: en esta clase se sigue el exploit desde user-mode y kernel-mode al mismo tiempo. Se exportan estructuras reconstruidas de
npfs.syshacia la IDB deksthunk.sys, se prepara el PoC de 32 bits, se observa el primer overflow real sobre un chunkNpFr, se confirma que el campo pisado esDataSize, y se empieza a entender la segunda etapa: usar el pipe agrandado para leer headers de pipes vecinos, recuperarFlink/Blink/punteros deIRP, insertar un fakeIRPy avanzar hacia leaks deEPROCESS/token.
Tabla de Contenidos¶
- Contexto de la clase
- Exportar estructuras de
npfs.sysa otras IDBs - Setup de doble debugging user/kernel
- Problemas prácticos con IDA, paths, snapshots y breakpoints
- Fase inicial del PoC
- Spray de pipes y handles de lectura/escritura
- Buffers user-mode del exploit
- Preparar el overwrite dentro del output buffer
- Crear el hueco y disparar el trigger
- Confirmación en kernel: chunk
KSppseguido de chunkNpFr - Aplicar la estructura
NP_DATA_QUEUE_ENTRY - Primer objetivo: pisar
DataSizecon0x4000 - Usar Watch List para observar el chunk corrupto
- Leer de más: encontrar el pipe overflowdeado
- Segunda etapa: reescritura de header y fake
IRP NtFsControlFile, fakeIRPy buffers controlados- De leak a token stealing
- Detalles verificados en IDA/MCP
- Debugging caveats: PatchGuard, antivirus y corrupción persistente
- Workflow mental completo
- Cheat sheet
- Glosario corto
1. Contexto de la clase¶
La clase 2 dejó claro el bug primario:
IOCTL 0x2F0007
-> CKSAutomationThunk::ThunkEnableEventIrp
-> integer overflow en OutputBufferLength
-> allocation KSpp demasiado chica
-> memmove desde UserBuffer+0x10 hacia pool+0x20
-> overflow lineal hacia el siguiente chunk
La clase 3 ya no se enfoca solo en encontrar el bug, sino en cómo el PoC transforma ese overflow en una explotación útil.
La secuencia de alto nivel:
1. Grooming con pipes NpFr
2. Liberar un hueco
3. Hacer que el chunk vulnerable KSpp caiga antes de un NpFr
4. Overflow desde KSpp hacia el header del NpFr siguiente
5. Cambiar NpFr.DataSize a 0x4000
6. Leer más allá de la data real del pipe
7. Recuperar datos del header/pipe vecino
8. Usar esos datos para armar una segunda corrupción
9. Insertar/controlar un fake IRP
10. Usar la primitiva resultante para leer/escribir kernel
11. Leer tokens y hacer token stealing
El punto pedagógico es ir viendo en vivo cómo cambian los chunks de pool, no solo leer el PoC terminado.
2. Exportar estructuras de npfs.sys a otras IDBs¶
En la clase anterior se reconstruyó en npfs.sys una estructura tipo data entry, basada en reversing + ReactOS.
Esa estructura es necesaria también en la IDB de ksthunk.sys, porque el overflow ocurre dentro de ksthunk.sys, pero el chunk que queda a continuación pertenece a npfs.sys.
Flujo usado¶
En la IDB de npfs.sys:
Después, en la IDB de ksthunk.sys:
Detalle práctico: a veces hay que ejecutar el IDC dos veces. La primera pasada crea tipos dependientes parcialmente; la segunda termina de resolver estructuras que dependían de otras.
Por qué hacerlo¶
Cuando el overflow llegue al chunk siguiente, se puede ir a:
y aplicar ahí la estructura reconstruida de pipe:
Así se ve qué campo cambia realmente, en vez de mirar solo bytes.
3. Setup de doble debugging user/kernel¶
La clase usa dos vistas a la vez:
| Debugger | Objetivo |
|---|---|
| IDA kernel debugger | ver ksthunk.sys, pool, KSpp, NpFr, memmove, excepciones |
| IDA remote Windows debugger | tracear el PoC user-mode de 32 bits |
El PoC es un proceso de 32 bits, por eso se usa el remote debugger Win32 de IDA.
Configuración práctica¶
En IDA user-mode:
Configurar:
- IP de la VM;
- puerto del server remoto;
- path local del ejecutable;
- path remoto del ejecutable;
- working directory remoto.
La clase remarca un problema típico: si se vuelve a un snapshot, puede quedar una versión vieja del PoC en la VM. Si el EXE local y el remoto no coinciden, IDA puede debuggear símbolos/código incorrectos. La solución fue copiar de nuevo ConsoleApplication15.exe a la VM.
4. Problemas prácticos con IDA, paths, snapshots y breakpoints¶
La clase tuvo varios problemas reales de debugging. Son parte del aprendizaje.
Software breakpoints en kernel¶
Un software breakpoint escribe 0xCC en memoria. En kernel eso puede ser delicado:
- el byte queda parcheado en código compartido;
- puede afectar a otro hilo/core;
- si IDA queda desincronizado, puede dejar bytes cambiados;
- una IDB o sesión puede quedar “rara” después de varios intentos.
Por eso se recomienda usar hardware breakpoints cuando sea posible.
Ejemplo conceptual:
Comandos recurrentes¶
Manual Memory Regions en IDA¶
Para poder aplicar structs sobre direcciones runtime, IDA a veces necesita que exista un segmento de memoria. El truco usado:
Después de que aparece una región tipo memory, IDA permite:
- doble click a direcciones;
- aplicar
Struct var; - abrir Watch sobre una región.
5. Fase inicial del PoC¶
La ejecución user-mode empieza con una fase de setup.
CheckVersion¶
El exploit detecta la versión/build de Windows para elegir offsets correctos.
Ejemplos de offsets mencionados:
| Campo | Offset observado / comentado |
|---|---|
EPROCESS.Token |
normalmente 0x4B8 en esa VM/build |
| otros builds | pueden variar; se ajustan por versión |
La idea es que un exploit de token stealing no puede hardcodear offsets universales. Windows cambia estructuras entre builds.
APIs resueltas dinámicamente¶
El PoC resuelve funciones necesarias con GetModuleHandle/GetProcAddress.
Una API mencionada:
Esto aparece porque el PoC es de 32 bits y necesita interactuar con direcciones/datos de 64 bits.
También obtiene el PID del proceso actual:
6. Spray de pipes y handles de lectura/escritura¶
El PoC crea muchas named pipes para controlar el layout del pool.
La función conceptual:
Por cada pipe se obtienen dos handles:
| Handle | API típica | Uso |
|---|---|---|
PHW |
CreateFile |
escritura |
PHR |
CreateNamedPipe |
lectura |
No importa tanto el nombre del pipe después de obtener los handles: el exploit trabaja principalmente con los handles guardados en arrays.
Holder pipe¶
Además del array de pipes, se crea un pipe especial:
El holder no es un array; es una sola estructura con handle de lectura y handle de escritura. Se usa como objeto auxiliar en fases posteriores para escribir/leer datos controlados.
7. Buffers user-mode del exploit¶
La clase identifica varios buffers del PoC.
dummyBuffer¶
Sirve como buffer grande de datos controlados para operaciones de pipe. En memoria se ve lleno de 0x77.
inputBuffer¶
Es el input del IOCTL vulnerable.
outputBuffer¶
El output se reserva con VirtualAlloc en una dirección baja/controlada.
Tamaño alineado observado:
La razón de usar una dirección baja/fija es evitar que ProbeForRead falle por sumar un tamaño enorme y cruzar un límite no permitido.
8. Preparar el overwrite dentro del output buffer¶
El PoC prepara una región específica del output buffer que terminará cayendo sobre el header del chunk NpFr vecino.
Cálculos vistos en clase:
Hay que leer esos valores con cuidado, porque hay varias bases posibles:
- base real devuelta por
VirtualAlloc; - puntero efectivo pasado como
UserBuffer; - offset dentro del chunk
KSpp; - offset dentro del chunk
NpFrsiguiente; - desplazamientos introducidos por el
memmovereal.
El punto importante de la clase: no confiar solo en la cuenta inicial. Hay que confirmar en runtime mirando:
SetupOverwrite¶
La función de setup arma bytes que van a pisar el header del pipe.
Primera etapa:
El objetivo inicial es solo agrandar el tamaño lógico del pipe.
9. Crear el hueco y disparar el trigger¶
La secuencia de grooming es:
1. Crear muchos pipes
2. Escribir data en pipes para crear chunks NpFr
3. Leer/vaciar un pipe del medio
4. Ese read libera el data entry y deja un hueco
5. Llamar al IOCTL vulnerable con Trigger
6. La allocation KSpp cae en ese hueco
7. El siguiente chunk queda siendo NpFr
La clase menciona un ReadDataEntry antes del trigger, usado para vaciar/liberar uno de los pipes:
Después se llama a:
Ese trigger llega al memmove vulnerable de ksthunk.sys.
10. Confirmación en kernel: chunk KSpp seguido de chunk NpFr¶
Una vez parado en kernel, se confirma la distribución con !pool.
La clase ve algo como:
Modelo:
El memmove vulnerable no empieza en el inicio del chunk KSpp, sino en:
Eso fue confirmado mirando el destination:
Entonces el overflow sale del chunk KSpp y pisa el header del NpFr siguiente.
11. Aplicar la estructura NP_DATA_QUEUE_ENTRY¶
En el chunk NpFr siguiente se aplica la estructura importada desde la IDB de npfs.sys.
Estructura conceptual:
typedef struct _NP_DATA_QUEUE_ENTRY_LIKE {
LIST_ENTRY QueueEntry; // Flink/Blink
PIRP Irp;
ULONG DataEntryType;
ULONG QuotaInEntry;
ULONG DataSize; // campo objetivo
UCHAR Data[];
} NP_DATA_QUEUE_ENTRY_LIKE;
Corrección de alineación¶
La clase muestra que la estructura importada quedó corrida. Hubo que insertar un gap de 8 bytes para que los offsets coincidieran.
Después de corregirla, el campo importante queda en:
Antes de la corrupción, el DataSize observado para un pipe normal rondaba:
Esto tiene sentido si se piensa que el chunk total es 0x1000, pero una parte se consume en header/metadata.
12. Primer objetivo: pisar DataSize con 0x4000¶
Después del memmove, se revisa el chunk NpFr con la estructura aplicada.
Resultado observado:
Ese es el primer objetivo real de la explotación.
Antes vs después¶
| Estado | DataSize |
|---|---|
| Pipe normal | 0xFC0 aprox. |
| Pipe corrupto | 0x4000 |
Por qué sirve¶
Si npfs.sys cree que el pipe tiene 0x4000 bytes disponibles, pero la allocation real era de alrededor de 0x1000, las operaciones de lectura/peek pueden cruzar hacia chunks siguientes.
Esto permite leer:
- data del pipe actual;
- header del siguiente
NpFr; - punteros internos como
Flink,BlinkoIRP; - eventualmente punteros kernel útiles para armar primitivas.
13. Usar Watch List para observar el chunk corrupto¶
Un truco útil de IDA mostrado en clase:
La Watch List queda apuntando al chunk NpFr interesante.
Ventaja:
- no hay que navegar manualmente a la dirección cada vez;
- se ve en vivo si cambia
DataSize; - se puede observar si posteriores triggers reescriben
Flink,Blink,IRP, etc.
En la clase se ve claramente el cambio:
14. Leer de más: encontrar el pipe overflowdeado¶
Después de agrandar el DataSize, el PoC lee/peeks sobre los pipes para encontrar cuál quedó corrupto.
Idea:
La clase menciona que se lee hacia dummyBuffer y se comprueba cuál pipe tiene el size anormal.
Por qué 0x4000¶
El PoC eligió 0x4000 como valor escrito en DataSize. Entonces solo el pipe corrupto reporta/permite operar con ese tamaño.
Qué se gana con esa lectura¶
Al leer de más, el exploit puede obtener bytes de estructuras vecinas, incluyendo datos del header del pipe siguiente.
Eso es clave para la segunda etapa: ya no solo se pisa un size; ahora se empieza a recuperar metadata para construir una corrupción más precisa.
15. Segunda etapa: reescritura de header y fake IRP¶
La clase intenta seguir una segunda llamada a Trigger.
La idea observada:
1. Primer trigger -> pisa DataSize = 0x4000
2. Lecturas/peeks -> recuperan datos del header vecino
3. Segundo trigger -> reescribe header del NpFr con valores más precisos
4. Se restauran/inyectan Flink/Blink y se reemplaza puntero IRP
5. El header ahora apunta a un fake IRP controlado por user-mode
La clase marca que esta parte es más difícil de seguir en vivo, pero el mecanismo general es claro.
Qué se ve en el Watch List¶
Después de otra pasada por el overflow:
- ya no solo aparece
DataSize = 0x4000; - aparecen valores tipo
Flink/Blinkrecuperados de una lectura previa; - se ve un puntero que parece apuntar a un fake
IRPen user space.
El instructor lo describe como:
Nota de cautela¶
La clase no termina de validar todos los offsets del fake IRP. Conviene tratar esta etapa como reconstrucción parcial:
No inventar más estructura de la visible; hay que seguir confirmándola en runtime.
16. NtFsControlFile, fake IRP y buffers controlados¶
El PoC usa llamadas de pipe más directas, incluyendo una ruta vía:
La clase lo resume como una forma de mandar una operación directa al driver de filesystem/pipe, parecida conceptualmente a un IOCTL interno.
La hipótesis de explotación:
header NpFr corrupto
-> puntero IRP reemplazado por fake IRP
-> npfs.sys usa campos del IRP para decidir buffers de lectura/escritura
-> el atacante controla hacia dónde lee o desde dónde escribe
Campos mencionados conceptualmente:
AssociatedIrp.SystemBuffer;- buffers de destino/origen usados por operaciones de pipe;
- campos de
IRPque cambian entre buffered/unbuffered.
La frase clave de la clase:
controlando IRP, como el tipo toma de IRP el buffer de destino / lectura / SystemBuffer,
vos sabés de dónde va a leer.
Por qué un fake IRP en user-space puede servir¶
El exploit está ejecutándose en el mismo proceso/contexto user-mode que dispara las operaciones. Si logra que código kernel use un puntero a una estructura controlada como si fuera un IRP, puede manipular campos que después npfs.sys consulta.
En debugging, si IDA no muestra correctamente esa memoria, puede ser por contexto o símbolos user-mode no recargados. Se mencionan:
como ideas para recuperar contexto/símbolos de user-mode.
17. De leak a token stealing¶
La etapa final buscada es la clásica elevación local:
leer punteros kernel
-> encontrar EPROCESS actual
-> encontrar EPROCESS de System
-> leer System.Token
-> escribir System.Token sobre CurrentProcess.Token
La clase menciona offsets específicos para esa VM/build:
También se menciona que al inspeccionar ciertos punteros con Object view se ve tag:
lo que sugiere que el objeto apuntado podría ser un EPROCESS.
Modelo mental de token stealing¶
CurrentEPROCESS + TokenOffset -> CurrentTokenAddress
SystemEPROCESS + TokenOffset -> SystemTokenAddress
SystemToken = read64(SystemTokenAddress)
write64(CurrentTokenAddress, SystemToken)
En esta clase se deja planteado el mecanismo; el análisis fino de cómo el fake IRP entrega la primitiva exacta queda para seguirlo con más debugging.
18. Detalles verificados en IDA/MCP¶
De la IDB de ksthunk.sys ya se habían confirmado estos puntos relevantes para esta clase:
| Dirección | Detalle |
|---|---|
0x1c0002ab8 |
CKSAutomationThunk::ThunkEnableEventIrp |
0x1c0002db6 |
ExAllocatePool2 para el chunk KSpp vulnerable |
0x1c0002dc2 |
se guarda el pool en Irp->AssociatedIrp.MasterIrp |
0x1c0002e39 |
size = OutputBufferLength - 0x10 |
0x1c0002e42 |
src = Irp->UserBuffer + 0x10 |
0x1c0002e46 |
dst = allocated_pool + 0x20 |
0x1c0002e4a |
memmove que produce el overflow |
Tags:
| Tag | Valor | Significado |
|---|---|---|
KSpp |
0x7070534B |
chunk vulnerable de ksthunk.sys |
NpFr |
variable según vista/little-endian | data entry de named pipe en npfs.sys |
Esto calza con lo observado en runtime en la clase:
19. Debugging caveats: PatchGuard, antivirus y corrupción persistente¶
La clase muestra varios problemas que son normales cuando se debuggea explotación kernel.
PatchGuard / corrupción sostenida¶
Si se deja el sistema mucho tiempo con estructuras kernel corruptas mientras se steppea, hay más chances de crash.
Motivos:
- el pipe queda en estado inconsistente;
- se paran threads en momentos sensibles;
- PatchGuard u otros chequeos pueden correr mientras el estado está corrupto;
- al cerrar handles o salir del proceso se liberan objetos corruptos y puede haber BSoD.
Antivirus / Defender¶
El instructor sospecha que el antivirus puede interferir con el PoC/debugging. Para este tipo de laboratorio, Defender puede:
- demorar ejecución;
- interceptar procesos;
- cambiar timing;
- hacer que debuggear sea menos estable.
Recomendaciones prácticas¶
- Usar snapshots frecuentes.
- Preferir hardware breakpoints.
- Evitar parar demasiado tiempo después del primer trigger.
- Usar
MessageBoxo pausas user-mode controladas en vez de breakpoints kernel constantes. - Si una sesión queda rara, restaurar snapshot en vez de seguir peleando con estado corrupto.
20. Workflow mental completo¶
1. Reconstruir NP_DATA_QUEUE_ENTRY en npfs.sys
2. Exportar tipos a IDC
3. Importar tipos en ksthunk.sys y en la IDB del PoC si hace falta
4. Correr PoC de 32 bits con IDA remote debugger
5. Resolver versión de Windows y offsets de token
6. Crear spray de named pipes NpFr
7. Crear holder pipe
8. Preparar dummyBuffer, inputBuffer y outputBuffer
9. Armar overwrite: header parcial + DataSize = 0x4000
10. Escribir pipes para poblar pool
11. Leer/vaciar un pipe del medio para crear hueco
12. Trigger IOCTL 0x2F0007
13. ksthunk.sys aloca KSpp en el hueco
14. memmove escribe desde KSpp+0x20 hacia NpFr siguiente
15. Confirmar con !pool: KSpp seguido de NpFr
16. Aplicar struct NP_DATA_QUEUE_ENTRY al NpFr
17. Ver DataSize pasar de ~0xFC0 a 0x4000
18. Leer/peek pipes para encontrar overflowedIndex
19. Leer de más para obtener header/punteros vecinos
20. Segundo trigger reescribe Flink/Blink/IRP
21. Insertar fake IRP controlado
22. Usar operaciones de pipe/NtFsControlFile para leer/escribir kernel
23. Leer EPROCESS/TOKEN y copiar token de System al proceso actual
21. Cheat sheet¶
Valores y offsets¶
| Valor | Uso |
|---|---|
0x1000 |
tamaño del chunk KSpp y de chunks NpFr usados para grooming |
0x10000 |
tamaño de dummyBuffer |
0x77 |
patrón con el que se llena dummyBuffer |
0x2000 |
tamaño alineado/reservado para output user-mode |
0x1060 |
tamaño usado en el cálculo del área efectiva del output |
0xFA0 |
desplazamiento derivado de 0x2000 - 0x1060 |
0x4000 |
nuevo DataSize del pipe corrupto |
0x28 |
offset aproximado de DataSize en la estructura corregida |
0x4B8 |
offset de EPROCESS.Token en la VM/build de la clase |
Debugging¶
IDA tricks¶
| Acción | Uso |
|---|---|
| Dump type info to IDC | exportar structs desde una IDB |
| Script file | importar structs en otra IDB |
| Manual Memory Regions | permitir navegación/aplicación de structs en memoria runtime |
| Struct var | aplicar NP_DATA_QUEUE_ENTRY al chunk NpFr |
| Add Watch | monitorear DataSize, Flink, Blink, IRP en vivo |
| Insert gap | corregir alineación de structs importados |
Secuencia de corrupción¶
KSpp chunk NpFr chunk siguiente
[........0x1000][Flink Blink IRP ... DataSize ... Data]
overflow -> DataSize = 0x4000
Secuencia de explotación conceptual¶
overwrite DataSize
-> read/peek over-read
-> leak header vecino
-> recuperar Flink/Blink/IRP
-> segundo overwrite más preciso
-> fake IRP
-> controlar buffers usados por npfs.sys
-> leer/escribir kernel
-> token stealing
22. Glosario corto¶
| Término | Descripción |
|---|---|
NP_DATA_QUEUE_ENTRY |
Estructura interna de npfs.sys que representa una entrada de data en un pipe. |
NpFr |
Pool tag asociado a data entries/buffers de named pipes. |
KSpp |
Pool tag del chunk vulnerable alocado por ksthunk.sys. |
DataSize |
Campo del data entry que indica cuántos bytes cree tener el pipe. |
Flink / Blink |
Punteros de lista doblemente enlazada usados en headers kernel. |
Fake IRP |
Estructura IRP falsa/controlada por el atacante para influir en buffers usados por npfs.sys. |
NtFsControlFile |
API nativa usada para enviar operaciones de control a filesystem/pipe drivers. |
NtWow64WriteVirtualMemory64 |
API útil desde proceso WOW64 para escribir memoria de 64 bits. |
| Watch List | Vista de IDA para observar una dirección/estructura mientras cambia durante debugging. |
| PatchGuard | Mecanismo de protección de integridad del kernel que puede crashear el sistema ante corrupción sostenida. |
| Token stealing | Técnica de EoP que copia el token de System al proceso actual. |
Apunte basado en la transcripción clase-modulo6-3 del Módulo 6 y en los hallazgos ya confirmados en la IDB de ksthunk.sys. La clase aterriza el exploit real: del overflow sobre KSpp a la corrupción de NpFr.DataSize, y de ahí hacia over-read, fake IRP y preparación de primitivas para token stealing.