Saltar a contenido

Clase 2 — Debugging del IOCTL vulnerable, integer overflow y grooming con pipes NpFr

← Volver al Módulo 6 · ← Clase 1

Resumen: en esta clase se continúa desde el dispatch de IOCTL encontrado en la clase anterior. Se reconstruyen los argumentos reales que llegan a la función vulnerable, se valida en runtime el integer overflow del OutputBufferLength, se observa cómo el memmove desborda una allocation de pool de 0x1000, y se empieza a estudiar npfs.sys para entender por qué las named pipes con tag NpFr sirven como objeto adyacente/reclaimer.


Tabla de Contenidos

  1. Contexto: desde el dispatch hasta la función vulnerable
  2. Reconstruir argumentos antes de entrar a la vulnerable
  3. Condiciones de entrada del IOCTL
  4. IO_STACK_LOCATION y campos de DeviceIoControl
  5. Valores del PoC: input y output sizes
  6. El integer overflow del OutputBufferLength
  7. ProbeForRead, try/except y por qué no hay BSoD inmediato
  8. Allocation vulnerable en pool
  9. El memmove que provoca el overflow
  10. Confirmación en debugger
  11. Grooming: por qué usar named pipes
  12. Pool tags y cómo frenar en allocations NpFr
  13. Breakpoint genérico vs breakpoint dentro de npfs.sys
  14. Qué pasa cuando se escriben pipes
  15. Estructuras de npfs.sys con ayuda de ReactOS
  16. Objetivo del overflow: pisar DataSize
  17. Detección del pipe overflowdeado
  18. Plan para doble debugging user/kernel
  19. Workflow mental completo
  20. Cheat sheet
  21. Glosario corto

1. Contexto: desde el dispatch hasta la función vulnerable

La clase arranca retomando el camino visto en la clase anterior:

DispatchIrpBridge
  -> dispatch por MajorFunction
  -> IRP_MJ_DEVICE_CONTROL
  -> DispatchIoctl / DispatchDeviceControl
  -> case IOCTL 0x2F007
  -> función vulnerable

Ya se había determinado que el IOCTL interesante era:

0x2F007

El foco ahora es dejar de ver la función vulnerable como pseudocódigo suelto y responder:

  • con qué argumentos reales se llama;
  • qué buffers vienen desde user-mode;
  • qué tamaños se controlan;
  • qué operación hace overflow;
  • qué objeto queda después en el pool;
  • qué campo se pisa en el objeto adyacente.

Hallazgos verificados con IDA/MCP

El binario cargado en IDA confirma esta ruta con nombres y direcciones concretas:

Dirección Función Rol en la ruta
0x1c0001320 CKernelFilterDevice::DispatchIrp dispatch genérico por IRP_MJ_*; para IRP_MJ_DEVICE_CONTROL llama virtualmente a DispatchIoctl
0x1c0001eb0 CKSThunkDevice::DispatchIoctl router WOW64 del device; enruta 0x2F0003, 0x2F0007, 0x2F000B
0x1c00020f0 CKSThunkPin::DispatchIoctl router equivalente cuando ya hay CKernelFilterFile/pin asociado; también enruta IOCTLs de streaming
0x1c0002ab8 CKSAutomationThunk::ThunkEnableEventIrp handler vulnerable alcanzado por 0x2F0007

La relación importante es:

0x2F0007
  -> CKSThunkDevice::DispatchIoctl / CKSThunkPin::DispatchIoctl
  -> CKSAutomationThunk::ThunkEnableEventIrp
  -> cálculo vulnerable de OutputBufferLength
  -> ExAllocatePool2 + memmove overflow

También queda confirmado que 0x2F0007 en decimal es 3080199, que es el valor que a veces muestra el decompiler cuando no aplica enums de IOCTL.


2. Reconstruir argumentos antes de entrar a la vulnerable

La clase dedica bastante tiempo a corregir los argumentos que IDA muestra como a1, a2, a3, a4, etc.

Desde el bridge se llega a una llamada conceptual parecida a:

VulnerableFunction(
    0,
    Irp,
    0,
    &Status
);

La reconstrucción vista en clase:

