Saltar a contenido

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.sys hacia la IDB de ksthunk.sys, se prepara el PoC de 32 bits, se observa el primer overflow real sobre un chunk NpFr, se confirma que el campo pisado es DataSize, y se empieza a entender la segunda etapa: usar el pipe agrandado para leer headers de pipes vecinos, recuperar Flink/Blink/punteros de IRP, insertar un fake IRP y avanzar hacia leaks de EPROCESS/token.


Tabla de Contenidos

  1. Contexto de la clase
  2. Exportar estructuras de npfs.sys a otras IDBs
  3. Setup de doble debugging user/kernel
  4. Problemas prácticos con IDA, paths, snapshots y breakpoints
  5. Fase inicial del PoC
  6. Spray de pipes y handles de lectura/escritura
  7. Buffers user-mode del exploit
  8. Preparar el overwrite dentro del output buffer
  9. Crear el hueco y disparar el trigger
  10. Confirmación en kernel: chunk KSpp seguido de chunk NpFr
  11. Aplicar la estructura NP_DATA_QUEUE_ENTRY
  12. Primer objetivo: pisar DataSize con 0x4000
  13. Usar Watch List para observar el chunk corrupto
  14. Leer de más: encontrar el pipe overflowdeado
  15. Segunda etapa: reescritura de header y fake IRP
  16. NtFsControlFile, fake IRP y buffers controlados
  17. De leak a token stealing
  18. Detalles verificados en IDA/MCP
  19. Debugging caveats: PatchGuard, antivirus y corrupción persistente
  20. Workflow mental completo
  21. Cheat sheet
  22. 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:

File
  -> Produce file
  -> Dump type info to IDC file

Después, en la IDB de ksthunk.sys:

File
  -> Script file
  -> ejecutar el IDC exportado

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:

chunk_KSpp + 0x1000

y aplicar ahí la estructura reconstruida de pipe:

Struct var -> data_entry / NP_DATA_QUEUE_ENTRY-like

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:

Debugger -> Select debugger -> Remote Windows debugger

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:

ba e1 <address>

Comandos recurrentes

.reload /f
.reload /user
bd *
bc *

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:

Debugger -> Manual memory regions -> editar/agregar rango amplio

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:

NtWow64WriteVirtualMemory64

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:

GetCurrentProcessId

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:

CreateSprayPipe

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:

Holder

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

size = 0x10000
fill = 0x77

Sirve como buffer grande de datos controlados para operaciones de pipe. En memoria se ve lleno de 0x77.

inputBuffer

size = 0x1000
fill = 0x00

Es el input del IOCTL vulnerable.

outputBuffer

El output se reserva con VirtualAlloc en una dirección baja/controlada.

Tamaño alineado observado:

alignedOutputBuffer = 0x2000

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:

0x2000 - 0x1060 = 0xFA0
0xFA0 + 0xFF0 = 0x1F90

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 NpFr siguiente;
  • desplazamientos introducidos por el memmove real.

El punto importante de la clase: no confiar solo en la cuenta inicial. Hay que confirmar en runtime mirando:

destination = R9 + 0x20
chunk KSpp
chunk NpFr = chunk KSpp + 0x1000

SetupOverwrite

La función de setup arma bytes que van a pisar el header del pipe.

Primera etapa:

Flink/Blink/control fields -> 0
DataSize                  -> 0x4000

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:

ReadDataEntry(pipe[...], PipeSize - 0x80, ...)

Después se llama a:

Trigger(hDevice, inputBuffer, outputBuffer, ...)

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:

chunk original: tag KSpp
chunk + 0x1000: tag NpFr

Modelo:

0x...6000  [KSpp, size 0x1000]
0x...7000  [NpFr, size 0x1000]

El memmove vulnerable no empieza en el inicio del chunk KSpp, sino en:

KSpp + 0x20

Eso fue confirmado mirando el destination:

RCX = R9 + 0x20

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:

DataSize offset ~= 0x28

Antes de la corrupción, el DataSize observado para un pipe normal rondaba:

0xFC0

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:

DataSize = 0x4000

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, Blink o IRP;
  • eventualmente punteros kernel útiles para armar primitivas.

13. Usar Watch List para observar el chunk corrupto

Un truco útil de IDA mostrado en clase:

clic derecho sobre la estructura/rango -> Add Watch

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:

antes: DataSize ~= 0xFC0
después: header con ceros y DataSize = 0x4000

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:

for each pipe:
    leer/peek hasta 0x4000
    si devuelve más de lo normal:
        overflowedIndex = i
        break

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/Blink recuperados de una lectura previa;
  • se ve un puntero que parece apuntar a un fake IRP en user space.

El instructor lo describe como:

leyó un Blink/Flink
los volvió a meter
armó un IRP trucho
metió ese fake IRP en el header

Nota de cautela

La clase no termina de validar todos los offsets del fake IRP. Conviene tratar esta etapa como reconstrucción parcial:

probable fake IRP + probable control de buffers vía campos del IRP

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:

NtFsControlFile

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 IRP que 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:

.process /p /r <EPROCESS>
.reload /user

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:

EPROCESS.Token ~= 0x4B8

También se menciona que al inspeccionar ciertos punteros con Object view se ve tag:

Proc

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:

KSpp + 0x1000 -> NpFr
memmove empieza en KSpp + 0x20
DataSize del NpFr siguiente termina en 0x4000

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 MessageBox o 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

!pool <chunk>
ba e1 <address>
.reload /f
.reload /user
bd *
bc *

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.