Saltar a contenido

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 (KiServiceTable y W32pServiceTable), la carga del driver vulnerable HEVD con disable signature enforcement, y los primeros pasos del reversing del DriverEntry y la función dispatch en IDA Pro.

← Volver al Módulo 5


Tabla de Contenidos

  1. BinDiff — instalación correcta y uso
  2. WinBindex — encontrar versiones de archivos de Windows
  3. Memoria virtual — repaso
  4. Direcciones canónicas en x64
  5. User mode vs Kernel mode — por qué la separación
  6. Setup del lab y consejos para la VM target
  7. WinDbg — sesión live y comandos
  8. Multi-core debugging — usar 1 core
  9. Cargar símbolos en IDA desde WinDbg
  10. Transición User → Kernel y la SSDT
  11. Resolver syscalls de NT — KiServiceTable
  12. Resolver syscalls de Win32k — W32pServiceTable
  13. j00ru — tabla pública de syscalls
  14. Cargar el driver HEVD
  15. Encontrar el device name — WinObj y CreateFile
  16. Importar símbolos de ntoskrnl.exe en IDA
  17. Reversing de DriverEntry
  18. La tabla MajorFunction y la enumeración IRP_MJ_*
  19. La función Dispatch — el verdadero punto de entrada
  20. IO_STACK_LOCATION — el problema de la union
  21. Cheat sheet rápida A. Apéndice A — enum Major_Codes para 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.exe
  • bindiff_config_setup.exe
  • binexport2dump.exe
  • binexport12_ida64.dll
  • bindiff8_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

  1. Abrir la versión parcheada (la nueva) en IDA primero. Esperar análisis. Guardar la base de datos. Cerrar IDA.
  2. Abrir la versión vulnerable (la vieja) en IDA. Esperar análisis.
  3. Edit → Plugin → BinDiff → Diff Database → seleccionar el .i64 parcheado.
  4. 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...