Argumento Valor real observado Motivo
arg1 / RCX 0 el path solo llega si esa condición previa deja RCX = 0
arg2 / RDX PIRP Irp viene directo del dispatch WDM
arg3 / R8 0 viene como constante desde el bridge
arg4 / R9 puntero a Status = 0 local inicializada en cero y pasada por referencia

La idea práctica es renombrar esos argumentos en IDA para no arrastrar ruido:

arg1_const_zero
Irp
arg3_const_zero
status_ptr_zero

Tip de IDA

Si una call indirecta no salta bien al destino, se puede usar el plugin/opción de edición de call address para fijar manualmente la dirección y poder navegar directo a la función llamada.


3. Condiciones de entrada del IOCTL

Antes del switch/case del IOCTL se validan dos condiciones relevantes:

proceso de 32 bits
RequestorMode == UserMode

En Windows kernel:

Valor Modo
0 KernelMode
1 UserMode

Entonces el bug está en una ruta pensada para requests desde user-mode, específicamente para un proceso WOW/32-bit que llega al driver de thunking.

Camino relevante

if (Is32BitProcess && Irp->RequestorMode == UserMode) {
    switch (IoControlCode) {
        case 0x2F007:
            vulnerable_path(...);
    }
}

4. IO_STACK_LOCATION y campos de DeviceIoControl

Dentro de la vulnerable, lo primero importante es recuperar el current stack location del IRP.

En el decompiler puede aparecer como acceso manual a offset:

Irp + 0xB8

Ese campo corresponde a:

Irp->Tail.Overlay.CurrentStackLocation

En IDA conviene forzar el field correcto:

Force offset field -> Tail.Overlay.CurrentStackLocation

Después hay que corregir la union de IO_STACK_LOCATION, porque para esta ruta estamos en IRP_MJ_DEVICE_CONTROL.

Los campos importantes son:

stack->Parameters.DeviceIoControl.InputBufferLength;
stack->Parameters.DeviceIoControl.OutputBufferLength;
stack->Parameters.DeviceIoControl.Type3InputBuffer;
Irp->UserBuffer;

Offsets confirmados en ThunkEnableEventIrp

En la función 0x1c0002ab8, el assembly muestra estos accesos:

Dirección Instrucción / acceso Interpretación
0x1c0002b08 mov rcx, [rdx+0B8h] Irp->Tail.Overlay.CurrentStackLocation
0x1c0002b14 mov r13d, [rcx+10h] InputBufferLength
0x1c0002b18 mov r12d, [rcx+8] OutputBufferLength
0x1c0002b5a mov rcx, [rcx+20h] Type3InputBuffer como input user-mode
0x1c0002dd5 mov rcx, [rsi+70h] Irp->UserBuffer como output user-mode

IDA puede mostrar nombres de otra union, como Parameters.Read.Length o Parameters.Create.Options, porque IO_STACK_LOCATION.Parameters es una union. Para esta ruta hay que leerlos mentalmente como campos de DeviceIoControl.

METHOD_NEITHER

La clase remarca que en esta ruta el driver trata buffers estilo METHOD_NEITHER:

  • Type3InputBuffer contiene el input user-mode;
  • UserBuffer se usa como output user-mode;
  • no hay SystemBuffer útil como en METHOD_BUFFERED;
  • el driver debe validar manualmente punteros y tamaños.

5. Valores del PoC: input y output sizes

El PoC llama a DeviceIoControl con valores elegidos para disparar el bug.

Valores vistos en clase:

Campo Valor aproximado Uso
InputBufferLength 0x1000 tamaño normal del input
OutputBufferLength 0xFFFFFFF0 valor grande/negativo en 32-bit
Type3InputBuffer buffer user-mode controlado input
UserBuffer output buffer user-mode controlado salida y fuente posterior del copy

El input de 0x1000 es válido y no busca fallar. El valor interesante es el output size:

0xFFFFFFF0

Ese valor está elegido para que algunas sumas/wrapping lo conviertan en un tamaño chico al momento de validar, pero siga representando un tamaño enorme cuando se usa como longitud de copia.

