Saltar a contenido

Next-Gen Reverse Engineering Training

Módulo 5: Explotación II — Fundamentos del Kernel de Windows

Objetivo del módulo: Comprender la arquitectura del kernel de Windows, cómo se establece y se depura desde WinDbg, los componentes principales del kernel-mode, el modelo de drivers (WDM), las estructuras internas más relevantes (KPCR, KPRCB, KTHREAD, KAPC_STATE, KPROCESS/EPROCESS), la comunicación user-kernel mediante IRPs e IOCTLs, y la técnica clásica de token stealing para obtener privilegios SYSTEM.


Tabla de Contenidos

  1. BinDiff — Instalación y uso
  2. Memoria virtual
  3. Layout del espacio de direcciones x64
  4. User mode vs Kernel mode
  5. Setup y depuración del kernel de Windows
  6. Lab Setup — VM target + WinDbg host
  7. WinDbg Basics
  8. User-Mode Processes
  9. NTDLL — la cara user-mode del kernel
  10. WIN32K.sys — syscalls gráficas
  11. Componentes del Kernel-Mode
  12. Windows Driver Model (WDM)
  13. DRIVER_OBJECT y DriverEntry
  14. DEVICE_OBJECT
  15. I/O Request Packet (IRP) e I/O Stack Location
  16. Acceso a buffers de user-mode (Buffered / Direct / Neither I/O)
  17. Comunicación User-Kernel
  18. Estructuras del kernel
  19. Token Stealing — obtener privilegios SYSTEM
  20. Demo de explotación — HEVD
  21. Glosario

Extras


1. BinDiff — Instalación y uso

BinDiff es un plugin de Google para IDA Pro que permite comparar dos binarios (típicamente la versión vulnerable y la parcheada del mismo módulo) para identificar exactamente qué cambió en un patch. Es la herramienta más usada para hacer patch diffing y descubrir vulnerabilidades 1-day.

Instalación para IDA Pro

Cómo usar BinDiff

  1. Obtener la versión parcheada del archivo a analizar.
  2. Obtener la versión vulnerable inmediatamente anterior al parche (o la más cercana posible).
  3. Abrir la versión parcheada en IDA, dejar que termine el análisis, guardar la base de datos y cerrar IDA.
  4. Abrir la versión vulnerable en IDA. Cuando termine el análisis: EDIT → Plugin → BinDiff → Diff Database
  5. Seleccionar el .i64 de la versión parcheada y esperar resultados.

Resultado típico: BinDiff muestra cada función emparejada con un similarity score. Las funciones con score < 1.0 son las que cambiaron — ahí está el patch, y por reflexión, la vulnerabilidad.


2. Memoria virtual

Windows implementa un sistema de memoria virtual basado en un espacio de direcciones plano (lineal) que da a cada proceso la ilusión de tener su propio espacio de direcciones grande y privado.

Concepto Descripción
Memoria virtual Vista lógica de la memoria que puede no corresponder al layout físico
Memory manager Traduce (mapea) direcciones virtuales a direcciones físicas en runtime
Paging Cuando hay menos memoria física que virtual en uso, el memory manager mueve páginas a disco (pagefile.sys)
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      │
│ page 3      │      └──────→│ frame W     │           │ page 3      │
└─────────────┘              └─────────────┘           └─────────────┘
                              ┌──────────┐
                              │   Disk   │  pagefile.sys (páginas swappeadas)
                              └──────────┘

3. Layout del espacio de direcciones x64

  • El espacio de direcciones teórico de 64 bits es de 16 exabytes (EB).
  • Las CPUs actuales solo implementan 48 líneas de direcciones, limitando el espacio efectivo a 256 TB.
  • Ese espacio se divide a la mitad:
  • Lower 128 TB: procesos privados de usuario
  • Upper 128 TB: espacio del sistema (kernel)

Direcciones canónicas

En x86-64 solo se usan 48 bits de dirección. El bit 47 determina si la dirección es:

Bit 47 Tipo
0 User space
1 Kernel space

Los bits 63 a 48 deben ser una copia del bit 47 (sign extension). Si no lo son, la dirección es NON-CANONICAL y la CPU genera un fault inmediato al usarla.

0000 0000 0000 0000   ←  inicio del user space
       ...
0000 7FFF FFFF FFFF   ←  fin del user space (128 TB)

  Non canonical addresses  (zona prohibida)

FFFF 8000 0000 0000   ←  inicio del kernel space
       ...
FFFF FFFF FFFF FFFF   ←  fin del kernel space

Mapa del kernel-mode (system space)

Algunas regiones notables del kernel-mode en x64:

Región Tamaño Propósito
Per Process 128 TB − 64 KB Espacio del proceso (user)
Hyperspace 1 TB Vistas de proceso, working sets
Win32k.sys / Session Data 512 GB Datos de sesión, Win32k, drivers
Four Level Page Table Map 512 GB Tablas de páginas
Shared System Page 4 KB Página compartida entre procesos
System PTEs WS info 512 GB − 4 KB Working set info de PTEs
Kernel CFG Bitmap 1 TB Bitmap de Control Flow Guard
Ultra Zero 16 TB Páginas zero-filled
System Cache 16 TB Caché del sistema
Paged Pool 512 GB − 256 TB Pool paginable
Special Pool 512 GB Pool especial (paged & non-paged)
System Cache WS 1 TB Working sets del system cache
System PTEs 16 TB PTEs del sistema
Non Paged Pool 512 GB − 16 TB Pool no paginable
PFN Database (variable) Base de datos de Page Frame Numbers
HAL Reserved 4 MB Reservado por la HAL

4. User mode vs Kernel mode

Aspecto User mode Kernel mode
Quién corre ahí Aplicaciones de usuario OS, drivers
Acceso a memoria Solo su propio espacio privado Toda la memoria del sistema
Instrucciones de CPU Subset (anillo 3) Todas (anillo 0)
Aislamiento Procesos no pueden interferir entre sí Sin restricciones

¿Por qué la separación?

Protege a las aplicaciones de usuario de acceder o modificar datos críticos del OS. Una aplicación que se comporta mal no puede afectar la estabilidad del sistema entero.

Transición user → kernel

Una aplicación de user mode pasa a kernel mode cuando hace una system service call. Por ejemplo, CreateFile eventualmente necesita llamar a la rutina interna de Windows que maneja la lectura de archivos. Esa rutina, al acceder a estructuras internas del sistema, debe correr en kernel mode.

Mecánica:

  1. La CPU ejecuta una instrucción especial (syscall en x64, int 2E en sistemas viejos).
  2. Esto causa que la CPU entre al system service dispatching code del kernel.
  3. El kernel ejecuta la operación solicitada.
  4. Antes de retornar al thread de usuario, la CPU vuelve a user mode.

Así, el OS se protege a sí mismo y a sus datos contra inspección y modificación por procesos de usuario.


5. Setup y depuración del kernel de Windows

Descargar WinDbg

https://aka.ms/windbg/download

Configurar el Symbol Path

  1. Crear el directorio C:\symbols para almacenar los archivos de símbolos localmente.
  2. Configurar la variable de entorno _NT_SYMBOL_PATH:
srv*C:\symbols*https://msdl.microsoft.com/download/symbols

Esto le dice a WinDbg que cachee los símbolos en C:\symbols y los descargue del Microsoft Symbol Server cuando no los tenga localmente.


6. Lab Setup — VM target + WinDbg host

El kernel debugging requiere dos máquinas: el target (la VM cuyo kernel se va a debuggear) y el host (donde corre WinDbg). La conexión típica es por red.

Paso 1 — Obtener IP de la VM target

En la VM target, ver con ipconfig la IP y el adaptador VMnet asociado. Ejemplo:

IPv4 Address: 192.168.119.128
VMnet1: Host-only, 192.168.119.0

Paso 2 — Identificar la IP del host en el VMnet correspondiente

En el host (donde correrá WinDbg):

Ethernet adapter VMware Network Adapter VMnet1:
  IPv4 Address: 192.168.119.1

Paso 3 — Configurar el target para kernel debugging

Abrir CMD como administrador en la VM target y ejecutar:

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

El primer comando configura los parámetros de debugging por red y genera una clave única. El segundo activa el modo debug en el boot.

Paso 4 — Anotar la clave

La clave generada por newkey debe copiarse — se la necesitará en WinDbg. Ejemplo:

Key=2cc411ap6pnl7.1r9ctkigja3xl.1lso9pd3mxid3.3poen5cou7st1

Paso 5 — Verificar configuración

bcdedit /dbgsettings

Debe mostrar:

key        2cc411ap6pnl7.1r9ctkigja3xl.1lso9pd3mxid3.3poen5cou7st1
debugtype  NET
hostip     192.168.119.1
port       50000
dhcp       Yes

Paso 6 — Conectar WinDbg

En el host:

  1. Abrir WinDbg → Start debugging → Attach to kernel → Net
  2. Completar:
  3. Port number: 50000
  4. Key: la clave del paso 4
  5. Click OK.

Paso 7 — Reiniciar la VM target

Al reiniciar, WinDbg comenzará a debuggear el kernel. La consola mostrará algo como:

Microsoft (R) Windows Debugger
Connected to Windows 10 x64 target at (date)
Kernel Debugger connection established.
nt!DbgBreakPointWithStatus:
fffff807`3fa28610 cc            int 3

7. WinDbg Basics

Módulos y símbolos

Comando Descripción
.reload /f Recarga símbolos de los módulos cargados (forzado)
.reload /u Descarga símbolos
lm Lista módulos cargados
x Muestra símbolos cargados
!process 0 0 Lista todos los procesos
!process -1 0 Muestra el contexto de proceso actual
.process /i <eprocess> Cambia al contexto de otro proceso

Breakpoints

Comando Descripción
bp <addr> Breakpoint normal (software)
bl Lista breakpoints activos
ba e1 <addr> Breakpoint hardware en ejecución, tamaño 1 byte
bc <id> Borrar breakpoint por ID

Tracing y stepping

Comando Descripción Atajo
g Continue F5
p Step over F10
t Step into F11

Cheat sheet completa: https://github.com/f1zm0/WinDBG-Cheatsheet


8. User-Mode Processes

Tipos de procesos en user-mode:

Tipo Descripción Ejemplos
User processes Procesos de aplicaciones (32 o 64 bits) notepad.exe, chrome.exe
Service processes Hostean servicios de Windows. Corren independientes del logon Task Scheduler, Print Spooler
System processes Procesos fijos no iniciados por el SCM system, smss.exe, services.exe, lsass.exe
Environment subsystem Implementan parte del soporte del entorno OS csrss.exe, wininit.exe

9. NTDLL — la cara user-mode del kernel

ntdll.dll es el módulo de user-mode que actúa como puente hacia el kernel: contiene los stubs (envoltorios) que disparan las syscalls.

Estructura de un stub de syscall en x64

ntdll!NtOpenFile:
00007FFB`06B5145D   4C:8BD1     mov    r10, rcx        ; primer argumento → r10
00007FFB`06B51460   B8 33000000 mov    eax, 33h        ; SSN (System Service Number)
00007FFB`06B51465   F60425 ...  test   byte ptr ds:[7FFE0308h], 1
00007FFB`06B5146D   75 03       jne    short next
00007FFB`06B5146F   0F05        syscall                ; transición a kernel
00007FFB`06B51471   C3          ret

El System Service Number (SSN) se carga en EAX (0x33 en este caso para NtOpenFile) y luego syscall transfiere el control al kernel.

Resolver la dirección kernel correspondiente

Estando en user-mode dentro de una función de NTDLL, para calcular la dirección de la función equivalente en kernel-mode hay que buscar en la System Service Descriptor Table (SSDT) la entrada indicada por el valor en EAX antes del syscall.

En x64, la SSDT contiene offsets relativos de 32 bits en lugar de punteros directos. Para obtener la dirección absoluta:

  1. Leer el DWORD de la entrada de la SSDT
  2. Hacer right-shift de 4 bits (>> 4)
  3. Sumar al base address de KiServiceTable

Ejemplo práctico en WinDbg

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

Comandos explicados:

Token Significado
dd Display dword
/c1 Mostrar 1 columna
nt!KiServiceTable + (0x33 * 4) KiServiceTable apunta al inicio de la tabla en ntoskrnl.exe. 0x33 es el SSN. * 4 porque cada entrada es DWORD
L1 Length: leer 1 elemento
u Unassemble
>>4 Right shift 4 bits para obtener el offset real en x64

Recurso útil: https://j00ru.vexillium.org/syscalls/nt/64/ — tablas de syscalls por versión de Windows, busca por SSN.


10. WIN32K.sys — syscalls gráficas

win32k.sys implementa la GUI (USER y GDI). Sus syscalls tienen SSN > 0x1000 y se resuelven contra W32pServiceTable en lugar de KiServiceTable.

Resolver una syscall de win32k.sys — ejemplo con SSN 0x1022

Paso 1 — Extraer el offset

0x1022 & 0xFFF = 0x22

Bitwise AND con 0xFFF para obtener el offset dentro de la tabla.

Paso 2 — Leer la entrada

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

Devuelve un valor codificado: 0xff84e740.

Paso 3 — Convertir a entero con signo

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

Interpretado como int32 signed: -8067264.

Paso 4 — Calcular la dirección real

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

Nota: 0n es prefijo decimal en WinDbg, >>> es arithmetic right shift (preserva el signo).


11. Componentes del Kernel-Mode

Componentes principales

Componente Archivo Descripción
Windows Executive Ntoskrnl.exe (prefijo Ex) Servicios base del OS: memory mgmt, process/thread mgmt, security I/O, networking, IPC
Windows Kernel Ntoskrnl.exe (prefijo Ke) Funciones de bajo nivel: thread scheduling, interrupt/exception dispatching, multiprocessor sync. Provee primitivas que el resto del executive usa
Device drivers *.sys Drivers de hardware (traducen I/O calls a operaciones de hardware) y no-hardware (file system, network, filter drivers)
HAL (Hardware Abstraction Layer) Hal.dll Aísla el kernel y los drivers de las diferencias de hardware específicas de plataforma
Win32k subsystem Win32k.sys Implementa funciones GUI (USER y GDI): ventanas, controles, drawing
Hypervisor Hvix64.exe El hypervisor en sí. Compone múltiples capas: memory manager propio, virtual processor scheduler, interrupt/time mgmt, sync routines, partitions (VMs), inter-partition communication (IPC)

Diagrama de capas

┌─────────────────────────────────────────────────────────────────────┐
│ User Mode                                                            │
│  Environment Subsystems (CSRSS) | System processes (Wininit, SCM)   │
│  Services (Spooler, SvcHost) | Applications (User Process)           │
│  Subsystem DLLs                                                      │
│              │                                                       │
│              ▼                                                       │
│           NTDLL.DLL                                                  │
└─────────────────────────────────────────────────────────────────────┘
─────────────────────────────────────────────────────────────────────────
┌─────────────────────────────────────────────────────────────────────┐
│ Kernel Mode                                                          │
│           System Service Dispatcher (KiSystemCall64)                 │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │ I/O Mgr | Sec Ref Mon | Plug&Play | Power | Memory Mgr      │    │
│  │ Process Mgr | Config Mgr | Object Mgr | ALPC                │    │
│  │              Device & File System Drivers                    │    │
│  │                       Kernel                                 │    │
│  │                                                  Win32k +   │    │
│  │                                                  Graphics    │    │
│  └─────────────────────────────────────────────────────────────┘    │
│           Hardware Abstraction Layer (HAL) + HAL Extensions          │
└─────────────────────────────────────────────────────────────────────┘
                          Hyper-V Hypervisor

12. Windows Driver Model (WDM)

  • WDM es el framework de drivers introducido con Windows 2000.
  • Los drivers WDM se organizan en stacks por capas y se comunican mediante I/O Request Packets (IRPs).
                  Aplicación user-mode
                  I/O Manager
             ┌───────────────────┐
             │ Filter Driver     │  ← capa superior (filter)
             ├───────────────────┤
             │ Function Driver   │  ← driver principal del dispositivo
             ├───────────────────┤
             │ Bus Driver        │  ← driver del bus (PCI, USB)
             └───────────────────┘
                     Hardware

Cada IRP recorre el stack de arriba hacia abajo (request) y de abajo hacia arriba (completion).


13. DRIVER_OBJECT y DriverEntry

DRIVER_OBJECT

El I/O Manager crea un DRIVER_OBJECT para cada driver instalado y cargado. Cada DRIVER_OBJECT representa la imagen de un driver kernel-mode cargado.

typedef struct _DRIVER_OBJECT {
    CSHORT                  Type;
    CSHORT                  Size;
    PDEVICE_OBJECT          DeviceObject;
    ULONG                   Flags;
    PVOID                   DriverStart;
    ULONG                   DriverSize;
    PVOID                   DriverSection;
    PDRIVER_EXTENSION       DriverExtension;
    UNICODE_STRING          DriverName;
    PUNICODE_STRING         HardwareDatabase;
    PFAST_IO_DISPATCH       FastIoDispatch;
    PDRIVER_INITIALIZE      DriverInit;
    PDRIVER_STARTIO         DriverStartIo;
    PDRIVER_UNLOAD          DriverUnload;
    PDRIVER_DISPATCH        MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT, *PDRIVER_OBJECT;

MajorFunction — la dispatch table

Es un array de punteros a funciones indexado por los códigos IRP_MJ_XXX. Cada driver setea entry points para los IRP majors que maneja.

Constante Valor Operación
IRP_MJ_CREATE 0x00 Apertura del dispositivo (CreateFile)
IRP_MJ_CLOSE 0x02 Cierre (CloseHandle)
IRP_MJ_READ 0x03 Lectura (ReadFile)
IRP_MJ_WRITE 0x04 Escritura (WriteFile)
IRP_MJ_DEVICE_CONTROL 0x0E IOCTL (DeviceIoControl)
IRP_MJ_CLEANUP 0x12 Cleanup

DriverEntry

DriverEntry es la primera rutina del driver que se invoca al cargar. Es responsable de inicializar el driver y su DRIVER_OBJECT.

NTSTATUS DriverEntry(
    _In_ PDRIVER_OBJECT  DriverObject,
    _In_ PUNICODE_STRING RegistryPath
)
{
    // ...

    // IRP_MJ_DEVICE_CONTROL = 0xE — handler de IOCTLs
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchDeviceControl;

    // Rutina de descarga del driver
    DriverObject->DriverUnload = DriverUnload;

    // ...
    return STATUS_SUCCESS;
}

14. DEVICE_OBJECT

El sistema operativo representa cada dispositivo mediante un DEVICE_OBJECT. Uno o más device objects pueden estar asociados a cada dispositivo y sirven como target de todas las operaciones sobre él.

  • Definidos por la estructura DEVICE_OBJECT, gestionados por el Object Manager.
  • Pueden tener nombre (named device object) y soportar handles abiertos sobre ellos.
  • Encadenados en una linked list (NextDevice).

Relación entre DRIVER_OBJECT y DEVICE_OBJECT

DRIVER_OBJECT                       DEVICE_OBJECT (1)              DEVICE_OBJECT (2)
┌───────────────┐                   ┌──────────────────┐           ┌──────────────────┐
│ ...           │                   │ Size  | Type     │           │ Size  | Type     │
│ DeviceObject ─┼──────────────────→│ DriverObject     ├──┐        │ DriverObject     ├── (apunta de vuelta al driver)
│ ...           │                   │ NextDevice       ├──┘ ──────→│ NextDevice       ├──→ NULL
└───────────────┘                   │ CurrentIrp       │           │ ...              │
                                    │ DeviceExtension  │           └──────────────────┘
                                    │ Device Ext Spec  │
                                    └──────────────────┘

Idea clave: el DRIVER_OBJECT representa el comportamiento del driver (su código), mientras que los DEVICE_OBJECT representan endpoints de comunicación concretos.


15. I/O Request Packet (IRP) e I/O Stack Location

IRP — concepto

El IRP es la estructura básica que encapsula operaciones de I/O en Windows.

  • Las operaciones de I/O viajan por el stack de drivers dentro de un IRP.
  • Cada driver del stack obtiene un I/O stack location propio con los parámetros de su request.
  • El IRP tiene major codes (operación principal: CREATE, READ, WRITE, DEVICE_CONTROL, CLEANUP, CLOSE) y minor codes (subdivisiones).
  • Los IRPs están asociados al thread que originó la I/O request.

Estructura del IRP

Consta de dos partes:

1. Header

Usado por el I/O Manager para almacenar info sobre el request original (parámetros independientes del dispositivo, dirección del device object donde el archivo está abierto, etc.). También usado por los drivers para almacenar el status final.

2. I/O stack locations

Después del header viene un set de I/O stack locations, una por cada driver del chain de drivers layered al que el request está bound. Cada stack location contiene los parámetros, function codes y context que el driver correspondiente necesita.

typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _IRP {
    // ...
    union {
        struct {
            union { ... };
            PETHREAD            Thread;
            PCHAR               AuxiliaryBuffer;
            struct {
                LIST_ENTRY      ListEntry;
                union {
                    struct _IO_STACK_LOCATION* CurrentStackLocation;  // ← location del driver actual
                    ULONG       PacketType;
                };
            };
            PFILE_OBJECT        OriginalFileObject;
        } Overlay;
        KAPC                    Apc;
        PVOID                   CompletionKey;
    } Tail;
} IRP;

IO_STACK_LOCATION

Cada IRP va seguido de uno o más I/O stack locations (uno por driver layered).

typedef struct _IO_STACK_LOCATION {
    UCHAR  MajorFunction;        // IRP_MJ_XXX
    UCHAR  MinorFunction;
    UCHAR  Flags;
    UCHAR  Control;
    union {
        // ...
        struct {
            ULONG OutputBufferLength;
            ULONG POINTER_ALIGNMENT InputBufferLength;
            ULONG POINTER_ALIGNMENT IoControlCode;   // ← IOCTL code
            PVOID Type3InputBuffer;
        } DeviceIoControl;
        // ...
    } Parameters;
    PDEVICE_OBJECT             DeviceObject;
    PFILE_OBJECT               FileObject;
    PIO_COMPLETION_ROUTINE     CompletionRoutine;
    PVOID                      Context;
} IO_STACK_LOCATION, *PIO_STACK_LOCATION;

Para reversing de drivers: la unión DeviceIoControl es donde aparecen los IoControlCode que identifican qué operación específica se está pidiendo. Estos IOCTLs son la superficie de ataque más común contra drivers vulnerables.


16. Acceso a buffers de user-mode (Buffered / Direct / Neither I/O)

Cuando un driver recibe un IRP que viene de user-mode, los buffers del usuario tienen que ser accedidos de forma segura. El I/O Manager soporta tres métodos:

Buffered I/O

El I/O Manager aloca un mirror buffer del mismo tamaño que el del usuario en non-paged pool, copia los datos y guarda el puntero al nuevo buffer en Irp->AssociatedIrp.SystemBuffer.

  • ✅ Seguro: el driver opera sobre memoria kernel.
  • ⚠️ Costoso: copia completa del buffer.

Direct I/O

El I/O Manager lockea las páginas del buffer del usuario en memoria física (mediante MDLs - Memory Descriptor Lists) y le da al driver una manera de acceder directamente sin copiar.

  • ✅ Eficiente para buffers grandes.
  • ⚠️ El driver debe usar MmGetSystemAddressForMdlSafe para mappear el MDL.

Neither I/O

El I/O Manager no realiza ningún manejo de buffer. El driver recibe el puntero original del usuario directamente.

  • Peligroso: el driver es responsable de validar el puntero, su rango, y las condiciones de race (TOCTOU).
  • 🎯 Vector de ataque común: muchos bugs de drivers vienen de Neither I/O sin validación adecuada (no se usa ProbeForRead/ProbeForWrite).

17. Comunicación User-Kernel

Para comunicarse desde user-mode a un driver kernel-mode, una aplicación debe:

  1. Abrir un handle al device con CreateFile (como si fuera un archivo).
  2. Enviar/recibir mensajes con DeviceIoControl, ReadFile o WriteFile.
  3. Cerrar el handle con CloseHandle.
HANDLE device = CreateFile(
    L"\\\\.\\DeviceName",                 // path al device (formato \\.\)
    GENERIC_READ | GENERIC_WRITE,
    FILE_SHARE_READ | FILE_SHARE_WRITE,
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    NULL
);

if (device != INVALID_HANDLE_VALUE) {
    DWORD bytes_returned;
    BOOL result = DeviceIoControl(
        device,
        IOCTL_CODE,                       // código IOCTL específico del driver
        input_buffer,  sizeof(input_buffer),
        output_buffer, sizeof(output_buffer),
        &bytes_returned,
        NULL
    );

    CloseHandle(device);
}

Para reversing/exploitation: identificar los IOCTL codes que el driver acepta (vía sus dispatch routines) es el primer paso. Cada IOCTL es un punto de entrada.


18. Estructuras del kernel

Las estructuras más relevantes para entender y explotar el kernel de Windows:

Estructura Descripción breve
KPCR Kernel Processor Control Region — info del procesador actual
KPRCB Kernel Processor Control Block — embedded en KPCR, info de scheduling
KTHREAD Kernel Thread — núcleo de la estructura ETHREAD
KAPC_STATE Kernel Asynchronous Procedure Call State
KPROCESS / EPROCESS Process Object — estructura de proceso

18.1 KPCR — Kernel Processor Control Region

Estructura de datos kernel-mode que contiene info sobre el procesador actual:

  • Interrupt Dispatch Table (IDT)
  • Task State Segment (TSS)
  • Global Descriptor Table (GDT)
  • Estado del interrupt controller

El kernel mantiene una KPCR por cada procesador lógico. En x64 Windows, el kernel guarda un puntero a la KPCR en el registro gs.

typedef struct _KPCR {
    // ...
    UCHAR   SecondLevelCacheAssociativity;
    UCHAR   ObsoleteNumber;
    UCHAR   Fill0;
    ULONG   Unused0[3];
    USHORT  MajorVersion;
    USHORT  MinorVersion;
    ULONG   StallScaleFactor;
    PVOID   Unused1[3];
    ULONG   KernelReserved[15];
    ULONG   SecondLevelCacheSize;
    ULONG   HalReserved[16];
    ULONG   Unused2;
    PVOID   KdVersionBlock;
    PVOID   Unused3;
    ULONG   PcrAlign1[24];
    KPRCB   Prcb;                    // ← KPRCB embedded
} KPCR, *PKPCR;

Acceso en x64: gs:[0] apunta a la KPCR del procesador actual.

18.2 KPRCB — Kernel Processor Control Block

Estructura embebida en la KPCR. Contiene:

  • Info de scheduling, dispatcher database del procesador
  • DPC queue
  • Info de vendor de CPU e identificación
  • Tamaños de cache, time accounting, I/O stats
  • Cache manager stats, DPC stats, memory manager stats
  • Look-aside lists de non-paged y paged pool
typedef struct _KPRCB {
    ULONG    MxCsr;
    UCHAR    Number;
    UCHAR    NestingLevel;
    BOOLEAN  InterruptRequest;
    BOOLEAN  IdleHalt;
    PKTHREAD CurrentThread;       // ← thread ejecutándose actualmente
    PKTHREAD NextThread;
    // ...
} KPRCB, *PKPRCB;

Tip de exploitation: desde un thread kernel se puede obtener el thread actual con KeGetCurrentThread() o leyendo gs:[0x188] (ofset hardcodeado a CurrentThread en el KPRCB).

18.3 KTHREAD — Kernel Thread

Es la porción del Kernel Core de la estructura ETHREAD. La ETHREAD es el thread object expuesto por el Object Manager; la KTHREAD es su núcleo. Contiene info para scheduling, sincronización y time-keeping.

struct _KTHREAD {
    // ...
    union {
        KAPC_STATE ApcState;          // ← estado APC actual (incluye proceso!)
        struct {
            UCHAR ApcStateFill[43];
            CHAR  Priority;
            ULONG UserIdealProcessor;
        };
    };
    volatile LONGLONG WaitStatus;
    PKWAIT_BLOCK      WaitBlockList;
    // ...
} KTHREAD, *PKTHREAD;

18.4 KAPC_STATE — APC State

Representa el estado de Asynchronous Procedure Call (APC) del thread. La parte clave:

typedef struct _KAPC_STATE {
    LIST_ENTRY        ApcListHead[2];
    struct _KPROCESS* Process;        // ← apunta al proceso del thread!
    union {
        UCHAR InProgressFlags;
        struct {
            UCHAR KernelApcInProgress  : 1;
            UCHAR SpecialApcInProgress : 1;
        };
    };
    UCHAR  KernelApcPending;
    union { ... };
} KAPC_STATE, *PKAPC_STATE;

Idea clave: desde el thread actual (KTHREAD) se puede llegar al EPROCESS del proceso actual vía Thread->ApcState.Process. Este es el punto de partida del token stealing.

18.5 KPROCESS / EPROCESS — Process Object

KPROCESS es la porción del Kernel del struct EPROCESS del Executive. La EPROCESS es el process object expuesto por el Object Manager; la KPROCESS es su inicio.

typedef struct _EPROCESS {
    KPROCESS         Pcb;                    // ← KPROCESS embedded
    EX_PUSH_LOCK     ProcessLock;
    VOID*            UniqueProcessId;        // PID
    LIST_ENTRY       ActiveProcessLinks;     // ← linked list de procesos
    // ...
    EX_FAST_REF      Token;                  // ← TOKEN del proceso (privilegios)
    // ...
} EPROCESS;

Campos clave para exploitation:

Campo Uso en explotación
UniqueProcessId El PID. PID 4 = SYSTEM
ActiveProcessLinks Lista doblemente enlazada — permite iterar todos los procesos
Token El access token que define los privilegios del proceso

19. Token Stealing — obtener privilegios SYSTEM

La técnica clásica de privilege escalation desde una vulnerabilidad kernel: robarle el token al proceso SYSTEM.

Algoritmo

  1. Obtener el EPROCESS actual (vía KTHREAD.ApcState.Process o IoGetCurrentProcess()).
  2. Iterar EPROCESS.ActiveProcessLinks (lista doblemente enlazada) para recorrer todos los procesos del sistema.
  3. Comparar cada EPROCESS.UniqueProcessId con 4. Cuando coincida → es el proceso SYSTEM.
  4. Reemplazar EPROCESS.Token del proceso actual con el Token del proceso SYSTEM.

A partir de ese momento, el proceso atacante (originalmente con permisos limitados) tendrá los privilegios SYSTEM.

Pseudo-código del shellcode kernel

// 1. Obtener el EPROCESS actual
PEPROCESS current = (PEPROCESS)__readgsqword(0x188);    // KPRCB.CurrentThread
current = (PEPROCESS)((PUCHAR)current + 0x220);          // KTHREAD.ApcState.Process (offset varía por versión)

// 2. Guardar el token del proceso actual (para restaurar luego si quisiéramos)
ULONG_PTR currentToken = current->Token;

// 3. Iterar ActiveProcessLinks buscando PID == 4
PEPROCESS p = current;
do {
    p = (PEPROCESS)((PUCHAR)p->ActiveProcessLinks.Flink - OFFSET_ActiveProcessLinks);
    if ((ULONG_PTR)p->UniqueProcessId == 4) {
        // 4. Robar el token (preservando los low bits del EX_FAST_REF)
        current->Token = (p->Token & ~0xF) | (currentToken & 0xF);
        break;
    }
} while (p != current);

Notas importantes

  • EX_FAST_REF: el campo Token es un EX_FAST_REF, que usa los 3 bits bajos como contador de referencias. Al copiar el token hay que preservar esos bits del token actual.
  • Offsets variables por versión: EPROCESS.Token, ActiveProcessLinks, UniqueProcessId están en distintos offsets según la versión de Windows. En exploits reales se calculan dinámicamente o se hardcodean por build.
  • Thread vs Process token impersonation: alternativa más sutil → no robar el Process Token sino impersonar usando un Thread Token.

Cómo prevenir token stealing (mitigaciones)

  • KASLR: randomización del kernel base address — dificulta encontrar nt!PsInitialSystemProcess y offsets.
  • SMEP (Supervisor Mode Execution Prevention): impide ejecutar shellcode en páginas user-mode desde kernel.
  • SMAP (Supervisor Mode Access Prevention): impide leer/escribir páginas user-mode desde kernel sin habilitarlo explícitamente.
  • CFG / kCFG: Control Flow Guard kernel-mode valida llamadas indirectas.
  • HVCI / VBS: Hypervisor-protected Code Integrity — el hypervisor protege la integridad del código kernel.

20. Demo de explotación — HEVD

HEVD (HackSys Extreme Vulnerable Driver) es un driver de Windows intencionalmente vulnerable, mantenido por HackSys Team, diseñado para enseñar exploitation kernel-mode. Implementa una variedad de vulnerabilidades clásicas:

  • Stack-based buffer overflow
  • Heap-based buffer overflow (Pool Overflow)
  • Use-After-Free
  • Double Fetch
  • Type Confusion
  • Arbitrary write (Write-What-Where)
  • Null Pointer Dereference
  • Integer Overflow
  • Race Conditions

Repositorio: https://github.com/hacksysteam/HackSysExtremeVulnerableDriver

Workflow típico para explotar HEVD

  1. Cargar HEVD en una VM target (con kernel debugging configurado contra el host con WinDbg).
  2. Identificar el device name que crea el driver (con WinObj o reversing del DriverEntry).
  3. Reversing del dispatch handler para enumerar los IOCTLs soportados y sus funciones vulnerables.
  4. Construir el exploit en user-mode que abre el device con CreateFile, dispara la vulnerabilidad con DeviceIoControl.
  5. Lograr ejecución de código kernel o primitiva de read/write.
  6. Token stealing → escalada a SYSTEM.
  7. Spawnear cmd.exe con privilegios SYSTEM para confirmar el éxito.

Aproximación recomendada: empezar por stack overflow (la vulnerabilidad más simple). Con SMEP activado se requiere ROP en kernel para bypass, lo que añade complejidad pero es el escenario realista.


21. Glosario

Término Significado Descripción
VA Virtual Address Dirección lógica usada por CPU/procesos. El MMU la traduce a una dirección física mediante tablas de páginas.
PA Physical Address Dirección real en memoria física/RAM. En WinDbg se puede leer con comandos de display memory físicos como !db o !dq.
PTE Page Table Entry Entrada de tabla de páginas. Contiene el PFN y flags de permisos/estado de una página.
PFN Page Frame Number Número del frame físico donde vive una página. La base física se obtiene con PFN << 12; la dirección exacta suma el offset VA & 0xFFF.
Page offset Offset de página Los 12 bits bajos de una dirección en páginas de 0x1000 bytes. Se conservan al pasar de VA a PA.
NPFS Named Pipe File System Driver/componente (npfs.sys) que implementa named pipes y pipes anónimos por debajo.
NpFr NPFS pool tag Pool tag de 4 bytes que identifica ciertas allocations internas de NPFS relacionadas con pipes.
Pool tag Etiqueta de pool Identificador de 4 bytes usado por el kernel para rastrear allocations del pool (Hack, NpFr, etc.). Se ve con !pool / !poolfind.
NonPagedPool Pool no paginable Memoria kernel que no puede irse a disco. Se usa para estructuras que deben estar disponibles a IRQL alto o durante I/O crítico.
NonPagedPoolNx NonPagedPool no ejecutable Variante no ejecutable del NonPagedPool. Reduce la posibilidad de ejecutar shellcode directamente desde pool.
HEVD HackSys Extreme Vulnerable Driver Driver vulnerable usado para practicar kernel exploitation.
UAF Use-After-Free Bug donde se usa un puntero después de liberar el objeto al que apuntaba.
Reclaimer Objeto reclamador Allocation controlada que intenta ocupar un chunk liberado por una vulnerabilidad. Ejemplo: pipes NpFr reclamando un chunk Hack.
Pool spray Spray de pool Crear muchas allocations parecidas para moldear el estado del allocator del kernel.
IRP I/O Request Packet Estructura que representa una operación de I/O en Windows y viaja por el stack de drivers.
IOCTL I/O Control Code Código enviado con DeviceIoControl para pedir una operación específica a un driver.
DRIVER_OBJECT Objeto de driver Estructura kernel que representa un driver cargado y contiene su tabla MajorFunction[].
DEVICE_OBJECT Objeto de dispositivo Endpoint kernel que una app abre con CreateFile("\\\\.\\DeviceName", ...).
MajorFunction[] Tabla de dispatch Array de handlers del driver indexado por códigos IRP_MJ_*.
IRP_MJ_DEVICE_CONTROL Major code 0x0E Handler de DeviceIoControl; suele ser la superficie principal de explotación en drivers.
SSN System Service Number Número de syscall cargado en EAX por los stubs de ntdll antes de ejecutar syscall.
SSDT System Service Descriptor Table Tabla que resuelve SSNs a funciones kernel (KiServiceTable para NT syscalls).
KiServiceTable Tabla de syscalls NT Tabla usada por el kernel para despachar syscalls normales de ntoskrnl.
W32pServiceTable Tabla de syscalls Win32k Tabla usada para syscalls gráficas/USER/GDI de win32k.sys.
KPCR Kernel Processor Control Region Estructura por CPU con datos del procesador actual; en x64 se accede vía gs.
KPRCB Kernel Processor Control Block Bloque dentro de KPCR con estado de scheduling, thread actual y datos por procesador.
KTHREAD Kernel Thread Núcleo kernel de un thread; desde ahí se llega al proceso actual vía ApcState.Process.
KAPC_STATE Kernel APC State Estado APC del thread; contiene el puntero al proceso asociado.
EPROCESS Executive Process Estructura kernel que representa un proceso. Contiene PID, lista de procesos y token.
Token Access token Objeto que define identidad, grupos y privilegios de un proceso/thread.
EX_FAST_REF Fast reference Tipo usado para referencias compactas; en campos como EPROCESS.Token reserva bits bajos para contador/flags.
KASLR Kernel Address Space Layout Randomization Randomiza bases de módulos kernel para dificultar direcciones hardcodeadas.
SMEP Supervisor Mode Execution Prevention Impide que kernel ejecute código ubicado en páginas user-mode.
SMAP Supervisor Mode Access Prevention Impide que kernel acceda a páginas user-mode sin habilitación explícita.
kCFG Kernel Control Flow Guard Mitigación que valida ciertos saltos/llamadas indirectas en kernel.
HVCI/VBS Hypervisor-protected Code Integrity / Virtualization-Based Security Mitigaciones basadas en virtualización para proteger integridad de código y aislar componentes.

Apunte basado en el material de Binary Gecko — Next-Gen Reverse Engineering Training, Módulo 5: Exploitation II — Kernel Basics.