strcpystrncpy (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:

  1. Buscar el nombre del archivo (ej. afd.sys, clfs.sys, win32k.sys).
  2. Filtrar por arquitectura (x64) y versión de Windows.
  3. 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:

  1. Stub en user-mode (en ntdll.dll o win32u.dll) carga el System Service Number (SSN) en EAX.
  2. Ejecuta syscall (en x64; int 2E en sistemas viejos).
  3. La CPU cambia a Ring 0 y salta al System Service Dispatcher del kernel.
  4. El dispatcher usa el SSN para indexar la SSDT (System Service Dispatch Table) y llamar a la función correspondiente.
  5. Cuando termina, retorna a user mode.

Consecuencia para reversing: No se puede tracear con F7 desde 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:

bcdedit /dbgsettings net hostip:192.168.119.1 port:50000 newkey
bcdedit /debug yes

Anotar la key que devuelve newkey. Verificar con:

bcdedit /dbgsettings

Reiniciar la VM. WinDbg debe estar escuchando en el host con la misma key.

Conectar con WinDbg standalone vs. desde IDA

WinDbg standalone:

File → Attach to kernel → Net → Port: 50000, Key: <key>

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. g no funciona en IDA — usar el botón Play (F9) y F7/F8 para step.

Marcar Use hardware temporary breakpoint en 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>. Para nt, el módulo se llama nt (no ntoskrnl). 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 /f inicial, 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 /f desde 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 F7 desde user a kernel. Aunque se esté en un debugger kernel-mode (WinDbg con bcdedit /debug yes), pararse en una call ntdll!NtOpenFile y apretar F7 salta 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 EAX antes del syscall en el stub de NTDLL.
  • Resolver via KiServiceTable o W32pServiceTable (§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 NtOpenFile el nombre user y kernel coinciden, muchas funciones tienen nombres distintos en kernel (ej. OpenProcessToken puede 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:

  1. Lee EAX (SSN).
  2. Indexa la SSDT (KiServiceTable o W32pServiceTable según el bit alto del SSN).
  3. 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!NtOpenFile en la dirección fffff80722d4d840. 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 0x0000x1FF
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 bit 0x1000 en 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

0x1022 & 0xFFF = 0x22

Paso 2 — Leer la entrada de la tabla

0: kd> dd win32k!W32pServiceTable + (0x22 * 4) L1
... 0xff84e740

Si el valor empieza con 0xFF, es negativo cuando se lo interpreta como int32 con signo. Esto es muy común en W32pServiceTable porque las funciones suelen estar antes de la tabla.

Paso 3 — Convertir a int32 con signo

0: kd> ? @@C++((int)(0xff84e740))
Evaluate expression: -8067264 = ffffffff`ff84e740

@@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

0: kd> u win32k!W32pServiceTable + (0n-8067264 >>> 4)
win32k!NtUserShowCaret:
...

Sintaxis WinDbg:

Token Significado
0n Prefijo 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

  1. Abrir la página correspondiente (NT o Win32k).
  2. Click en Show all primero (carga toda la tabla).
  3. Highlight del SSN buscado (ej. 0x33).
  4. 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)

  1. Settings → Recovery → Advanced startup → Restart now.
  2. Al reiniciar, el menú de boot ofrece opciones avanzadas.
  3. Troubleshoot → Advanced options → Startup Settings → Restart.
  4. Cuando arranca el menú de Startup Settings, presionar 7 para 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):

  1. Abrir DriverLoader.exe como administrador.
  2. Cargar HEVD.sys (la versión x64).
  3. Click Register (lo agrega a Service Control Manager) → Start (lo carga).
  4. 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)

  1. Bajar Sysinternals Suite (incluye winobj.exe).
  2. Correr winobj64.exe como administrador.
  3. Navegar a \Device\ — ahí están todos los devices nombrados.
  4. Buscar (Ctrl+F) HackSys para 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 de IRP_MJ_CREATE lo 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

  1. Copiar C:\Windows\System32\ntoskrnl.exe desde la VM target al host.
  2. Abrirlo en una nueva instancia de IDA (otra base de datos, distinta del driver).
  3. IDA pregunta si bajar el PDB → aceptar. Va a descargar el PDB al cache.
  4. Anotar la ruta del PDB descargado (IDA la muestra en el output).

Aplicar al driver

Volver a la base de datos del driver y:

  1. File → Load file → PDB file... → seleccionar el PDB de ntoskrnl recién bajado.
  2. Importante: cambiar la dirección de carga a una distinta de la del driver (ej. 0x0), para que no pise las funciones del driver.
  3. 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.exe tiene 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:

  1. Inicializar el driver y su DRIVER_OBJECT.
  2. Crear el DEVICE_OBJECT (con IoCreateDevice).
  3. Crear el SymbolicLink (con IoCreateSymbolicLink).
  4. Setear los handlers de MajorFunction[] y DriverUnload.

Prototipo

NTSTATUS DriverEntry(
    _In_ PDRIVER_OBJECT  DriverObject,
    _In_ PUNICODE_STRING RegistryPath
);

Pasos en IDA

  1. Encontrar DriverEntry (IDA generalmente la marca, está en el entry point).
  2. Aplicar F5 (decompiler).
  3. Tipear los argumentos: Y (set type) → NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath).
  4. Si IDA no aplica el tipo a la variable local, usar Convert to struct → _DRIVER_OBJECT después de hacer click derecho sobre el registro o variable que tiene el DriverObject.

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 + 0x68 es DriverUnload y RBX + 0x70 + i*8 es MajorFunction[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 DeviceIoControlel 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 de ntoskrnl.exe (es un define del header wdm.h). Hay que agregarla manualmente.

  1. View → Open Subviews → Enums (o Shift+F10).
  2. Click derecho → Add enum... → nombre: MajorCodes.
  3. 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 / 14Use enumMajorCodes. 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 Y y poner:

NTSTATUS Dispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp);

IDA puede no aceptar _In_ / _Inout_ — quitarlos si se quejan.

Acceder al IO_STACK_LOCATION actual

El stack location del driver actual está en:

PIO_STACK_LOCATION IrpStack = Irp->Tail.Overlay.CurrentStackLocation;

En offset, eso es Irp + 0xB8 en x64. Pero la dereferencia en assembly se ve fea:

mov rax, [rdx + 0B8h]   ; rdx = Irp, [rdx+0xB8] = CurrentStackLocation

Truco en IDA: Click derecho en +B8hStructure 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:

  1. Click derecho en la variable de retorno → Convert to typeNTSTATUS.
  2. O cambiar el tipo de retorno de la función a NTSTATUS con Y.

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:

// INCORRECTO — el decompiler eligió Read.Length
v3 = IrpStack->Parameters.Read.Length;

Cuando en realidad debería ser:

// CORRECTO — porque estamos en IRP_MJ_DEVICE_CONTROL
IoControlCode = IrpStack->Parameters.DeviceIoControl.IoControlCode;

Cómo arreglarlo en IDA

  1. Click derecho sobre el campo malinterpretado (ej. Read.Length).
  2. Select union field... (o Alt+Y).
  3. En el diálogo, navegar el árbol y seleccionar DeviceIoControl.IoControlCode (o InputBufferLength, OutputBufferLength, Type3InputBuffer según corresponda).
  4. 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 por DriverEntry: 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 / ProbeForWrite antes 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_CODE del SDK construye los IOCTL codes:

#define CTL_CODE(DeviceType, Function, Method, Access) \
    (((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))

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

  1. Reversar DriverEntry → encontrar IoCreateDevice y IoCreateSymbolicLinkdevice name.
  2. Identificar handlers de MajorFunction[] → renombrar.
  3. Reversar el dispatcher (IRP_MJ_DEVICE_CONTROL) → encontrar el switch sobre IoControlCode.
  4. Por cada IOCTL: ver el method (METHOD_BUFFERED/IN_DIRECT/OUT_DIRECT/NEITHER), ubicar los buffers, identificar la función vulnerable.
  5. Validar con dynamic debugging: bp en el dispatcher, mandar IOCTL desde user-mode con DeviceIoControl.

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:

DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchDeviceControl;

Sin enum, el decompiler suele dejarlo así:

DriverObject->MajorFunction[14] = sub_140001000;

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_POWER puede aparecer como alias histórico de IRP_MJ_PNP, y IRP_MJ_MAXIMUM_FUNCTION puede figurar como 0x1B. 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:

  1. Abrir View → Open Subviews → Enums (Shift+F10).
  2. Click derecho → Add enum....
  3. Nombre sugerido: Major_Codes.
  4. Agregar los miembros del enum con sus valores.

Opción por header:

  1. Poner el enum en un .h.
  2. En IDA: File → Load file → C header file....
  3. Seleccionar el header.
  4. Verificar que Major_Codes aparezca en la ventana de Enums.

Cómo aplicarlo en el decompiler

Cuando veas algo así:

DriverObject->MajorFunction[14] = sub_140001000;

procedimiento:

  1. Poner el cursor sobre 14.
  2. Apretar m.
  3. Elegir Major_Codes.
  4. Seleccionar IRP_MJ_DEVICE_CONTROL.

Resultado esperado:

DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = sub_140001000;

También sirve sobre comparaciones o switches:

if (IrpStack->MajorFunction == 14)

después de aplicar enum:

if (IrpStack->MajorFunction == IRP_MJ_DEVICE_CONTROL)

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.