El binario también permite entender por qué otras variantes cercanas funcionan. Por ejemplo, si OutputBufferLength = 0xFFFFFFF1, el cálculo de alineación queda en 0x8; si OutputBufferLength = 0xFFFFFFF0, queda en 0x0. En ambos casos el problema es el mismo: el check usa arithmetic de 32 bits después del wrap.


6. El integer overflow del OutputBufferLength

El patrón vulnerable aparece cuando el driver intenta alinear o validar el output size:

aligned = (OutputBufferLength + 0x17) & 0xFFFFFFF8;

En assembly:

0x1c0002b1c  lea eax, [r12+17h]
0x1c0002b21  and eax, 0FFFFFFF8h
0x1c0002b24  mov [rsp+Size], eax

Con el valor del PoC:

OutputBufferLength = 0xFFFFFFF0
OutputBufferLength + 0x10 -> 0x00000000   // wrap en 32-bit
OutputBufferLength + 0x17 -> valor chico/alineado

La clase lo valida en debugger: después de sumar y aplicar el AND, el valor queda chico o incluso 0, entonces pasa checks que estaban pensados para tamaños normales.

Check defectuoso

Un patrón observado:

AlignedOutputLength = (OutputBufferLength + 0x17) & ~7
tmp = OutputBufferLength + 0x10

if (AlignedOutputLength < tmp) {
    ExRaiseStatus(STATUS_INVALID_BUFFER_SIZE)
}

El problema es que el check mira el resultado después del wrap, no el tamaño original.

En el binario vulnerable:

0x1c0002d86  mov r15d, [rsp+Size]      ; AlignedOutputLength
0x1c0002d8e  lea eax, [r12+10h]        ; OutputBufferLength + 0x10, truncado a 32 bits
0x1c0002d93  cmp r15d, eax
0x1c0002d96  jb  invalid_buffer_size

Con OutputBufferLength = 0xFFFFFFF0:

AlignedOutputLength = 0
OutputBufferLength + 0x10 = 0
0 < 0  -> false

Entonces el check no detecta el tamaño malicioso.

Lectura de reversing

El bug no es simplemente “copiar mucho”. La causa primaria es:

integer overflow / wraparound en cálculo de size
validación pasa con tamaño chico
copia usa tamaño grande controlado
pool overflow

7. ProbeForRead, try/except y por qué no hay BSoD inmediato

La función usa ProbeForRead para validar punteros user-mode.

Puntos importantes de la clase:

  • ProbeForRead no garantiza que toda la lógica posterior sea segura.
  • Valida que el rango user-mode sea accesible según dirección y tamaño.
  • Si falla, levanta excepción.
  • Como el código está dentro de try/except, la excepción se captura.
  • El driver sale con error en vez de provocar BSoD directo.

Forma conceptual:

__try {
    ProbeForRead(user_buffer, size, alignment);
    ...
    memmove(dst, src, huge_size);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
    status = GetExceptionCode();
}

Esto explica por qué el exploit puede escribir lo suficiente para corromper el pool y después caer en exception handling sin tumbar inmediatamente la máquina.

Idea clave

La excepción ocurre después de que parte de la copia ya pisó memoria adyacente. El except evita que el sistema muera ahí, pero no deshace la corrupción.


8. Allocation vulnerable en pool

Después de pasar los checks, el driver aloca un buffer de pool.

En la clase se ve una allocation de tamaño:

0x1000

con tag asociado al componente Kernel Streaming.

Ejemplo de !pool visto conceptualmente:

pool block: size 0x1000, tag KSpp

En IDA/MCP el tag aparece como inmediato:

0x7070534B -> bytes 4B 53 70 70 -> "KSpp"

El puntero a esa allocation se guarda reusando un campo del IRP que para esta ruta no se usa realmente.

Reuso de MasterIrp

En el decompiler puede aparecer como:

Irp->AssociatedIrp.MasterIrp = allocated_buffer;

Pero en una ruta METHOD_NEITHER, ese campo no tiene el mismo significado que en otras rutas. La clase lo interpreta como una variable temporal/reuso interno del driver.

Condición previa

El código comprueba que ese campo esté en 0 antes de seguir:

if (MasterIrp != 0) error;

