Clase 1 — Setup, WinDbg y reversing inicial de drivers¶
Resumen: Esta clase cubre el setup completo del entorno de kernel debugging (BinDiff, WinDbg, símbolos), el repaso de memoria virtual y modos user/kernel, los comandos esenciales de WinDbg en sesión live, la transición user → kernel via SSDT (
KiServiceTableyW32pServiceTable), la carga del driver vulnerable HEVD con disable signature enforcement, y los primeros pasos del reversing delDriverEntryy la función dispatch en IDA Pro.
← Volver al Módulo 5¶
Tabla de Contenidos¶
- BinDiff — instalación correcta y uso
- WinBindex — encontrar versiones de archivos de Windows
- Memoria virtual — repaso
- Direcciones canónicas en x64
- User mode vs Kernel mode — por qué la separación
- Setup del lab y consejos para la VM target
- WinDbg — sesión live y comandos
- Multi-core debugging — usar 1 core
- Cargar símbolos en IDA desde WinDbg
- Transición User → Kernel y la SSDT
- Resolver syscalls de NT —
KiServiceTable - Resolver syscalls de Win32k —
W32pServiceTable - j00ru — tabla pública de syscalls
- Cargar el driver HEVD
- Encontrar el device name — WinObj y CreateFile
- Importar símbolos de
ntoskrnl.exeen IDA - Reversing de
DriverEntry - La tabla
MajorFunctiony la enumeraciónIRP_MJ_* - La función Dispatch — el verdadero punto de entrada
IO_STACK_LOCATION— el problema de la union- Cheat sheet rápida
A. Apéndice A — enum
Major_Codespara IDA
1. BinDiff — instalación correcta y uso¶
BinDiff se usa para hacer patch diffing entre la versión vulnerable y la parcheada de un binario. La regla de oro:
Siempre comparar la versión vulnerable contra la versión inmediatamente posterior parcheada. Si se eligen versiones con varios builds de diferencia, va a haber demasiadas funciones cambiadas y la diff va a ser inutilizable.
Instalación — los dos lugares donde reemplazar las DLLs¶
Es crítico reemplazar las DLLs corregidas en ambas ubicaciones, porque IDA al arrancar pisa con las viejas si solo se reemplazan en una:
| Ubicación | Para qué |
|---|---|
C:\Program Files\BinDiff\bin\ |
Carpeta de instalación de BinDiff |
C:\Program Files\BinDiff\Plugins\IDA Pro\ |
Plugin BinDiff para IDA |
C:\Users\<usuario>\AppData\Roaming\Hex-Rays\IDA Pro\plugins\ |
Plugins de IDA del usuario |
Archivos a reemplazar:
bindiff.exebindiff_config_setup.exebinexport2dump.exebinexport12_ida64.dllbindiff8_ida64.dll
Importante: El instalador del BinDiff viejo creaba 4 DLLs (32-bit + 64-bit). El nuevo solo trae las de 64-bit. Si hay sobrantes de 32-bit en la carpeta, borrarlas.
Por qué hace falta el instalador completo: BinDiff usa Java para mostrar la GUI de comparación gráfica. Si solo se reemplazan las DLLs sin instalar el MSI, no arranca el comparador.
Workflow para hacer un diff¶
- Abrir la versión parcheada (la nueva) en IDA primero. Esperar análisis. Guardar la base de datos. Cerrar IDA.
- Abrir la versión vulnerable (la vieja) en IDA. Esperar análisis.
- Edit → Plugin → BinDiff → Diff Database → seleccionar el
.i64parcheado. - Esperar resultados.
Interpretar los resultados¶
BinDiff abre varias ventanas. La más importante es Matched Functions:
- Score 1.0 → función idéntica, no cambió.
- Score < 1.0 → función modificada. Cuanto más bajo el score, más cambios.
- Functions in primary unmatched / secondary unmatched → funciones agregadas o eliminadas.
Tip: Ordenar la columna Similarity ascendente y enfocarse en los scores más bajos primero. Click derecho en una función → View Flow Graph para ver la diff visual:
Color Significado Amarillo Basic block modificado (instrucciones cambiadas) Gris Basic block agregado o eliminado Rosa Bloque sin match (huérfano)
Patrones típicos en patches de seguridad¶
| Cambio observado | Indica probable... |
|---|---|
strcpy → strncpy (con size) |
Buffer overflow parchado |
Bloque nuevo con cmp eax, 0xFF y salto de error |
Filtro de tamaño/rango agregado (validación faltante) |
Llamada a ProbeForRead / ProbeForWrite agregada |
Neither I/O sin validación parchado |
| Nueva verificación de NULL antes de un acceso | Null pointer dereference o UAF parchado |
2. WinBindex — encontrar versiones de archivos de Windows¶
WinBindex indexa casi todos los archivos de Windows (drivers, DLLs, EXEs del sistema) por versión, KB y build. Es la herramienta para conseguir la versión exacta del archivo objetivo y su versión inmediatamente parcheada para hacer diff.
Uso:
- Buscar el nombre del archivo (ej.
afd.sys,clfs.sys,win32k.sys). - Filtrar por arquitectura (
x64) y versión de Windows. - Descargar el SHA1 / SHA256 que coincide con el build deseado.
Limitación: Solo cubre archivos de Microsoft. Drivers de terceros no aparecen ahí.
3. Memoria virtual — repaso¶
Windows usa un espacio de direcciones virtual lineal que da a cada proceso la ilusión de tener su propio espacio privado.
| Concepto | Descripción |
|---|---|
| Memoria virtual | Vista lógica que puede no corresponder al layout físico |
| MMU (Memory Management Unit) | Traduce direcciones virtuales a físicas en runtime |
| Paging | Páginas no usadas se mueven al disco (pagefile.sys) cuando hace falta RAM |
Process A (virtual) Physical Memory Process B (virtual)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ page 0 │─────────────→│ frame X │←─────────│ page 0 │
│ page 1 │ │ frame Y │ │ page 1 │
│ page 2 │──┐ │ frame Z │ │ page 2 │
└──────────┘ └──────────→│ ... │ └──────────┘
└──────────┘
│
▼
┌────────────┐
│ pagefile │ ← páginas swappeadas a disco
└────────────┘
Importante: La memoria de un proceso no es contigua en RAM física. La MMU se encarga de mappear los pedazos. Por eso desde user no se puede asumir nada sobre la dirección física.
Profundización: Los detalles de cómo se traduce una virtual address a física (PTEs, PFN database, page tables de 4 niveles en x64) están fuera del alcance del módulo. Recurso recomendado: la documentación oficial del Windows Internals (Mark Russinovich).
4. Direcciones canónicas en x64¶
En x86-64 solo se usan 48 bits de dirección, no los 64 completos. Esto limita el espacio efectivo a 256 TB.
El bit 47 manda¶
| Bit 47 | Tipo de dirección |
|---|---|
0 |
User space |
1 |
Kernel space |
Los bits 63 a 48 deben ser una copia del bit 47 (sign extension). Si no lo son → NON-CANONICAL → la CPU tira fault inmediato.
0000 0000 0000 0000 ← inicio user space
...
0000 7FFF FFFF FFFF ← fin user space (≈128 TB)
╔══════════════════════════════════╗
║ Direcciones non-canonical ║ ← cualquier acceso → fault
║ (no se pueden usar) ║
╚══════════════════════════════════╝
FFFF 8000 0000 0000 ← inicio kernel space
...
FFFF FFFF FFFF FFFF ← fin kernel space
Tip práctico de debugging¶
Cuando se ve una dirección, mirar los primeros bytes:
| Empieza con | Es... |
|---|---|
00007F... |
User space |
FFFF8... o FFFFF... |
Kernel space |
41414141..., 31313131... |
Non-canonical — fault. Útil para detectar valores corruptos en exploits |
Diferencia con x86: En 32 bits el espacio se dividía mitad/mitad (2 GB user / 2 GB kernel). En x64 no: hay user (lower), zona prohibida (middle), y kernel (upper).
5. User mode vs Kernel mode — por qué la separación¶
| Aspecto | User mode (Ring 3) | Kernel mode (Ring 0) |
|---|---|---|
| Quién | Aplicaciones | OS, drivers |
| Memoria | Solo su espacio privado | Toda la memoria del sistema |
| Instrucciones | Subset (sin cli, lgdt, wrmsr, etc.) |
Todas |
| Aislamiento | Procesos no se ven entre sí | Todo compartido — un bug = BSoD |
Punto clave: Los procesos de user corren todos separados unos de otros, "se creen que no existen otras cosas". El kernel en cambio es un único espacio compartido donde están todos los drivers, el sistema, etc.
Por eso los BSoDs son tan dramáticos: en kernel cualquier macana afecta a todo el sistema, no solo al proceso ofensor.
Transición user → kernel¶
Una app de user pasa a kernel cuando hace una system service call. Por ejemplo, CreateFile en user-mode termina invocando una rutina interna (en ntoskrnl.exe) que accede a estructuras del sistema.
Mecánica:
- Stub en user-mode (en
ntdll.dllowin32u.dll) carga el System Service Number (SSN) enEAX. - Ejecuta
syscall(en x64;int 2Een sistemas viejos). - La CPU cambia a Ring 0 y salta al System Service Dispatcher del kernel.
- El dispatcher usa el SSN para indexar la SSDT (System Service Dispatch Table) y llamar a la función correspondiente.
- Cuando termina, retorna a user mode.
Consecuencia para reversing: No se puede tracear con
F7desde user a kernel. Más detalle en §10.
6. Setup del lab y consejos para la VM target¶
Configuración mínima en la VM target¶
Antes de empezar a trabajar con kernel debugging, en la VM target conviene:
| Configuración | Por qué |
|---|---|
Excepción del antivirus en C:\ (o desactivarlo) |
Para que no intercepte muestras de malware o drivers no firmados |
| Firewall desactivado (Public + Private) | Es VM de prueba, no hay riesgo |
| Power Plan: nunca apagar pantalla / suspender | IDA crashea cuando la VM se suspende |
| Folder Options: mostrar archivos ocultos y extensiones | Para navegar C:\Windows\System32 y otros paths del sistema |
| Disable Driver Signature Enforcement | Para cargar drivers no firmados (§14) |
| Procesadores: 1 core | Más estable para kernel debugging (§8) |
Configuración del host¶
| Configuración | Cómo |
|---|---|
| WinDbg instalado | https://aka.ms/windbg/download |
| Symbol path | srv*C:\symbols*https://msdl.microsoft.com/download/symbols en _NT_SYMBOL_PATH |
| IDA Pro | Para reversing del driver |
| BinDiff instalado correctamente | Ver §1 |
Habilitar kernel debugging en la VM target¶
En CMD como administrador dentro de la VM:
Anotar la key que devuelve newkey. Verificar con:
Reiniciar la VM. WinDbg debe estar escuchando en el host con la misma key.
Conectar con WinDbg standalone vs. desde IDA¶
WinDbg standalone:
Desde IDA (plugin WinDbg):
Debugger → Select debugger → WinDBG
Debugger → Debugger Options → Kernel debug → Port + Key
Debugger → Attach to process
IDA + WinDbg: Funciona casi igual que WinDbg standalone. Los comandos van en la consolita inferior.
gno funciona en IDA — usar el botón Play (F9) yF7/F8para step.Marcar
Use hardware temporary breakpointen las opciones del debugger en IDA para drivers — ayuda a no confundirse con tantos procesos pasando por el mismo punto.
Si la VM crashea¶
Causas comunes:
- Hardware del host muy distinto al original de la VM → bajar cantidad de cores a 1.
- Driver de video no compatible → deshabilitar aceleración 3D en VMware.
- Versión vieja de VMware → actualizar a la última.
7. WinDbg — sesión live y comandos¶
Símbolos y módulos¶
| Comando | Descripción |
|---|---|
.reload /f |
Recarga símbolos de los módulos kernel (forzado) |
.reload /user |
Recarga solo símbolos user-mode (útil al cambiar contexto) |
.reload /u |
Descarga símbolos |
lm |
Lista módulos cargados (kernel + user del proceso actual) |
lm m kernel32 |
Filtra por nombre de módulo |
x nt!* |
Examina (lista) todos los símbolos de nt |
x nt!Create* |
Símbolos que empiezan con Create |
x nt!*Open* |
Símbolos que contienen Open |
Sintaxis de símbolo:
<modulo>!<funcion>. Parant, el módulo se llamant(nontoskrnl). Mayúsculas y minúsculas exactas. Solo módulos con PDB privados muestran símbolos completos.
Procesos y contextos¶
| Comando | Descripción | Velocidad |
|---|---|---|
!process -1 0 |
Proceso actual (donde paró el debugger) | Rápido |
!process 0 0 |
Lista todos los procesos con sus EPROCESS | Lento |
!dml_proc |
Lista procesos con links HTML clickeables | Mucho más rápido que !process 0 0 |
.process /i <eprocess> |
Cambia al contexto de otro proceso (requiere g después) |
— |
Cuando WinDbg breakea por primera vez suele estar en el proceso System (PID 4). Para debuggear código que solo corre en otro proceso (ej. la calculadora), hay que cambiar contexto.
Ejemplo — cambiar al contexto de la calculadora¶
0: kd> !dml_proc
... (links a cada EPROCESS)
0: kd> .process /i ffffe50c12345678 ; EPROCESS de calc.exe
You need to continue execution (press 'g') for the context to be fully switched.
0: kd> g
... (breakea de nuevo en otro lugar)
0: kd> !process -1 0
PROCESS ffffe50c12345678
SessionId: 1 Cid: 1234 Peb: ... ParentCid: ...
Image: CalculatorApp.exe
Después del
.reload /finicial, los símbolos kernel quedan cargados. Si solo se cambia de contexto user-to-user, no hace falta volver a hacer.reload /f— lo único que cambia son los módulos user-mode del nuevo proceso (para esos,.reload /user).
Breakpoints¶
| Comando | Descripción |
|---|---|
bp <addr> |
Breakpoint software |
bp nt!NtOpenFile |
Breakpoint por símbolo |
bp <addr> /p <eprocess> |
Breakpoint solo cuando ese proceso pasa por ahí (filtro) |
bl |
Lista breakpoints activos |
bd <id> |
Deshabilitar breakpoint |
be <id> |
Habilitar breakpoint |
bc <id> |
Borrar breakpoint |
bc * |
Borrar todos los breakpoints |
ba e1 <addr> |
Hardware breakpoint execution size 1 |
ba r4 <addr> |
Hardware breakpoint read size 4 |
ba w4 <addr> |
Hardware breakpoint write size 4 |
/p <eprocess>es esencial cuando se pone un breakpoint en una API de Windows que pasan todos los procesos (ej.nt!NtOpenFile). Sin filtro, la máquina queda inservible porque breakea constantemente.
Stepping¶
| Comando | Descripción | Atajo en WinDbg | Atajo en IDA |
|---|---|---|---|
g |
Continue | F5 |
F9 (Play) |
p |
Step over | F10 |
F8 |
t |
Step into | F11 |
F7 |
pt |
Step out (to next return) | — | — |
Otros útiles¶
| Comando | Descripción |
|---|---|
dt nt!_EPROCESS |
Muestra estructura _EPROCESS con offsets |
dt nt!_EPROCESS <addr> |
Aplica el tipo a una dirección concreta |
dd /c1 <addr> L<n> |
Display dwords, 1 columna, n elementos |
dq <addr> |
Display qwords |
u <addr> |
Unassemble desde una dirección |
r |
Ver registros |
~ |
Lista threads |
k |
Stack trace |
Cheat sheet completa: https://github.com/f1zm0/WinDBG-Cheatsheet
8. Multi-core debugging — usar 1 core¶
Recomendación fuerte: En kernel debugging, configurar la VM con 1 solo core.
Por qué¶
Cuando hay varios cores, el debugger salta entre ellos de manera no lineal. El usuario hace F8 (step over) y de pronto aparece en otra función completamente distinta porque el OS programó otro thread en otro core.
| Configuración | Efecto |
|---|---|
| 1 core | Trace lineal, mucho más predecible |
| Múltiples cores | Saltos entre cores que descolocan el flujo de tracing |
Caveat: Spectre, predicción de ramas, race conditions multi-core — esos bugs requieren múltiples cores. Pero para reversing tradicional de drivers, 1 core es lo más cómodo.
Threads vs cores: Threads sí se manejan bien con un comando (
~). El problema son los cores físicos, que multiplican el caos.
9. Cargar símbolos en IDA desde WinDbg¶
Cuando se debuggea kernel desde IDA, la primera vez tarda mucho (descarga todos los PDBs del Microsoft Symbol Server). Una vez bajados al C:\symbols, las próximas veces son rápidas.
Tip: Es exactamente lo mismo bajarlos desde WinDbg standalone que desde IDA — ambos usan el mismo Symbol Server. Si IDA es muy lento la primera vez, ejecutar
.reload /fdesde WinDbg standalone primero, y después abrir IDA.Si IDA crashea durante la descarga: Matarlo con la cruz, volver a abrirlo, conectar. La VM target queda congelada en el estado donde estaba.
Verificar qué módulos tienen símbolos¶
0: kd> lm
start end module name
fffff800`12000000 fffff800`13000000 nt (pdb symbols) ntkrnlmp.pdb
fffff803`xxxxx... fffff803`xxxxx... HEVD (deferred) ; sin símbolos cargados
...
| Indicador | Significado |
|---|---|
(pdb symbols) o (private pdb symbols) |
PDB cargado correctamente |
(deferred) |
El módulo está cargado pero los símbolos no se descargaron aún |
(no symbols) |
No hay PDB disponible |
(export symbols) |
Solo se ven los símbolos exportados (peor que privado) |
10. Transición User → Kernel y la SSDT¶
Punto crítico que descoloca a todo el mundo: No se puede tracear con
F7desde user a kernel. Aunque se esté en un debugger kernel-mode (WinDbg conbcdedit /debug yes), pararse en unacall ntdll!NtOpenFiley apretarF7salta por encima sin entrar al kernel.
Por qué pasa esto¶
La transición user → kernel atraviesa zonas críticas del CPU (cambio de privilegio, cambio de stack, IDT) donde no se pueden poner breakpoints. Si el debugger intentara tracear ahí, desestabilizaría la máquina (porque tracear, internamente, es poner breakpoints temporales paso a paso). Por seguridad, el debugger directamente lo impide.
Solución: poner el breakpoint en la función kernel¶
Para detenerse en la función kernel correspondiente, hay que saber a dónde va a saltar y poner ahí un bp. Para eso:
- Ver el System Service Number (SSN) que se carga en
EAXantes delsyscallen el stub de NTDLL. - Resolver via
KiServiceTableoW32pServiceTable(§11 y §12). - O usar la tabla pública de j00ru.
Stub típico en NTDLL — nt!NtOpenFile¶
ntdll!NtOpenFile:
00007FFB`06B5145D 4C:8BD1 mov r10, rcx ; primer argumento → r10
00007FFB`06B51460 B8 33000000 mov eax, 33h ; ← SSN para NtOpenFile
00007FFB`06B51465 F60425 ... test byte ptr ds:[7FFE0308h], 1
00007FFB`06B5146D 75 03 jne short next
00007FFB`06B5146F 0F05 syscall ; ← NO se puede tracear F7 aquí
00007FFB`06B51471 C3 ret
Nombres no coinciden necesariamente: Si bien para
NtOpenFileel nombre user y kernel coinciden, muchas funciones tienen nombres distintos en kernel (ej.OpenProcessTokenpuede llamarse de otra forma internamente). Siempre usar la fórmula o la tabla, no asumir que el nombre se preserva.
El registro LSTAR y la cadena de saltos¶
En x64, syscall lee el MSR LSTAR para saber a dónde saltar. LSTAR apunta a nt!KiSystemCall64, que es el dispatcher que:
- Lee
EAX(SSN). - Indexa la SSDT (
KiServiceTableoW32pServiceTablesegún el bit alto del SSN). - Salta a la función kernel correspondiente.
user-mode (ntdll!NtOpenFile)
│ syscall (no traceable)
▼
LSTAR → nt!KiSystemCall64
│ resolución via SSDT
▼
nt!NtOpenFile (kernel-mode)
11. Resolver syscalls de NT — KiServiceTable¶
nt!KiServiceTable es un array de DWORDs donde cada entrada es un offset codificado que apunta a la función kernel correspondiente al SSN.
Fórmula¶
Para un SSN <N>:
1. dd /c1 nt!KiServiceTable + (<N> * 4) L1 ; lee el DWORD codificado
2. u (nt!KiServiceTable + (<DWORD> >> 4)) ; calcula la dirección real
Por qué multiplicar por 4¶
KiServiceTable es una tabla de DWORDs (4 bytes cada entrada). Para llegar al elemento N, hay que avanzar N * 4 bytes desde la base.
Por qué shift right 4¶
En x64, los bits bajos del DWORD se usan para otros propósitos (en algunos kernels antiguos contaban argumentos). Se descartan haciendo >> 4. El resultado, sumado a KiServiceTable, es la dirección real de la función kernel.
En x86 viejo, el DWORD era directamente la dirección. Microsoft complicó la cosa en x64 — pero ya no hay sistemas x86, así que esto se ignora.
Ejemplo completo — resolver SSN 0x33¶
0: kd> dd /c1 nt!KiServiceTable + (0x33 * 4) L1
fffff807`226cef5c 067e9b02
0: kd> u (nt!KiServiceTable + (0x067e9b02 >> 4))
nt!NtOpenFile:
fffff807`22d4d840 4c8bdc mov r11, rsp
fffff807`22d4d843 4881ec88000000 sub rsp, 88h
fffff807`22d4d84a 8b8424b8000000 mov eax, dword ptr [rsp+0B8h]
fffff807`22d4d851 4533d2 xor r10d, r10d
...
Listo: se obtuvo
nt!NtOpenFileen la direcciónfffff80722d4d840. Ahora se puedebpahí, idealmente filtrando por proceso con/p`.
12. Resolver syscalls de Win32k — W32pServiceTable¶
Las syscalls gráficas (USER, GDI, ventanas, menús, brushes) van a win32k.sys, no a nt. Sus SSNs tienen el bit alto seteado — son mayores a 0x1000.
| Categoría | Módulo target | Rango de SSN |
|---|---|---|
| Sistema general (file, process, registry, memory, ALPC, ...) | nt |
0x000 – 0x1FF |
| Gráficos (USER, GDI, composition, sprites, ...) | win32k.sys |
0x1000+ |
Por qué dos tablas: El kernel original solo tenía una SSDT. Cuando Microsoft movió la GUI al kernel (por performance), agregó una segunda tabla solo para
win32k. El bit0x1000en el SSN indica cuál usar.
Fórmula para win32k.sys — SSN 0x1022¶
Paso 1 — Sacar el flag 0x1000 y obtener el offset de la tabla¶
Paso 2 — Leer la entrada de la tabla¶
Si el valor empieza con
0xFF, es negativo cuando se lo interpreta comoint32con signo. Esto es muy común enW32pServiceTableporque las funciones suelen estar antes de la tabla.
Paso 3 — Convertir a int32 con signo¶
@@C++(...) evalúa una expresión como C++. Forzando el cast a int (32 bits con signo), el valor 0xff84e740 se interpreta como -8067264.
Paso 4 — Calcular la dirección real¶
Sintaxis WinDbg:
Token Significado 0nPrefijo decimal (sin él, WinDbg interpreta hexa) >>Right shift lógico >>>Right shift aritmético (preserva el signo)
13. j00ru — tabla pública de syscalls¶
Recurso esencial: https://j00ru.vexillium.org/syscalls/nt/64/ y https://j00ru.vexillium.org/syscalls/win32k/64/
Estas páginas mantienen un mapeo SSN → nombre de función para cada versión de Windows histórica (XP, 7, 8, 8.1, 10, 11). Permite saltearse la fórmula si solo se necesita saber el nombre.
Workflow¶
- Abrir la página correspondiente (NT o Win32k).
- Click en Show all primero (carga toda la tabla).
- Highlight del SSN buscado (ej.
0x33). - Buscar la fila highlighted en la columna correspondiente al build del Windows objetivo.
Caveat: Para Windows 10/11 las columnas van al final de la tabla — la página tiene años acumulados.
Cuándo usar la fórmula vs. la tabla¶
| Usar... | Cuando... |
|---|---|
| Tabla j00ru | Solo se necesita el nombre, build común de Windows, no se quiere abrir WinDbg |
| Fórmula manual | Hay que confirmar la dirección exacta en runtime, build atípico, validar contra el sistema corriendo |
14. Cargar el driver HEVD¶
HEVD (HackSys Extreme Vulnerable Driver) es un driver de prueba no firmado. Windows con Driver Signature Enforcement (DSE) activo no lo carga.
Repo: https://github.com/hacksysteam/HackSysExtremeVulnerableDriver
Disable Driver Signature Enforcement (temporal)¶
- Settings → Recovery → Advanced startup → Restart now.
- Al reiniciar, el menú de boot ofrece opciones avanzadas.
- Troubleshoot → Advanced options → Startup Settings → Restart.
- Cuando arranca el menú de Startup Settings, presionar
7para Disable driver signature enforcement.
El efecto es temporal — al próximo reboot vuelve a estar activo. Suficiente para una sesión de lab.
Otras opciones que pueden bloquear la carga¶
| Opción | Cómo deshabilitar |
|---|---|
| Secure Boot (UEFI) | BIOS/UEFI de la VM → Boot → Secure Boot Off |
| HVCI / VBS (Hypervisor-protected Code Integrity) | Core isolation settings → Memory integrity Off |
| Smart App Control (Windows 11) | Settings → Privacy & Security → Off |
Cargar el driver¶
Con DSE deshabilitado, usar el driver loader que viene con HEVD (o cualquier loader como OSR Driver Loader):
- Abrir
DriverLoader.execomo administrador. - Cargar
HEVD.sys(la versión x64). - Click Register (lo agrega a Service Control Manager) → Start (lo carga).
- Si todo va bien, debe decir "Driver loaded successfully".
Si falla: Probable que DSE no esté realmente desactivado. Reintentar el procedimiento de boot. Algunas VMs requieren reiniciar dos veces para que tome.
15. Encontrar el device name — WinObj y CreateFile¶
Crítico: El device name no es igual al nombre del driver. Si el driver se llama
HEVD.sys, el device suele llamarse algo como\Device\HackSysExtremeVulnerableDriver. Sin saber el device name, no se puede comunicar con el driver desde user-mode.
Encontrar el device name con WinObj (Sysinternals)¶
- Bajar Sysinternals Suite (incluye
winobj.exe). - Correr
winobj64.execomo administrador. - Navegar a
\Device\— ahí están todos los devices nombrados. - Buscar (
Ctrl+F)HackSyspara HEVD.
\Device\HackSysExtremeVulnerableDriver ← este es el device real
\GLOBAL??\HackSysExtremeVulnerableDriver ← este es el SymbolicLink (alias accesible desde user)
Sintaxis para CreateFile¶
Desde user-mode, el namespace \Device\ no se accede directamente. Se usa el alias \GLOBAL??\ que se mappea como \\.\ (con doble backslash escapado):
| Path real (kernel) | Alias user (\GLOBAL??\) |
En código C |
|---|---|---|
\Device\HackSysExtremeVulnerableDriver |
\\.\HackSysExtremeVulnerableDriver |
"\\\\.\\HackSysExtremeVulnerableDriver" |
HANDLE hDevice = CreateFileA(
"\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hDevice == INVALID_HANDLE_VALUE) {
// El driver no está cargado, o el device name está mal,
// o los permisos solicitados (GENERIC_*) no están permitidos.
return GetLastError();
}
// hDevice ahora es válido. Se puede usar con DeviceIoControl.
CloseHandle(hDevice);
Permisos: No siempre se permite
GENERIC_READ | GENERIC_WRITE. Algunos drivers solo aceptan uno u otro. Hay que probar combinaciones — el reversing del dispatcher deIRP_MJ_CREATElo dice.
Sin WinObj (alternativa por reversing)¶
Mientras se reversea el driver, eventualmente aparece la string del device name en IoCreateDevice o IoCreateSymbolicLink. Es una UNICODE_STRING que apunta al texto literal \Device\<name>.
16. Importar símbolos de ntoskrnl.exe en IDA¶
Para reversear un driver con todos los tipos de Windows kernel (_DRIVER_OBJECT, _IRP, _IO_STACK_LOCATION, _EPROCESS, etc.), conviene cargar el PDB de ntoskrnl.exe en la base de datos de IDA del driver.
Workflow¶
- Copiar
C:\Windows\System32\ntoskrnl.exedesde la VM target al host. - Abrirlo en una nueva instancia de IDA (otra base de datos, distinta del driver).
- IDA pregunta si bajar el PDB → aceptar. Va a descargar el PDB al cache.
- Anotar la ruta del PDB descargado (IDA la muestra en el output).
Aplicar al driver¶
Volver a la base de datos del driver y:
- File → Load file → PDB file... → seleccionar el PDB de ntoskrnl recién bajado.
- Importante: cambiar la dirección de carga a una distinta de la del driver (ej.
0x0), para que no pise las funciones del driver. - Open → IDA importa todos los tipos del PDB.
Verificar en View → Open Subviews → Local Types que ahora aparecen:
_DRIVER_OBJECT_DEVICE_OBJECT_IRP_IO_STACK_LOCATION_EPROCESS,_KPROCESS_KTHREAD,_ETHREAD_KAPC_STATE_FILE_OBJECT- ...y cientos más
Por qué no usar tipos hardcodeados: Las estructuras pueden cambiar entre versiones de Windows (ej. nuevos campos, offsets distintos). Importar el PDB del target específico garantiza offsets correctos.
Dato: El PDB privado de
ntoskrnl.exetiene decenas de miles de tipos. Es la fuente de verdad sobre estructuras kernel.
17. Reversing de DriverEntry¶
DriverEntry es la primera función que se ejecuta cuando el driver se carga. Es responsable de:
- Inicializar el driver y su
DRIVER_OBJECT. - Crear el
DEVICE_OBJECT(conIoCreateDevice). - Crear el SymbolicLink (con
IoCreateSymbolicLink). - Setear los handlers de
MajorFunction[]yDriverUnload.
Prototipo¶
Pasos en IDA¶
- Encontrar
DriverEntry(IDA generalmente la marca, está en el entry point). - Aplicar
F5(decompiler). - Tipear los argumentos:
Y(set type) →NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath). - Si IDA no aplica el tipo a la variable local, usar Convert to struct →
_DRIVER_OBJECTdespués de hacer click derecho sobre el registro o variable que tiene elDriverObject.
Patrón típico que aparece en DriverEntry de HEVD¶
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
UNICODE_STRING DeviceName, SymbolicLinkName;
PDEVICE_OBJECT DeviceObject;
NTSTATUS status;
// 1. Construir UNICODE_STRING con el device name
RtlInitUnicodeString(&DeviceName, L"\\Device\\HackSysExtremeVulnerableDriver");
// 2. Crear el device object
status = IoCreateDevice(
DriverObject,
0, // DeviceExtensionSize
&DeviceName,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE, // Exclusive
&DeviceObject
);
if (!NT_SUCCESS(status)) return status;
// 3. Construir UNICODE_STRING con el symbolic link
RtlInitUnicodeString(&SymbolicLinkName, L"\\DosDevices\\HackSysExtremeVulnerableDriver");
// 4. Crear el symbolic link (esto es lo que permite acceso desde \\.\ en user-mode)
status = IoCreateSymbolicLink(&SymbolicLinkName, &DeviceName);
if (!NT_SUCCESS(status)) {
IoDeleteDevice(DeviceObject);
return status;
}
// 5. Limpiar la tabla MajorFunction (todo a NULL primero)
for (int i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
DriverObject->MajorFunction[i] = NULL;
// 6. Setear handlers específicos
DriverObject->MajorFunction[IRP_MJ_CREATE] = IrpCreateCloseHandler;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = IrpCreateCloseHandler;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IrpDeviceIoCtlHandler; // ← el dispatcher
DriverObject->DriverUnload = DriverUnloadHandler;
return STATUS_SUCCESS;
}
Cosas a renombrar al reversear¶
| Función original (sub_X) | Renombrar a |
|---|---|
Handler de IRP_MJ_CREATE y IRP_MJ_CLOSE (mismo) |
IrpCreateCloseHandler |
Handler de IRP_MJ_DEVICE_CONTROL |
IrpDeviceIoCtlHandler o Dispatch |
Handler de DriverUnload |
DriverUnloadHandler |
18. La tabla MajorFunction y la enumeración IRP_MJ_*¶
DRIVER_OBJECT->MajorFunction es un array de punteros a función indexado por los códigos IRP_MJ_*. Cuando un IRP llega al driver, el I/O Manager indexa esta tabla con el MajorFunction del IRP y llama al handler correspondiente.
Layout en la estructura _DRIVER_OBJECT¶
+0x000 Type
+0x002 Size
+0x008 DeviceObject
+0x010 Flags
+0x018 DriverStart
+0x020 DriverSize
+0x028 DriverSection
+0x030 DriverExtension
+0x038 DriverName
+0x048 HardwareDatabase
+0x050 FastIoDispatch
+0x058 DriverInit
+0x060 DriverStartIo
+0x068 DriverUnload ← NO está en MajorFunction[], está aparte
+0x070 MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1] ← acá empieza la tabla
Por eso al reversear,
RBX + 0x68esDriverUnloadyRBX + 0x70 + i*8esMajorFunction[i].
Enumeración IRP_MJ_*¶
| Constante | Valor | Operación user-mode equivalente |
|---|---|---|
IRP_MJ_CREATE |
0x00 |
CreateFile |
IRP_MJ_CREATE_NAMED_PIPE |
0x01 |
CreateNamedPipe |
IRP_MJ_CLOSE |
0x02 |
CloseHandle |
IRP_MJ_READ |
0x03 |
ReadFile |
IRP_MJ_WRITE |
0x04 |
WriteFile |
IRP_MJ_QUERY_INFORMATION |
0x05 |
GetFileInformationByHandle |
IRP_MJ_SET_INFORMATION |
0x06 |
SetFileInformationByHandle |
IRP_MJ_QUERY_EA |
0x07 |
— |
IRP_MJ_SET_EA |
0x08 |
— |
IRP_MJ_FLUSH_BUFFERS |
0x09 |
FlushFileBuffers |
IRP_MJ_QUERY_VOLUME_INFORMATION |
0x0A |
— |
IRP_MJ_SET_VOLUME_INFORMATION |
0x0B |
— |
IRP_MJ_DIRECTORY_CONTROL |
0x0C |
— |
IRP_MJ_FILE_SYSTEM_CONTROL |
0x0D |
FSCTL_* |
IRP_MJ_DEVICE_CONTROL |
0x0E |
DeviceIoControl ← el más importante para exploitation |
IRP_MJ_INTERNAL_DEVICE_CONTROL |
0x0F |
— |
IRP_MJ_SHUTDOWN |
0x10 |
— |
IRP_MJ_LOCK_CONTROL |
0x11 |
LockFileEx |
IRP_MJ_CLEANUP |
0x12 |
(al cerrar último handle) |
IRP_MJ_CREATE_MAILSLOT |
0x13 |
— |
IRP_MJ_QUERY_SECURITY |
0x14 |
— |
IRP_MJ_SET_SECURITY |
0x15 |
— |
IRP_MJ_POWER |
0x16 |
— |
IRP_MJ_SYSTEM_CONTROL |
0x17 |
WMI |
IRP_MJ_DEVICE_CHANGE |
0x18 |
— |
IRP_MJ_QUERY_QUOTA |
0x19 |
— |
IRP_MJ_SET_QUOTA |
0x1A |
— |
IRP_MJ_PNP |
0x1B |
— |
IRP_MJ_MAXIMUM_FUNCTION |
0x1B |
(último válido) |
Importarla en IDA como enum¶
La enumeración
IRP_MJ_*no está incluida en el PDB dentoskrnl.exe(es un define del headerwdm.h). Hay que agregarla manualmente.
- View → Open Subviews → Enums (o
Shift+F10). - Click derecho → Add enum... → nombre:
MajorCodes. - Agregar miembros con los valores de la tabla.
O bien, importarla desde un header file (File → Load file → C header file... con un .h que tenga los #define o enum).
Aplicar la enum a las constantes en el decompiler¶
Una vez tipeados los MajorFunction[i] = ... en DriverEntry, en el decompiler aparecen MajorFunction[0], MajorFunction[2], MajorFunction[14], etc. Click derecho sobre el 0 / 2 / 14 → Use enum → MajorCodes. En pseudocódigo también se puede poner el cursor sobre la constante y apretar m (Set enum / Use enum) para elegir la enum. Resultado:
DriverObject->MajorFunction[IRP_MJ_CREATE] = sub_xxxx;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = sub_xxxx; // misma función que CREATE
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = sub_yyyy; // ← el dispatcher
Ahora sí es claro qué función maneja cada operación.
Para tener el enum copiable/importable, ver Apéndice A.
19. La función Dispatch — el verdadero punto de entrada¶
El Dispatch (handler de IRP_MJ_DEVICE_CONTROL) es donde vive toda la lógica de IOCTLs del driver, y donde están casi todas las vulnerabilidades.
Prototipo según DRIVER_DISPATCH¶
typedef NTSTATUS (DRIVER_DISPATCH)(
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
);
Tipear esto en IDA: En la pseudocódigo de la función dispatch, presionar
Yy poner:IDA puede no aceptar
_In_/_Inout_— quitarlos si se quejan.
Acceder al IO_STACK_LOCATION actual¶
El stack location del driver actual está en:
En offset, eso es Irp + 0xB8 en x64. Pero la dereferencia en assembly se ve fea:
Truco en IDA: Click derecho en +B8h → Structure offset / field name → seleccionar _IRP.Tail.Overlay.CurrentStackLocation. Ahora aparece como nombre legible.
Patrón típico del dispatcher¶
NTSTATUS Dispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
PIO_STACK_LOCATION IrpStack;
NTSTATUS status = STATUS_NOT_SUPPORTED;
ULONG IoControlCode;
IrpStack = Irp->Tail.Overlay.CurrentStackLocation;
if (IrpStack) {
IoControlCode = IrpStack->Parameters.DeviceIoControl.IoControlCode;
switch (IoControlCode) {
case HEVD_IOCTL_STACK_OVERFLOW:
status = TriggerStackOverflow(...);
break;
case HEVD_IOCTL_HEAP_OVERFLOW:
status = TriggerHeapOverflow(...);
break;
// ... más IOCTLs
default:
status = STATUS_INVALID_DEVICE_REQUEST;
break;
}
}
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
Tipos de retorno — NTSTATUS¶
Cuando el decompiler muestra return 0xC00000BBh, eso es STATUS_NOT_SUPPORTED. Para que IDA lo muestre por nombre:
- Click derecho en la variable de retorno → Convert to type →
NTSTATUS. - O cambiar el tipo de retorno de la función a
NTSTATUSconY.
Códigos NTSTATUS comunes:
| Código | Nombre |
|---|---|
0x00000000 |
STATUS_SUCCESS |
0xC0000001 |
STATUS_UNSUCCESSFUL |
0xC0000022 |
STATUS_ACCESS_DENIED |
0xC000000D |
STATUS_INVALID_PARAMETER |
0xC0000023 |
STATUS_BUFFER_TOO_SMALL |
0xC0000058 |
STATUS_NOT_SUPPORTED |
0xC0000005 |
STATUS_ACCESS_VIOLATION |
0xC000010A |
STATUS_PROCESS_IS_TERMINATING |
Lista completa: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55
20. IO_STACK_LOCATION — el problema de la union¶
IO_STACK_LOCATION es la estructura más confusa para el decompiler porque su campo Parameters es una union enorme con un miembro distinto por cada tipo de IRP_MJ.
typedef struct _IO_STACK_LOCATION {
UCHAR MajorFunction;
UCHAR MinorFunction;
UCHAR Flags;
UCHAR Control;
union {
// Read
struct { ULONG Length; ULONG Key; LARGE_INTEGER ByteOffset; } Read;
// Write
struct { ULONG Length; ULONG Key; LARGE_INTEGER ByteOffset; } Write;
// Create
struct { ... } Create;
// DeviceIoControl ← este es el que importa para IRP_MJ_DEVICE_CONTROL
struct {
ULONG OutputBufferLength;
ULONG InputBufferLength;
ULONG IoControlCode;
PVOID Type3InputBuffer;
} DeviceIoControl;
// ...muchos más
} Parameters;
PDEVICE_OBJECT DeviceObject;
PFILE_OBJECT FileObject;
PIO_COMPLETION_ROUTINE CompletionRoutine;
PVOID Context;
} IO_STACK_LOCATION, *PIO_STACK_LOCATION;
El error que comete el decompiler¶
Cuando IDA tipea automáticamente el IO_STACK_LOCATION, elige una rama de la union arbitrariamente — generalmente la primera, que suele ser Read. Si se está reverseando una función dispatch (IRP_MJ_DEVICE_CONTROL), eso es incorrecto: el código real está leyendo IoControlCode, no Read.Length.
Ejemplo de output incorrecto:
Cuando en realidad debería ser:
// CORRECTO — porque estamos en IRP_MJ_DEVICE_CONTROL
IoControlCode = IrpStack->Parameters.DeviceIoControl.IoControlCode;
Cómo arreglarlo en IDA¶
- Click derecho sobre el campo malinterpretado (ej.
Read.Length). - Select union field... (o
Alt+Y). - En el diálogo, navegar el árbol y seleccionar
DeviceIoControl.IoControlCode(oInputBufferLength,OutputBufferLength,Type3InputBuffersegún corresponda). - Ahora el decompiler muestra el campo correcto.
Regla: Saber en qué
IRP_MJ_*se está reverseando para elegir la rama correcta de la union. Esto se sabe porDriverEntry: qué función está asignada a quéMajorFunction[i].
Buffers en DeviceIoControl¶
El usuario manda los buffers en estos campos:
struct {
ULONG OutputBufferLength; // tamaño del buffer de salida que el user espera
ULONG InputBufferLength; // tamaño del buffer que el user mandó
ULONG IoControlCode; // ← el IOCTL elegido por el user
PVOID Type3InputBuffer; // puntero al buffer de entrada (solo para METHOD_NEITHER)
} DeviceIoControl;
El buffer real depende del buffering method del IOCTL (METHOD_BUFFERED, METHOD_IN_DIRECT, METHOD_OUT_DIRECT, METHOD_NEITHER):
| Method | Dónde está el InputBuffer | Dónde está el OutputBuffer |
|---|---|---|
METHOD_BUFFERED |
Irp->AssociatedIrp.SystemBuffer |
Irp->AssociatedIrp.SystemBuffer (mismo, reused) |
METHOD_IN_DIRECT / METHOD_OUT_DIRECT |
Irp->AssociatedIrp.SystemBuffer |
Irp->MdlAddress (MDL) |
METHOD_NEITHER |
IrpStack->Parameters.DeviceIoControl.Type3InputBuffer (puntero user-mode crudo!) |
Irp->UserBuffer (puntero user-mode crudo!) |
METHOD_NEITHER es el método más peligroso: el driver recibe punteros raw de user-mode. Si no llama a
ProbeForRead/ProbeForWriteantes de usarlos, hay vulnerabilidad arbitrary read/write o kernel pointer dereference.
Cómo se codifica el method en el IOCTL code¶
Un IoControlCode (DWORD) tiene este layout:
Bits 31-16 : DeviceType (FILE_DEVICE_*, custom usually 0x800+)
Bits 15-14 : RequiredAccess (FILE_ANY_ACCESS, FILE_READ_DATA, FILE_WRITE_DATA)
Bits 13-2 : FunctionCode (0-0xFFF, usually 0x800+ for custom)
Bits 1-0 : Method (00=BUFFERED, 01=IN_DIRECT, 10=OUT_DIRECT, 11=NEITHER)
Los 2 bits más bajos del IOCTL determinan el method. Con un AND 3 se obtiene el method:
// Decodificar IOCTL en partes
DeviceType = (IoCtl >> 16) & 0xFFFF;
RequiredAccess = (IoCtl >> 14) & 0x3;
FunctionCode = (IoCtl >> 2) & 0xFFF;
Method = IoCtl & 0x3;
Macro
CTL_CODEdel SDK construye los IOCTL codes:
21. Cheat sheet rápida¶
Comandos WinDbg de uso diario¶
.reload /f ; cargar todos los símbolos kernel
!dml_proc ; lista procesos rápidamente
!process -1 0 ; proceso actual
.process /i <eprocess> ; cambiar contexto (luego g)
lm ; módulos cargados
x nt!*Open* ; buscar símbolo
bp nt!NtOpenFile /p <eprocess> ; bp filtrado por proceso
bl ; listar bps
bc * ; borrar todos
dt nt!_EPROCESS <addr> ; mostrar struct con datos
dd /c1 nt!KiServiceTable + (N*4) L1 ; leer entrada SSDT
u (nt!KiServiceTable + (DWORD>>4)) ; resolver dirección
Workflow de reversing de un driver¶
- Reversar
DriverEntry→ encontrarIoCreateDeviceyIoCreateSymbolicLink→ device name. - Identificar handlers de
MajorFunction[]→ renombrar. - Reversar el dispatcher (
IRP_MJ_DEVICE_CONTROL) → encontrar el switch sobreIoControlCode. - Por cada IOCTL: ver el method (METHOD_BUFFERED/IN_DIRECT/OUT_DIRECT/NEITHER), ubicar los buffers, identificar la función vulnerable.
- Validar con dynamic debugging:
bpen el dispatcher, mandar IOCTL desde user-mode conDeviceIoControl.
Recursos clave¶
| Recurso | URL |
|---|---|
| Symbol Server | srv*C:\symbols*https://msdl.microsoft.com/download/symbols |
| WinDbg | https://aka.ms/windbg/download |
| Sysinternals | https://learn.microsoft.com/en-us/sysinternals/ |
| BinDiff | https://github.com/google/bindiff/releases |
| WinBindex | https://winbindex.m417z.com/ |
| j00ru NT syscalls | https://j00ru.vexillium.org/syscalls/nt/64/ |
| j00ru Win32k syscalls | https://j00ru.vexillium.org/syscalls/win32k/64/ |
| WinDbg cheat sheet | https://github.com/f1zm0/WinDBG-Cheatsheet |
| HEVD repo | https://github.com/hacksysteam/HackSysExtremeVulnerableDriver |
| NTSTATUS codes | https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55 |
| IRP_MJ codes | https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/irp-major-function-codes |
Apéndice A — enum Major_Codes para IDA¶
Este enum sirve para que IDA/Hex-Rays muestre nombres semánticos en lugar de números crudos cuando se reversea la tabla:
Sin enum, el decompiler suele dejarlo así:
Con enum aplicado, el índice 14 pasa a verse como IRP_MJ_DEVICE_CONTROL, que inmediatamente indica que esa función es el dispatcher de IOCTLs.
Enum para cargar¶
enum Major_Codes : unsigned __int8
{
IRP_MJ_CREATE = 0x0,
IRP_MJ_CREATE_NAMED_PIPE = 0x1,
IRP_MJ_CLOSE = 0x2,
IRP_MJ_READ = 0x3,
IRP_MJ_WRITE = 0x4,
IRP_MJ_QUERY_INFORMATION = 0x5,
IRP_MJ_SET_INFORMATION = 0x6,
IRP_MJ_QUERY_EA = 0x7,
IRP_MJ_SET_EA = 0x8,
IRP_MJ_FLUSH_BUFFERS = 0x9,
IRP_MJ_QUERY_VOLUME_INFORMATION = 0xA,
IRP_MJ_SET_VOLUME_INFORMATION = 0xB,
IRP_MJ_DIRECTORY_CONTROL = 0xC,
IRP_MJ_FILE_SYSTEM_CONTROL = 0xD,
IRP_MJ_DEVICE_CONTROL = 0xE,
IRP_MJ_INTERNAL_DEVICE_CONTROL = 0xF,
IRP_MJ_SHUTDOWN = 0x10,
IRP_MJ_LOCK_CONTROL = 0x11,
IRP_MJ_CLEANUP = 0x12,
IRP_MJ_CREATE_MAILSLOT = 0x13,
IRP_MJ_QUERY_SECURITY = 0x14,
IRP_MJ_SET_SECURITY = 0x15,
IRP_MJ_POWER = 0x16,
IRP_MJ_SYSTEM_CONTROL = 0x17,
IRP_MJ_DEVICE_CHANGE = 0x18,
IRP_MJ_QUERY_QUOTA = 0x19,
IRP_MJ_SET_QUOTA = 0x1A,
IRP_MJ_PNP = 0x1B,
IRP_MJ_PNP_POWER = 0x1C,
IRP_MJ_MAXIMUM_FUNCTION = 0x1D,
};
Nota de versión: En headers/documentación WDK modernos,
IRP_MJ_PNP_POWERpuede aparecer como alias histórico deIRP_MJ_PNP, yIRP_MJ_MAXIMUM_FUNCTIONpuede figurar como0x1B. Para reversing, lo importante es que el enum que uses coincida con los valores que querés ver nombrados en el decompiler del target.
Cómo agregarlo en IDA¶
Opción manual:
- Abrir View → Open Subviews → Enums (
Shift+F10). - Click derecho → Add enum....
- Nombre sugerido:
Major_Codes. - Agregar los miembros del enum con sus valores.
Opción por header:
- Poner el enum en un
.h. - En IDA: File → Load file → C header file....
- Seleccionar el header.
- Verificar que
Major_Codesaparezca en la ventana de Enums.
Cómo aplicarlo en el decompiler¶
Cuando veas algo así:
procedimiento:
- Poner el cursor sobre
14. - Apretar
m. - Elegir
Major_Codes. - Seleccionar
IRP_MJ_DEVICE_CONTROL.
Resultado esperado:
También sirve sobre comparaciones o switches:
después de aplicar enum:
Por qué importa¶
La tabla MajorFunction[] es el mapa de entrypoints del driver. Cuando se nombra bien:
| Antes | Después | Lectura |
|---|---|---|
MajorFunction[0] |
MajorFunction[IRP_MJ_CREATE] |
Apertura del device con CreateFile |
MajorFunction[2] |
MajorFunction[IRP_MJ_CLOSE] |
Cierre del handle |
MajorFunction[14] |
MajorFunction[IRP_MJ_DEVICE_CONTROL] |
Dispatcher de DeviceIoControl |
Para exploitation, el más importante suele ser IRP_MJ_DEVICE_CONTROL: ahí se parsean los IOCTLs y aparecen los bugs de validación de buffers, METHOD_NEITHER, overflows, UAFs, arbitrary write, etc.