Clase 1 — Patch diffing de ksthunk.sys y reversing inicial del filter driver¶
← Volver al Módulo 6¶
Resumen: en esta clase se arranca desde el diff entre una versión vulnerable y una versión parcheada de
ksthunk.sys/fstunc.sys, se usa BinDiff para encontrar el cambio real del parche, se interpreta el gateIsEnableDeviceUsage, y después se empieza a reconstruir el driver como código C++:DriverEntry, clases, vtables,AddDevice, device stack, dispatch de IRPs y cómo llegar al IOCTL vulnerable.
Tabla de Contenidos¶
- Contexto del caso
- Workflow correcto para patch diffing
IsEnableDeviceUsage: el gate típico de Microsoft- Cómo leer el diff en BinDiff
- Corregir matches incorrectos
- Identificar el bloque real del parche
- Reversing desde
DriverEntry - Reconstrucción de clases C++ en IDA
CKernelFilterDriver::InitializeAddDevicey creación del filter device- Filter drivers y device stack
DispatchIrpBridgey ruta hacia el IOCTL- Por qué no aparece un
\\.\DeviceNamedirecto - Obtener el symbolic link con GUID
- WinDbg: confirmar el stack de devices
- Workflow mental completo
- Cheat sheet
- Glosario corto
1. Contexto del caso¶
El caso de estudio del módulo es un parche real de Microsoft para el componente Kernel Streaming WOW Thunk Service Driver.
En la clase aparecen nombres como:
La idea principal no es adivinar la vulnerabilidad desde cero, sino comparar dos binarios:
| Versión | Rol |
|---|---|
| Binario viejo | Versión vulnerable o previa al parche |
| Binario nuevo | Versión parcheada |
El diff permite responder:
- qué función cambió;
- qué bloque nuevo agregó Microsoft;
- qué condición valida el parche;
- qué ruta vieja quedó protegida por una condición nueva;
- cómo se llega a esa función desde user-mode.
El bug del CVE está asociado a una ruta de IOCTL dentro de un driver de Kernel Streaming, pero la clase se enfoca primero en ubicar y entender el patch.
2. Workflow correcto para patch diffing¶
El workflow recomendado es:
- Conseguir la versión vulnerable o inmediatamente anterior.
- Conseguir la versión parcheada.
- Abrir primero la versión parcheada en IDA.
- Esperar que termine el auto-analysis.
- Guardar la base
.i64. - Abrir la versión vulnerable en IDA.
- Guardar su base
.i64. - Correr BinDiff entre ambas bases.
- Revisar funciones con similitud menor a
1.0y funciones unmatched.
La clase recalca que conviene comparar contra la versión más cercana posible al parche. Si se compara contra una build demasiado vieja, el diff queda lleno de ruido: cambios de refactor, cambios de build, funciones movidas, fixes viejos y diferencias que no pertenecen al CVE.
Orden práctico¶
Qué se busca primero¶
- Funciones nuevas en el binario parcheado.
- Funciones viejas modificadas.
- Basic blocks nuevos con checks de tamaño.
- Branches nuevos que separan ruta vulnerable y ruta parcheada.
- Calls a funciones con nombre tipo
IsEnable....
3. IsEnableDeviceUsage: el gate típico de Microsoft¶
Uno de los puntos más importantes de la clase es el patrón IsEnableDeviceUsage.
Microsoft muchas veces no elimina directamente la ruta vieja. En vez de eso, introduce un feature gate:
En la práctica:
| Retorno | Significado práctico |
|---|---|
0 |
tomar ruta vieja / no parcheada |
1 |
tomar ruta nueva / parcheada |
Esto es útil para Microsoft porque puede controlar comportamiento con feature flags, compatibilidad o rollout interno. Para reversing es muy útil porque el gate marca dónde está el fix.
Por qué mirar funciones IsEnable*¶
Cuando en el binario parcheado aparece una función nueva tipo:
o una variante con sufijo numérico, suele ser una señal fuerte de que hay una mitigación o un cambio de comportamiento agregado por el parche.
Cuidado con gates viejos¶
No todo IsEnable* es del parche actual.
Puede haber gates agregados en meses anteriores. Si están en ambos binarios, BinDiff los va a matchear como funciones comunes. Para el parche actual interesa especialmente:
- función
IsEnable*presente solo en el binario nuevo; - call nuevo hacia ese gate;
- bloque nuevo que depende del retorno;
- validación nueva detrás del branch patched.
4. Cómo leer el diff en BinDiff¶
En BinDiff hay varias vistas útiles, pero en la clase se remarca especialmente Secondary unmatched.
La idea:
- abrir el diff;
- mirar unmatched functions;
- prestar atención a las funciones que existen solo en la versión parcheada;
- buscar
IsEnableDeviceUsageo funciones similares; - saltar desde esa función hacia sus xrefs/callers.
Lectura mental del diff¶
Si el binario nuevo tiene:
y el viejo no, entonces el camino es:
función nueva IsEnable...
↑ xref
función modificada que llama al gate
↑ revisar basic blocks
bloque con check nuevo
Por qué no quedarse solo con similarity¶
Una función puede tener similarity alta y aun así contener el fix real. Un parche de seguridad puede ser apenas:
Eso cambia pocos bytes, pero semánticamente es todo el parche.
5. Corregir matches incorrectos¶
BinDiff no es perfecto. Puede matchear mal funciones parecidas o basic blocks parecidos.
La clase muestra que hay que corregir el diff manualmente cuando el match no tiene sentido.
Correcciones típicas¶
| Problema | Acción |
|---|---|
| Función vieja matcheada contra función nueva equivocada | borrar el match incorrecto |
| Función correspondiente quedó unmatched | agregar match manual |
| Basic blocks similares pero no equivalentes | borrar match de bloque |
| Bloque nuevo del patch quedó mezclado con bloque viejo | separar y revisar en Flow Graph |
Regla práctica¶
Si el graph no cierra visualmente, hay que desconfiar del match automático.
La herramienta ayuda, pero la interpretación final la hace el reverser.
6. Identificar el bloque real del parche¶
Una vez ubicado el caller de IsEnableDeviceUsage, se mira la función en Flow Graph.
La forma general observada en clase es:
call IsEnableDeviceUsage
test eax, eax
jz old_path
patched_path:
nuevo check de size / límites
si falla -> error
si pasa -> continuar
old_path:
comportamiento anterior
El bloque interesante suele ser el que aparece solo en el binario parcheado. En este caso, la corrección está asociada a un size check antes de llegar a la operación vulnerable.
Cómo reconocer el bloque de parche¶
Buscar:
- comparación nueva contra un tamaño máximo;
- branch nuevo hacia error;
- constantes nuevas usadas como límite;
- return de error tipo
STATUS_INVALID_PARAMETER; - separación explícita entre patched path y old path.
Probar comportamiento parcheado vs viejo¶
En una VM parcheada se puede observar la rama tomada. Si el objetivo es estudiar la ruta vieja, se puede forzar el branch en debugger o alterar temporalmente el retorno del gate para análisis.
Punto importante de la clase: si hay varios gates del mismo parche, no alcanza con forzar uno solo. Puede haber que invertir o forzar todas las decisiones relacionadas para reproducir la ruta anterior.
7. Reversing desde DriverEntry¶
Después de ubicar el patch, la clase vuelve al inicio del driver para reconstruir la arquitectura.
La función inicial es:
En Windows drivers, DriverEntry cumple un rol parecido a main, pero para kernel-mode.
Qué buscar en DriverEntry¶
- Inicialización de globals.
- Allocations de objetos C++.
- Constructors manuales o inlined.
- Inicialización de vtables.
- Registro de callbacks del
DriverObject. DriverUnload.AddDevice.- Tabla
MajorFunction[].
En la clase se observa una allocation chica, de tamaño 0x18, que corresponde al objeto principal del driver:
Ese objeto después se inicializa y se guarda asociado al DriverObject.
8. Reconstrucción de clases C++ en IDA¶
El driver está escrito con patrones de C++: objetos, métodos, vtables y herencia.
Para ordenar el decompiler, se reconstruyen estructuras.
Plugin HRT / Kaspersky¶
En la clase se usa un plugin tipo HRT/Kaspersky para IDA que ayuda a crear structs a partir de allocations.
Ejemplo mental:
ExAllocatePoolWithTag(..., 0x18, ...)
↓
Create dummy struct
↓
struct S18 { ... }
↓
rename a CKernelFilterDriver
El plugin puede crear:
- struct dummy por tamaño;
- campo de vtable;
- tipos auxiliares;
- funciones virtuales aproximadas.
Renombrar el struct¶
Un nombre inicial tipo:
se renombra a algo semántico:
La clase también menciona el truco de usar typedefs para vincular nombres reconstruidos con nombres esperados por símbolos o tipos de Microsoft.
Objetivo real¶
No se busca que la estructura sea perfecta desde el principio. Se busca que el pseudocódigo deje de verse como punteros crudos y empiece a expresar intención:
en vez de:
(*(void (__fastcall **)(__int64, PDRIVER_OBJECT, PUNICODE_STRING))(*(_QWORD *)obj + 0x10))(obj, DriverObject, RegistryPath);
9. CKernelFilterDriver::Initialize¶
La función de inicialización del objeto principal configura el driver real.
En la clase se reconstruye algo parecido a:
Operaciones importantes¶
- Llama a
IoAllocateDriverObjectExtension. - Reserva una extensión de
8bytes asociada alDriverObject. - Guarda un puntero al objeto
CKernelFilterDriveren esa extensión. - Inicializa callbacks del
DriverObject. - Rellena
MajorFunction[]con un puente común. - Registra
AddDevice. - Registra
DriverUnload.
IoAllocateDriverObjectExtension¶
El patrón observado:
IoAllocateDriverObjectExtension(
DriverObject,
ClientIdentificationAddress,
8,
&DriverObjectExtension
);
El tamaño 8 sugiere que la extensión guarda un solo puntero:
Esto permite recuperar el objeto C++ desde callbacks estáticos del modelo WDM.
Tabla MajorFunction[]¶
El driver rellena muchas entradas con la misma función puente:
En el transcript se menciona un loop de aproximadamente 0x1C entradas desde el offset de MajorFunction.
La idea es que distintos IRP major codes entren por el mismo bridge y después se redistribuyan según el tipo de request.
10. AddDevice y creación del filter device¶
En WDM/PnP, AddDevice se llama cuando el sistema enumera un device al que el driver debe asociarse.
Firma conceptual:
Qué hace en esta clase¶
La ruta reconstruida es:
IsDeviceFilterable¶
Consulta configuración/registry para decidir si el device concreto debe ser filtrado.
Esto es típico en filter drivers: no necesariamente se attachan a todo, sino a devices compatibles con cierta clase/interfaz.
CreateFilterDevice¶
La clase muestra una allocation de tamaño 0x80, que se interpreta como un objeto tipo:
Ese objeto se asocia con el DeviceObject creado por el driver.
11. Filter drivers y device stack¶
El detalle clave: el driver crea un device con nombre NULL.
Conceptualmente:
IoCreateDevice(
DriverObject,
DeviceExtensionSize,
NULL,
DeviceType,
DeviceCharacteristics,
FALSE,
&DeviceObject
);
Si DeviceName = NULL, no hay un nombre directo estilo:
Eso indica que no se trata de un device standalone pensado para abrirse directamente con CreateFile, sino de un filter device que se adjunta a un stack existente.
DeviceExtension¶
El objeto C++ del filtro se guarda en:
Después, cuando llega un IRP, el bridge recupera ese contexto desde el DeviceObject.
Attach al stack¶
La función importante es:
Esto mete al filter driver arriba de otro device en el stack.
Modelo mental¶
User-mode request
↓
Device interface / symbolic link
↓
Top of stack: KSTUNC filter device
↓
Lower driver: MSKSSRV
↓
Lower driver: SWENUM
El filtro intercepta o procesa IRPs que van hacia ese stack.
12. DispatchIrpBridge y ruta hacia el IOCTL¶
El driver registra una función puente para varios major functions.
La idea general:
NTSTATUS DispatchIrpBridge(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
FilterDevice *filter = (FilterDevice *)DeviceObject->DeviceExtension;
return filter->DispatchIrp(Irp);
}
El bridge convierte el callback estilo C de WDM en una llamada a método C++.
Ruta hacia device control¶
El camino relevante es:
DispatchIrpBridge
-> FilterDevice::DispatchIrp
-> switch (MajorFunction)
-> IRP_MJ_DEVICE_CONTROL
-> IOCTL dispatcher
-> case 0x2F007
-> función vulnerable / parcheada
IOCTL mencionado¶
En la clase aparece el IOCTL:
Ese valor es importante porque conecta la superficie user-mode con la función donde se ve el patch.
Qué mirar en IDA¶
IoGetCurrentIrpStackLocationo acceso manual aIrp->Tail.Overlay.CurrentStackLocation.MajorFunction.Parameters.DeviceIoControl.IoControlCode.- switch/cases sobre IOCTLs.
- branch hacia el caso
0x2F007. - validaciones nuevas agregadas por el patch.
13. Por qué no aparece un \\.\DeviceName directo¶
En drivers simples, como ejemplos de HEVD, suele existir:
porque el driver crea un device con nombre y symbolic link propio.
En este caso no es así.
La clase remarca que IoCreateDevice recibe DeviceName = NULL, entonces buscar un string directo tipo \\.\KSTUNC no alcanza.
Consecuencia¶
Para hablar con el driver desde user-mode hay que abrir el device expuesto por la interfaz de dispositivo del stack, no un nombre directo inventado por el driver.
El symbolic link lo administra Plug and Play / Configuration Manager en base a un GUID de interfaz.
14. Obtener el symbolic link con GUID¶
La clase muestra que se puede obtener el link usando APIs de Configuration Manager / SetupAPI.
Una API mencionada es:
El flujo conceptual:
GUID de interfaz
↓
CM_Get_Device_Interface_List_Size
↓
CM_Get_Device_Interface_List
↓
lista de symbolic links
↓
CreateFile(link, ...)
Qué representa el GUID¶
El GUID corresponde a una interfaz relacionada con Microsoft Streaming / Kernel Streaming. No es simplemente el GUID del driver, sino de la clase/interfaz de device expuesta por el stack.
Cómo se ve el link¶
Los links de device interface suelen verse largos, por ejemplo:
No hay que memorizarlos a mano. La forma robusta es enumerar por GUID.
Relación con el filter driver¶
Aunque el handle se abre contra la interfaz del stack, el IRP baja/sube por el stack y puede llegar al filter driver KSTUNC, que es donde está el dispatch relevante.
15. WinDbg: confirmar el stack de devices¶
Para confirmar runtime qué device object se está usando, la clase propone romper en el dispatch y mirar el stack.
Breakpoint mental¶
Cuando se corta, el primer argumento en x64 Windows está en rcx:
Comandos útiles¶
Stack observado en clase¶
La salida mostraba una pila parecida a:
Interpretación:
KSTUNCestá arriba como filter driver.MSKSSRVaparece debajo como parte del servicio de Kernel Streaming.SWENUMestá más abajo como enumerador software.
Esto confirma que la ruta user-mode no abre un device propio de KSTUNC, sino una interfaz que termina pasando por ese stack.
16. Workflow mental completo¶
El camino completo de la clase se puede resumir así:
1. Tengo vulnerable.sys y patched.sys
2. Genero IDBs en IDA
3. Corro BinDiff
4. Busco Secondary unmatched
5. Encuentro IsEnableDeviceUsage en patched
6. Voy a sus xrefs
7. Identifico función modificada
8. Corrijo matches de BinDiff si hace falta
9. Miro Flow Graph
10. Separo old path vs patched path
11. Reconozco size check nuevo
12. Vuelvo a DriverEntry
13. Reconstruyo CKernelFilterDriver
14. Sigo Initialize -> AddDevice -> CreateFilterDevice
15. Entiendo que es filter driver con DeviceName NULL
16. Encuentro DispatchIrpBridge
17. Sigo IRP_MJ_DEVICE_CONTROL -> IOCTL 0x2F007
18. Enumero device interface por GUID para hablar desde user-mode
19. Confirmo el stack con !devstack
La parte más importante es no analizar la función vulnerable aislada. Hay que unir tres vistas:
| Vista | Pregunta que responde |
|---|---|
| Patch diff | ¿Qué cambió? |
| Reversing estático | ¿Cómo está estructurado el driver? |
| Debugging runtime | ¿Cómo llego a esa ruta desde user-mode? |
17. Cheat sheet¶
BinDiff¶
| Elemento | Uso |
|---|---|
Secondary unmatched |
encontrar funciones nuevas del binario parcheado |
IsEnable* |
buscar gates de Microsoft |
Similarity < 1.0 |
encontrar funciones modificadas |
| Flow Graph | ver bloques nuevos y branches |
| Manual matching | corregir errores del diff automático |
Señales de parche real¶
- Call nuevo a
IsEnableDeviceUsage. - Branch nuevo según retorno del gate.
- Check nuevo de tamaño/límite.
- Camino de error nuevo.
- Bloque presente solo en patched.
- Función alcanzable desde IOCTL.
WDM / C++ driver¶
| Patrón | Significado |
|---|---|
DriverEntry |
entrada principal del driver |
IoAllocateDriverObjectExtension |
guardar contexto C++ asociado al DriverObject |
MajorFunction[] |
tabla de dispatch por IRP major code |
AddDevice |
callback PnP para adjuntar devices |
IoCreateDevice(..., NULL, ...) |
device sin nombre directo; probable filter device |
DeviceObject->DeviceExtension |
contexto privado del device |
IoAttachDeviceToDeviceStack |
attach del filtro al stack inferior |
DispatchIrpBridge |
puente C/WDM hacia método C++ |
WinDbg¶
User-mode¶
| Acción | API |
|---|---|
| Obtener tamaño de lista de interfaces | CM_Get_Device_Interface_List_Size |
| Obtener symbolic links | CM_Get_Device_Interface_List |
| Abrir handle | CreateFile |
| Enviar IOCTL | DeviceIoControl |
18. Glosario corto¶
| Término | Descripción |
|---|---|
| Patch diffing | Comparar binario vulnerable y parcheado para inferir el fix. |
| BinDiff | Herramienta para comparar bases de IDA y matchear funciones/basic blocks. |
| Unmatched function | Función que existe en una versión pero no fue matcheada en la otra. |
| Feature gate | Función/condición que activa o desactiva una ruta nueva. |
IsEnableDeviceUsage |
Gate usado por Microsoft para seleccionar ruta parcheada/no parcheada. |
| Filter driver | Driver que se adjunta arriba o abajo de otro device stack para interceptar IRPs. |
| Device stack | Cadena de DEVICE_OBJECTs por donde circula un IRP. |
AddDevice |
Callback PnP usado para crear/adjuntar el device object del driver. |
DeviceExtension |
Memoria privada asociada a un DEVICE_OBJECT. |
IRP_MJ_DEVICE_CONTROL |
Major function usada para IOCTLs. |
| Device interface GUID | GUID usado por PnP/Configuration Manager para enumerar symbolic links de devices. |
Apunte basado en la transcripción clase-modulo6-1 del Módulo 6. La clase conecta patch diffing real de Microsoft con reversing de un driver C++ WDM/filter y debugging de la ruta IOCTL.