Como en esta ruta viene en 0, el driver lo reutiliza para guardar el puntero de pool.

Allocation exacta confirmada

La rama vulnerable llega a:

0x1c0002d9c  lea eax, [r15+r13]      ; AlignedOutputLength + InputBufferLength
0x1c0002dab  mov ecx, 61h            ; flags de ExAllocatePool2
0x1c0002db0  mov r8d, 7070534Bh      ; tag "KSpp"
0x1c0002db6  call ExAllocatePool2
0x1c0002dc2  mov [rsi+18h], rax      ; Irp->AssociatedIrp.MasterIrp = pool

Para el caso de clase:

AlignedOutputLength = 0
InputBufferLength   = 0x1000
AllocationSize      = 0x1000

9. El memmove que provoca el overflow

El overflow ocurre en una copia hacia el buffer de pool de 0x1000.

La forma mental:

memmove(
    allocated_pool_buffer + 0x20,
    user_output_buffer + 0x10,
    OutputBufferLength - 0x10
);

Direcciones confirmadas:

0x1c0002e39  lea r8,  [r12-10h]       ; size = OutputBufferLength - 0x10
0x1c0002e3e  mov rdx, [rsi+70h]
0x1c0002e42  add rdx, 10h             ; src = Irp->UserBuffer + 0x10
0x1c0002e46  lea rcx, [r9+20h]        ; dst = pool + 0x20
0x1c0002e4a  call memmove

El destino pertenece a una allocation de 0x1000, pero el size usado viene del output length enorme.

El resultado:

[KSpp allocation 0x1000][siguiente chunk NpFr][...]
          overflow ---------------> pisa header del siguiente chunk

Por qué está calculado “justo”

El output buffer user-mode se prepara para que:

  • exista y sea accesible durante la parte inicial;
  • contenga bytes controlados;
  • al copiar más de la cuenta, se pise el chunk siguiente;
  • luego ocurra una excepción controlada cuando se intenta seguir leyendo/escribiendo fuera de rango.

La corrupción útil ya ocurrió antes del exception handler.


10. Confirmación en debugger

La clase usa IDA debugger conectado a kernel para validar lo visto estáticamente.

Problema práctico de IDA

Para ver estructuras apuntadas por registros, a veces IDA no deja dereferenciar si no tiene región de memoria cargada. El workaround mostrado:

Manual Memory Regions -> agregar rango 0 .. FFFFFFFFFFFFFFFF

Después se puede ir a registros como RDX y aplicar structs.

Registros importantes al entrar

Registro Significado
RCX 0
RDX PIRP
R8 0
R9 puntero a status

Validaciones hechas en runtime

  • RDX efectivamente apunta a un IRP.
  • CurrentStackLocation se levanta desde el IRP.
  • InputBufferLength = 0x1000.
  • OutputBufferLength = 0xFFFFFFF0.
  • El cálculo de alineación/wrap produce el valor chico esperado.
  • MasterIrp está en 0 al entrar.
  • ProbeForRead del input pasa.
  • La allocation de 0x1000 aparece en pool.
  • El chunk siguiente puede ser un NpFr de pipe.
  • El overflow real está en 0x1c0002e4a, no en la copia posterior de input.

Comandos útiles

!pool <address>

Sirve para confirmar tag, tamaño y chunk adyacente.


11. Grooming: por qué usar named pipes

Para convertir el overflow en una primitiva útil, el exploit necesita controlar qué objeto queda después de la allocation vulnerable.

El PoC usa named pipes porque permiten crear muchas allocations de tamaño parecido y bastante predecibles.

Objetivo del grooming

spray de pipes
liberar un hueco
trigger del driver vulnerable
allocation KSpp cae en el hueco
pipe NpFr queda justo después
overflow pisa header del pipe

Tag observado

El tag de los buffers de named pipe es:

NpFr

En memoria como entero little-endian suele verse invertido, por lo que siempre conviene pensar tanto en ASCII como en valor entero.

Por qué tamaño 0x1000

La clase remarca que allocations alrededor de 0x1000 tienden a ir por el pool estándar, no por Low Fragmentation Heap/pool para tamaños chicos. Eso hace más predecible el layout.

Regla práctica mencionada:

Tamaño Comportamiento esperado
chico, por debajo de cierto umbral más probable LFH / comportamiento menos lineal
0x1000 aprox. pool estándar, más secuencial/predecible

12. Pool tags y cómo frenar en allocations NpFr

La clase explica dos formas de frenar cuando se aloca un tag específico.

Variable global nt!PoolHitTag

WinDbg permite configurar un tag global para que el kernel dispare una excepción controlada cuando una allocation usa ese tag.

Comando conceptual:

ed nt!PoolHitTag <tag_little_endian>

Para NpFr, hay que escribir el valor entero correspondiente al tag en little-endian.

Ventaja:

  • no es un breakpoint sobre una función ultra usada;
  • la comprobación está integrada en la ruta de allocation.

Desventajas:

  • no se puede filtrar por proceso;
  • puede parar muchas veces si el tag es común;
  • conviene activarlo cerca del momento del spray.

Técnica práctica mencionada

Si el tag aparece demasiado:

  1. Preparar el comando ed nt!PoolHitTag ....
  2. Poner un MessageBox o pausa en user-mode antes del spray.
  3. Al continuar, activar el tag justo antes del momento interesante.
  4. Dejar que pare en las primeras allocations cercanas al spray.

13. Breakpoint genérico vs breakpoint dentro de npfs.sys

Otra opción es poner un breakpoint en la función genérica de pool:

nt!ExAllocatePool2

El problema: es demasiado usada por todo el sistema.

Incluso con condición por tag, el debugger puede quedar lento o inusable porque el breakpoint se evalúa constantemente.

Breakpoint filtrado por proceso

WinDbg permite usar /p para limitar por proceso:

bp /p <EPROCESS> nt!ExAllocatePool2 "...condición..."

Pero en la práctica sigue siendo incómodo si hay muchas allocations o si el proceso hace otras operaciones del sistema.

Mejor estrategia

Una vez ubicada la call específica dentro de npfs.sys, conviene poner el breakpoint ahí, no en nt!ExAllocatePool2 global.

npfs!<función_que_aloca_NpFr> + offset_call_ExAllocatePool2

Ahí el breakpoint condicional sí es manejable, porque se evalúa en una ruta mucho más específica.


14. Qué pasa cuando se escriben pipes

La clase aclara un punto importante: la allocation NpFr no ocurre cuando se crea el pipe, sino cuando se escribe data.

Flujo user-mode

CreateNamedPipe / CreateFile
obtengo handles
WriteFile
npfs.sys aloca buffer NpFr para guardar data

Crear el pipe solo prepara objetos/handles. Para que exista la allocation de data con el tamaño elegido, hay que escribir.

Handles de pipe

Un mismo pipe tiene típicamente:

Handle Uso
read handle leer desde el pipe
write handle escribir al pipe

El PoC guarda handles en arrays, por ejemplo:

pipes[i].read
pipes[i].write

Luego usa WriteFile para crear las allocations y ReadFile/PeekNamedPipe para inspeccionar o consumir data.


15. Estructuras de npfs.sys con ayuda de ReactOS

npfs.sys no trae símbolos tan ricos como ksthunk.sys. Muchas funciones solo tienen nombre, sin prototipos ni argumentos útiles.

Para reconstruir estructuras, la clase usa ReactOS como referencia.

La función relevante se identifica como algo parecido a:

NpAddDataQueueEntry

ReactOS ayuda a entender nombres y layouts aproximados:

NP_DATA_QUEUE_ENTRY
NP_DATA_QUEUE
NP_CCB
NP_FCB

No hay que asumir que Windows y ReactOS son idénticos, pero sirve muchísimo para orientar el reversing.

Problema de alineación

Al copiar estructuras desde ReactOS o reconstruirlas manualmente en IDA, puede fallar la alineación.

Síntomas:

  • campos no coinciden con accesos reales;
  • quedan bytes vacíos inesperados;
  • offsets del decompiler no matchean;
  • un campo que debería ser IRP no apunta a un IRP válido.

La clase recomienda verificar cada campo contra runtime:

dt / object view / !irp / inspección de punteros

Si un campo supuestamente es PIRP, hay que comprobar que realmente apunta a una estructura IRP válida.


16. Objetivo del overflow: pisar DataSize

El objeto de pipe tiene un header antes de la data. La estructura conceptual:

typedef struct _NP_DATA_QUEUE_ENTRY_LIKE {
    LIST_ENTRY QueueEntry;
    PIRP Irp;
    ULONG DataEntryType;
    ULONG QuotaInEntry;
    ULONG DataSize;
    UCHAR Data[];
} NP_DATA_QUEUE_ENTRY_LIKE;

La clase remarca que el target del overflow es el campo:

DataSize

Antes del overflow, el pipe fue escrito con tamaño cercano a 0x1000. Después del overflow, el exploit busca cambiar ese size a algo mayor, por ejemplo:

0x4000

Layout mental

chunk vulnerable KSpp, size 0x1000
  [datos controlados ...]
  overflow sale del chunk
chunk pipe NpFr
  [NP_DATA_QUEUE_ENTRY header]
       ... DataSize <- campo pisado
  [Data[]]

Por qué sirve pisar DataSize

Si el pipe cree que tiene más data de la real, operaciones de lectura/peek pueden leer más allá de los límites originales del buffer.

Eso abre la puerta a:

  • leak de memoria adyacente;
  • identificación del pipe corrompido;
  • construcción posterior de primitivas de read/write;
  • recuperación de punteros kernel necesarios para la explotación.

17. Detección del pipe overflowdeado

El PoC mantiene un array de muchos pipes. Después de disparar el overflow, necesita encontrar cuál quedó corrompido.

La idea:

for each pipe in pipes:
    size = PeekNamedPipe(pipe)
    if size > expected_threshold:
        overflowed_index = i
        break

En la clase se menciona una comparación contra un umbral derivado de:

0x4000 - 0x100

El único pipe que reporta un tamaño anormalmente grande es el que quedó después del chunk vulnerable y cuyo DataSize fue pisado.

Intuición

pipe normal       -> DataSize ~= 0x1000
pipe overflowed   -> DataSize ~= 0x4000

Esto permite transformar una corrupción probabilística en un objeto identificado dentro del array de handles.


18. Plan para doble debugging user/kernel

La clase termina preparando el siguiente paso: debuggear a la vez el PoC user-mode y el kernel.

Objetivo

Ver simultáneamente:

  • qué hace el PoC en user-mode;
  • cuándo crea pipes;
  • cuándo escribe pipes;
  • cuándo llama al IOCTL vulnerable;
  • cómo cambia el pool en kernel;
  • qué campo se pisa en NpFr;
  • cómo se detecta el pipe corrompido;
  • cómo se avanza hacia leak y token stealing.

Setup conceptual

IDA kernel debugger  -> conectado a la VM para ksthunk.sys / npfs.sys
IDA user debugger    -> conectado al PoC de 32 bits

Como el PoC es de 32 bits, se usa el servidor/debugger remoto Win32 de IDA para user-mode.

Por qué hacerlo así

El bug cruza capas:

PoC user-mode
   -> DeviceIoControl
   -> ksthunk.sys overflow
   -> pool layout
   -> npfs.sys pipe object
   -> leak / write primitive

Si se mira solo kernel, cuesta entender qué fase del PoC está corriendo. Si se mira solo user-mode, no se ve qué objeto kernel se corrompe.


19. Workflow mental completo

1. Partir del IOCTL 0x2F007 identificado en clase 1
2. Reconstruir argumentos: 0, IRP, 0, &status
3. Confirmar UserMode + proceso 32-bit
4. Recuperar CurrentStackLocation desde Irp->Tail.Overlay
5. Cambiar la union a DeviceIoControl
6. Leer InputBufferLength = 0x1000
7. Leer OutputBufferLength = 0xFFFFFFF0
8. Ver wraparound al sumar/alinear OutputBufferLength
9. Confirmar que los checks pasan por el tamaño wrapped
10. Confirmar allocation KSpp de 0x1000
11. Preparar spray/grooming con pipes NpFr de tamaño parecido
12. Ubicar chunk NpFr adyacente con !pool
13. Llegar al memmove que copia tamaño enorme
14. Ver que pisa el header del siguiente NpFr
15. Reconstruir NP_DATA_QUEUE_ENTRY con ayuda de ReactOS
16. Identificar DataSize como campo objetivo
17. Cambiar DataSize de ~0x1000 a ~0x4000
18. Usar PeekNamedPipe/lecturas para encontrar el pipe corrompido
19. Preparar doble debugging para estudiar leak y escritura posteriores

20. Cheat sheet

Valores clave

Valor Significado
0x2F007 IOCTL vulnerable
0x1000 input size y tamaño de allocation vulnerable
0xFFFFFFF0 output size malicioso que provoca wrap
0x4000 tamaño grande usado para marcar pipe corrompido
KSpp / 0x7070534B tag de allocation del componente vulnerable
KSqb / 0x6271534B tag del query buffer usado en la rama sincrónica 0x400
NpFr tag de buffer/data entry de named pipes

Funciones verificadas en IDA

Dirección Función
0x1c0001320 CKernelFilterDevice::DispatchIrp
0x1c0001eb0 CKSThunkDevice::DispatchIoctl
0x1c00020f0 CKSThunkPin::DispatchIoctl
0x1c0002ab8 CKSAutomationThunk::ThunkEnableEventIrp
0x1c0002e4a memmove vulnerable dentro de ThunkEnableEventIrp

Campos a corregir en IDA

Acceso Interpretación
Irp + 0xB8 Tail.Overlay.CurrentStackLocation
IO_STACK_LOCATION.Parameters elegir union DeviceIoControl
Type3InputBuffer input user-mode en METHOD_NEITHER
Irp->UserBuffer output user-mode
AssociatedIrp.MasterIrp campo reusado como temporal en esta ruta

Debugger

!pool <address>
ed nt!PoolHitTag <tag_little_endian>
bp /p <EPROCESS> nt!ExAllocatePool2 "..."

Pipe exploitation mental model

spray NpFr
free hole
allocate KSpp vulnerable chunk
overflow KSpp -> NpFr header
overwrite NpFr.DataSize
find corrupted pipe with PeekNamedPipe
use corrupted pipe for leak/read/write stages

Errores comunes al reversear esta ruta

  • Confundir METHOD_BUFFERED con METHOD_NEITHER.
  • Tomar SystemBuffer como válido cuando la ruta usa Type3InputBuffer/UserBuffer.
  • Creer que ProbeForRead evita el overflow.
  • Mirar el size después del wrap y olvidar el valor original.
  • Poner breakpoints globales en ExAllocatePool2 y congelar el sistema.
  • Copiar structs de ReactOS sin verificar offsets/alineación.
  • Cerrar/liberar objetos corruptos antes de restaurar o controlar su estado.

21. Glosario corto

Término Descripción
METHOD_NEITHER Método de IOCTL donde el driver recibe punteros user-mode directos y debe validarlos manualmente.
Type3InputBuffer Puntero al input user-mode para rutas METHOD_NEITHER.
UserBuffer Puntero user-mode usado como output buffer.
ProbeForRead API kernel para validar lectura desde un rango user-mode; puede levantar excepción.
Integer overflow / wraparound Cálculo de tamaño que se desborda y vuelve a un valor chico.
Pool grooming Técnica para influir qué objetos quedan adyacentes en pool.
NpFr Pool tag asociado a buffers/data entries de named pipes.
KSpp Tag observado para la allocation vulnerable en la ruta Kernel Streaming.
PoolHitTag Variable global de kernel para frenar cuando se aloca un tag específico.
NP_DATA_QUEUE_ENTRY Estructura conceptual de npfs.sys que representa una entrada de data de pipe.
DataSize Campo del data entry de pipe que indica cuántos bytes cree tener disponibles.
PeekNamedPipe API user-mode para consultar datos disponibles en un pipe sin consumirlos completamente.

Apunte basado en la transcripción clase-modulo6-2 del Módulo 6. La clase conecta el IOCTL vulnerable con el integer overflow real, el pool overflow y el uso de named pipes NpFr como objeto de grooming para convertir la corrupción en primitivas explotables.