Saltar a contenido

Pattern matching de drivers vulnerables de Windows

Objetivo: tener ejemplos de código C/C++ de drivers Windows intencionalmente vulnerables para reconocer patrones durante reversing: DriverEntry, MajorFunction[], IRP_MJ_DEVICE_CONTROL, IO_STACK_LOCATION, métodos de buffering, bugs de tamaño, UAF, arbitrary write, leaks y APIs peligrosas de memoria física.

← Volver al Módulo 5


Tabla de Contenidos

  1. Cómo leer estos ejemplos
  2. Base mínima de un driver WDM vulnerable
  3. IOCTLs y métodos de buffering
  4. Stack overflow vía METHOD_NEITHER
  5. Pool overflow vía METHOD_BUFFERED
  6. Use-After-Free con global dangling pointer
  7. Write-What-Where / arbitrary write
  8. Deref insegura de punteros user-mode
  9. Double fetch / TOCTOU en METHOD_NEITHER
  10. Information leak por memoria no inicializada
  11. Leak de MSR: __readmsr controlado por usuario
  12. Mapeo inseguro de memoria física
  13. Kernel virtual arbitrary read
  14. Kernel virtual arbitrary write
  15. Read/write de memoria de procesos con MmCopyVirtualMemory
  16. Mapear memoria kernel/física a userland
  17. Virtual-to-Physical helper inseguro
  18. I/O ports, PCI config y MSR write expuestos
  19. Pattern matching rápido en IDA / WinDbg
  20. Checklist de auditoría estática

1. Cómo leer estos ejemplos

Estos snippets son patrones educativos de laboratorio. Están escritos como C++ de WDK, pero el estilo real de muchos drivers Windows sigue siendo C con tipos del kernel (PIRP, PDEVICE_OBJECT, PIO_STACK_LOCATION, NTSTATUS, etc.).

La idea no es copiar/pegar un driver productivo, sino aprender a reconocer en IDA o Ghidra:

Patrón Qué significa
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ... Handler de DeviceIoControl: superficie principal de ataque
Irp->Tail.Overlay.CurrentStackLocation Acceso al IO_STACK_LOCATION actual
Parameters.DeviceIoControl.IoControlCode Switch de IOCTLs
Type3InputBuffer / UserBuffer Punteros raw de user-mode (METHOD_NEITHER)
Irp->AssociatedIrp.SystemBuffer Buffer kernel usado por METHOD_BUFFERED
ExAllocatePool* + ExFreePool* Objetos del kernel pool; buscar UAF / pool overflow
MmMapIoSpace, ZwMapViewOfSection, __readmsr APIs de alto riesgo si sus argumentos vienen de user-mode

Regla mental: en kernel-mode, un bug de validación no crashea “solo el programa”: puede crashear toda la máquina o convertirse en una primitiva de lectura/escritura kernel.


2. Base mínima de un driver WDM vulnerable

Este esqueleto muestra el patrón común: DriverEntry crea el device, crea el symbolic link, registra IRP_MJ_CREATE, IRP_MJ_CLOSE y IRP_MJ_DEVICE_CONTROL, y el dispatcher rutea IOCTLs.

#include <ntddk.h>

#define DEVICE_NAME   L"\\Device\\VulnPatterns"
#define SYMLINK_NAME  L"\\DosDevices\\VulnPatterns"

#define IOCTL_VULN_STACK_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_NEITHER,  FILE_ANY_ACCESS)
#define IOCTL_VULN_POOL_OVERFLOW  CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_VULN_UAF_ALLOC      CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_VULN_UAF_FREE       CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_VULN_UAF_USE        CTL_CODE(FILE_DEVICE_UNKNOWN, 0x804, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_VULN_WWW            CTL_CODE(FILE_DEVICE_UNKNOWN, 0x805, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_VULN_DOUBLE_FETCH   CTL_CODE(FILE_DEVICE_UNKNOWN, 0x806, METHOD_NEITHER,  FILE_ANY_ACCESS)
#define IOCTL_VULN_INFO_LEAK      CTL_CODE(FILE_DEVICE_UNKNOWN, 0x807, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_VULN_READ_MSR       CTL_CODE(FILE_DEVICE_UNKNOWN, 0x808, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_VULN_PHYS_READ      CTL_CODE(FILE_DEVICE_UNKNOWN, 0x809, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_VULN_KERNEL_READ    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80A, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_VULN_KERNEL_WRITE   CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80B, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_VULN_PROC_RW        CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80C, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_VULN_MAP_USER       CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80D, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_VULN_VA_TO_PA       CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80E, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_VULN_IO_PORT        CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80F, METHOD_BUFFERED, FILE_ANY_ACCESS)

extern "C" DRIVER_INITIALIZE DriverEntry;
extern "C" DRIVER_UNLOAD DriverUnload;

static NTSTATUS DispatchCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp);
static NTSTATUS DispatchDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp);

static NTSTATUS CompleteIrp(PIRP Irp, NTSTATUS Status, ULONG_PTR Information)
{
    Irp->IoStatus.Status = Status;
    Irp->IoStatus.Information = Information;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return Status;
}

extern "C"
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(RegistryPath);

    UNICODE_STRING deviceName;
    UNICODE_STRING symbolicLink;
    PDEVICE_OBJECT deviceObject = nullptr;

    RtlInitUnicodeString(&deviceName, DEVICE_NAME);

    NTSTATUS status = IoCreateDevice(
        DriverObject,
        0,
        &deviceName,
        FILE_DEVICE_UNKNOWN,
        FILE_DEVICE_SECURE_OPEN,
        FALSE,
        &deviceObject
    );

    if (!NT_SUCCESS(status)) {
        return status;
    }

    RtlInitUnicodeString(&symbolicLink, SYMLINK_NAME);
    status = IoCreateSymbolicLink(&symbolicLink, &deviceName);

    if (!NT_SUCCESS(status)) {
        IoDeleteDevice(deviceObject);
        return status;
    }

    DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreateClose;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchCreateClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchDeviceControl;
    DriverObject->DriverUnload = DriverUnload;

    return STATUS_SUCCESS;
}

extern "C"
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    UNICODE_STRING symbolicLink;
    RtlInitUnicodeString(&symbolicLink, SYMLINK_NAME);
    IoDeleteSymbolicLink(&symbolicLink);
    IoDeleteDevice(DriverObject->DeviceObject);
}

static NTSTATUS DispatchCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    return CompleteIrp(Irp, STATUS_SUCCESS, 0);
}

Cliente user-mode típico:

HANDLE hDevice = CreateFileA(
    "\\\\.\\VulnPatterns",
    GENERIC_READ | GENERIC_WRITE,
    FILE_SHARE_READ | FILE_SHARE_WRITE,
    nullptr,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    nullptr
);

DeviceIoControl(
    hDevice,
    IOCTL_VULN_STACK_OVERFLOW,
    inputBuffer,
    inputLength,
    outputBuffer,
    outputLength,
    &bytesReturned,
    nullptr
);

Qué buscar en reversing

En DriverEntry:

DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchDeviceControl;

En assembly x64, el DRIVER_OBJECT suele estar en RCX al entrar a DriverEntry, y la tabla MajorFunction[] empieza alrededor de +0x70:

mov     qword ptr [rcx+70h+0Eh*8], offset DispatchDeviceControl

3. IOCTLs y métodos de buffering

Un IOCTL es un DWORD construido con CTL_CODE:

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

Los dos bits bajos (IoControlCode & 3) definen el método:

Method Valor InputBuffer OutputBuffer Riesgo típico
METHOD_BUFFERED 0 Irp->AssociatedIrp.SystemBuffer Irp->AssociatedIrp.SystemBuffer Overflows / leaks si no se validan longitudes
METHOD_IN_DIRECT 1 SystemBuffer Irp->MdlAddress Mal uso de MDL
METHOD_OUT_DIRECT 2 SystemBuffer Irp->MdlAddress Mal uso de MDL
METHOD_NEITHER 3 Type3InputBuffer Irp->UserBuffer Punteros user-mode crudos, TOCTOU, arbitrary read/write

Dispatcher común:

static NTSTATUS DispatchDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);

    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    ULONG ioctl = stack->Parameters.DeviceIoControl.IoControlCode;
    NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
    ULONG_PTR information = 0;

    switch (ioctl) {
    case IOCTL_VULN_STACK_OVERFLOW:
        status = VulnStackOverflow(Irp);
        break;
    case IOCTL_VULN_POOL_OVERFLOW:
        status = VulnPoolOverflow(Irp);
        break;
    case IOCTL_VULN_UAF_ALLOC:
        status = VulnUafAllocate();
        break;
    case IOCTL_VULN_UAF_FREE:
        status = VulnUafFree();
        break;
    case IOCTL_VULN_UAF_USE:
        status = VulnUafUse();
        break;
    default:
        status = STATUS_INVALID_DEVICE_REQUEST;
        break;
    }

    return CompleteIrp(Irp, status, information);
}

Pattern matching del dispatcher

En IDA/Hex-Rays:

stack = Irp->Tail.Overlay.CurrentStackLocation;
ioctl = stack->Parameters.DeviceIoControl.IoControlCode;
switch (ioctl) { ... }

En assembly:

mov     rax, [rdx+0B8h]       ; Irp->Tail.Overlay.CurrentStackLocation
mov     ecx, [rax+18h]        ; Parameters.DeviceIoControl.IoControlCode
cmp     ecx, 222003h
jz      short loc_stack_overflow

Si Hex-Rays muestra Parameters.Read.Length o Parameters.Create.* en un dispatcher de IOCTLs, aplicar Select union field y elegir Parameters.DeviceIoControl.


4. Stack overflow vía METHOD_NEITHER

Patrón clásico estilo HEVD: el driver recibe un puntero user-mode crudo (Type3InputBuffer) y un tamaño (InputBufferLength). Valida que el puntero sea leíble, pero copia userLength bytes a un buffer local fijo de 0x800 sin truncar.

static NTSTATUS VulnStackOverflow(PIRP Irp)
{
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);

    PVOID userBuffer = stack->Parameters.DeviceIoControl.Type3InputBuffer;
    SIZE_T userLength = stack->Parameters.DeviceIoControl.InputBufferLength;

    UCHAR kernelStackBuffer[0x800];
    NTSTATUS status = STATUS_SUCCESS;

    if (userBuffer == nullptr) {
        return STATUS_INVALID_PARAMETER;
    }

    __try {
        ProbeForRead(userBuffer, userLength, sizeof(UCHAR));

        // VULNERABLE:
        // userLength está controlado por user-mode.
        // Si userLength > sizeof(kernelStackBuffer), pisa stack kernel.
        RtlCopyMemory(kernelStackBuffer, userBuffer, userLength);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        status = GetExceptionCode();
    }

    return status;
}

Por qué es vulnerable

Elemento Tamaño / control
Destino kernelStackBuffer[0x800] fijo
Source Type3InputBuffer, controlado por user-mode
Length InputBufferLength, controlado por user-mode

ProbeForRead no protege contra esto: valida que el rango user-mode sea accesible, pero no verifica que el destino kernel tenga capacidad suficiente.

Qué buscar en reversing

Patrones fuertes:

ProbeForRead(userBuffer, userLength, 1);
RtlCopyMemory(localStackBuffer, userBuffer, userLength);
sub     rsp, 828h          ; frame grande: buffer local 0x800 + padding
lea     rcx, [rsp+20h]     ; destino = buffer en stack
mov     rdx, rbx           ; source = puntero user
mov     r8, rsi            ; size = InputBufferLength
call    memmove_or_RtlCopyMemory

Fix mínimo:

if (userLength > sizeof(kernelStackBuffer)) {
    return STATUS_BUFFER_TOO_SMALL;
}

5. Pool overflow vía METHOD_BUFFERED

En METHOD_BUFFERED, el I/O Manager copia el input del usuario a Irp->AssociatedIrp.SystemBuffer. Eso elimina el puntero raw, pero no elimina bugs de tamaño.

typedef struct _POOL_PACKET {
    ULONG Magic;
    UCHAR Data[0x80];
} POOL_PACKET, *PPOOL_PACKET;

static NTSTATUS VulnPoolOverflow(PIRP Irp)
{
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);

    PVOID systemBuffer = Irp->AssociatedIrp.SystemBuffer;
    ULONG inputLength = stack->Parameters.DeviceIoControl.InputBufferLength;

    if (systemBuffer == nullptr || inputLength == 0) {
        return STATUS_INVALID_PARAMETER;
    }

    PPOOL_PACKET packet = (PPOOL_PACKET)ExAllocatePool2(
        POOL_FLAG_NON_PAGED,
        sizeof(POOL_PACKET),
        'kPvG'
    );

    if (packet == nullptr) {
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    packet->Magic = 0x41414141;

    // VULNERABLE:
    // inputLength puede ser mayor que sizeof(packet->Data).
    RtlCopyMemory(packet->Data, systemBuffer, inputLength);

    ExFreePoolWithTag(packet, 'kPvG');
    return STATUS_SUCCESS;
}

Por qué es vulnerable

El destino real es packet->Data[0x80], pero el tamaño copiado es InputBufferLength. Si el usuario manda más de 0x80, corrompe memoria adyacente del kernel pool.

Pattern matching

Buscar esta cadena:

ExAllocatePool2 / ExAllocatePoolWithTag
buffer interno de tamaño fijo
RtlCopyMemory / memcpy con InputBufferLength

En IDA también mirar tags:

mov     r8d, 6B507647h     ; pool tag en little-endian
call    ExAllocatePoolWithTag

Fix mínimo:

if (inputLength > sizeof(packet->Data)) {
    ExFreePoolWithTag(packet, 'kPvG');
    return STATUS_BUFFER_TOO_SMALL;
}

6. Use-After-Free con global dangling pointer

Este patrón replica la idea de HEVD: varios IOCTLs comparten estado global. Uno aloca, otro libera pero no limpia el puntero, y otro usa el objeto liberado.

typedef VOID (*PUAF_CALLBACK)(VOID);

typedef struct _UAF_OBJECT {
    PUAF_CALLBACK Callback;   // +0x00: objetivo típico del atacante
    UCHAR Buffer[0x58];       // +0x08
} UAF_OBJECT, *PUAF_OBJECT;

static PUAF_OBJECT g_UafObject = nullptr;

static VOID LegitimateCallback()
{
    DbgPrint("[+] LegitimateCallback called\n");
}

static NTSTATUS VulnUafAllocate()
{
    PUAF_OBJECT object = (PUAF_OBJECT)ExAllocatePool2(
        POOL_FLAG_NON_PAGED,
        sizeof(UAF_OBJECT),
        'faUG'
    );

    if (object == nullptr) {
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    RtlZeroMemory(object, sizeof(UAF_OBJECT));
    object->Callback = LegitimateCallback;

    g_UafObject = object;
    return STATUS_SUCCESS;
}

static NTSTATUS VulnUafFree()
{
    if (g_UafObject != nullptr) {
        ExFreePoolWithTag(g_UafObject, 'faUG');

        // VULNERABLE:
        // Falta: g_UafObject = nullptr;
    }

    return STATUS_SUCCESS;
}

static NTSTATUS VulnUafUse()
{
    if (g_UafObject != nullptr) {
        // VULNERABLE:
        // Si el chunk fue reclaimed por datos controlados, Callback puede cambiar.
        g_UafObject->Callback();
    }

    return STATUS_SUCCESS;
}

Reclaimer controlado por el usuario

Un cuarto IOCTL suele intentar ocupar el slot liberado con un objeto del mismo bucket:

static NTSTATUS VulnUafAllocateFake(PIRP Irp)
{
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    PVOID systemBuffer = Irp->AssociatedIrp.SystemBuffer;
    ULONG inputLength = stack->Parameters.DeviceIoControl.InputBufferLength;

    if (systemBuffer == nullptr || inputLength < sizeof(UAF_OBJECT)) {
        return STATUS_INVALID_PARAMETER;
    }

    PUAF_OBJECT fake = (PUAF_OBJECT)ExAllocatePool2(
        POOL_FLAG_NON_PAGED,
        sizeof(UAF_OBJECT),
        'faUG'
    );

    if (fake == nullptr) {
        return STATUS_INSUFFICIENT_RESOURCES;
    }


    // VULNERABLE COMO RECLAIMER:
    // Copia un layout controlado por user-mode al mismo bucket del objeto UAF.
    RtlCopyMemory(fake, systemBuffer, sizeof(UAF_OBJECT));

    return STATUS_SUCCESS;
}

Qué buscar en reversing

Patrón de UAF:

IOCTL A: ExAllocatePoolWithTag(...) → escribe global
IOCTL B: ExFreePoolWithTag(global, ...) → NO escribe global = 0
IOCTL C: lee global → dereferencia → call indirect / acceso a campo

Assembly sospechoso:

mov     rcx, cs:g_UafObject
test    rcx, rcx
jz      short loc_done
call    qword ptr [rcx]       ; callback/vtable en offset 0

Fix mínimo:

ExFreePoolWithTag(g_UafObject, 'faUG');
g_UafObject = nullptr;

En targets modernos, el reuse no suele ser determinista por LFH, segment heap, pool randomization y caches por CPU. Ahí aparecen primitives de pool spray/reclaim como pipes (NpFr) o thread names.


7. Write-What-Where / arbitrary write

El driver recibe una estructura con dos punteros y escribe What en Where sin validar que Where sea un destino permitido.

typedef struct _WRITE_WHAT_WHERE {
    PVOID What;
    PVOID Where;
} WRITE_WHAT_WHERE, *PWRITE_WHAT_WHERE;

static NTSTATUS VulnWriteWhatWhere(PIRP Irp)
{
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    PWRITE_WHAT_WHERE request = (PWRITE_WHAT_WHERE)Irp->AssociatedIrp.SystemBuffer;
    ULONG inputLength = stack->Parameters.DeviceIoControl.InputBufferLength;

    if (request == nullptr || inputLength < sizeof(WRITE_WHAT_WHERE)) {
        return STATUS_INVALID_PARAMETER;
    }

    // VULNERABLE:
    // El usuario controla Where. Esto puede escribir en cualquier VA accesible por kernel.
    *(PVOID*)request->Where = request->What;

    return STATUS_SUCCESS;
}

Por qué es grave

Esto da una primitiva de escritura kernel: “escribir este valor en esta dirección”. Con leaks adecuados, ese tipo de primitiva puede modificar estructuras del kernel.

Pattern matching

En pseudocódigo:

request = (PWRITE_WHAT_WHERE)Irp->AssociatedIrp.SystemBuffer;
*(PVOID*)request->Where = request->What;

En assembly:

mov     rax, [rcx]       ; What
mov     rdx, [rcx+8]     ; Where
mov     [rdx], rax       ; write-what-where

Fix conceptual:

  • No aceptar punteros kernel arbitrarios desde user-mode.
  • Escribir solo en buffers propios del driver.
  • Si se necesita copiar a user-mode, usar ProbeForWrite dentro de __try/__except y limitar estrictamente el tamaño.

8. Deref insegura de punteros user-mode

METHOD_NEITHER entrega punteros crudos. Si el driver los dereferencia directo, user-mode puede pasar direcciones inválidas, direcciones kernel, o cambiar el contenido durante el uso.

static NTSTATUS VulnNeitherDeref(PIRP Irp)
{
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);

    PULONG userValue = (PULONG)stack->Parameters.DeviceIoControl.Type3InputBuffer;
    ULONG inputLength = stack->Parameters.DeviceIoControl.InputBufferLength;

    if (userValue == nullptr || inputLength < sizeof(ULONG)) {
        return STATUS_INVALID_PARAMETER;
    }

    // VULNERABLE:
    // Deref directa de un puntero user-mode, sin ProbeForRead y sin SEH.
    ULONG value = *userValue;

    DbgPrint("[+] value = 0x%08X\n", value);
    return STATUS_SUCCESS;
}

Bugs derivados

Caso Resultado
userValue = nullptr null deref / BSoD si no se maneja
userValue = dirección no mapeada page fault en kernel
userValue = kernel VA posible kernel read si no hay validación de modo/rango
User cambia memoria entre lecturas double fetch / TOCTOU

Fix mínimo:

__try {
    ProbeForRead(userValue, sizeof(ULONG), __alignof(ULONG));
    ULONG value = *userValue;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
    return GetExceptionCode();
}

Aun con ProbeForRead, copiar primero a un buffer kernel local y trabajar sobre la copia suele ser más seguro.


9. Double fetch / TOCTOU en METHOD_NEITHER

Double fetch: el driver lee dos veces una estructura user-mode. Entre el check y el uso, otro thread del atacante puede modificar el contenido.

typedef struct _USER_COPY_REQUEST {
    ULONG Size;
    PVOID Buffer;
} USER_COPY_REQUEST, *PUSER_COPY_REQUEST;

static NTSTATUS VulnDoubleFetch(PIRP Irp)
{
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    PUSER_COPY_REQUEST request =
        (PUSER_COPY_REQUEST)stack->Parameters.DeviceIoControl.Type3InputBuffer;

    UCHAR localBuffer[0x100];

    if (request == nullptr) {
        return STATUS_INVALID_PARAMETER;
    }

    __try {
        ProbeForRead(request, sizeof(USER_COPY_REQUEST), __alignof(USER_COPY_REQUEST));

        // CHECK: primera lectura de request->Size.
        if (request->Size > sizeof(localBuffer)) {
            return STATUS_BUFFER_TOO_SMALL;
        }

        ProbeForRead(request->Buffer, request->Size, sizeof(UCHAR));

        // VULNERABLE:
        // USE: segunda lectura de request->Size.
        // Un thread atacante puede cambiar Size entre el check y esta copia.
        RtlCopyMemory(localBuffer, request->Buffer, request->Size);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        return GetExceptionCode();
    }

    return STATUS_SUCCESS;
}

Pattern matching

Buscar lecturas repetidas desde el mismo puntero user-mode:

if (request->Size <= 0x100) {
    ...
    RtlCopyMemory(dst, request->Buffer, request->Size);
}

Fix conceptual: capturar una copia estable.

USER_COPY_REQUEST captured;

__try {
    ProbeForRead(request, sizeof(captured), __alignof(USER_COPY_REQUEST));
    RtlCopyMemory(&captured, request, sizeof(captured));
}
__except (EXCEPTION_EXECUTE_HANDLER) {
    return GetExceptionCode();
}

if (captured.Size > sizeof(localBuffer)) {
    return STATUS_BUFFER_TOO_SMALL;
}

10. Information leak por memoria no inicializada

Un driver puede filtrar stack/pool kernel si copia una estructura no inicializada a SystemBuffer y setea IoStatus.Information con sizeof(struct).

typedef struct _LEAK_RESPONSE {
    ULONG Status;
    PVOID KernelPointer;
    UCHAR Data[0x30];
    ULONG Flags;
} LEAK_RESPONSE, *PLEAK_RESPONSE;

static NTSTATUS VulnInfoLeak(PIRP Irp)
{
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    ULONG outputLength = stack->Parameters.DeviceIoControl.OutputBufferLength;
    PVOID systemBuffer = Irp->AssociatedIrp.SystemBuffer;

    if (systemBuffer == nullptr || outputLength < sizeof(LEAK_RESPONSE)) {
        return STATUS_BUFFER_TOO_SMALL;
    }

    LEAK_RESPONSE response;

    // VULNERABLE:
    // response no se inicializa completa.
    // KernelPointer, Data y padding pueden contener basura del stack kernel.
    response.Status = 0x1337;

    RtlCopyMemory(systemBuffer, &response, sizeof(response));
    Irp->IoStatus.Information = sizeof(response);

    return STATUS_SUCCESS;
}

Por qué importa

Un leak de punteros kernel puede romper KASLR y facilitar ROP/data-only attacks. En drivers reales, también se filtran handles, direcciones de pool, EPROCESS, direcciones de módulos, etc.

Pattern matching

struct local en stack
solo algunos campos inicializados
RtlCopyMemory/SystemBuffer, &local, sizeof(local)
IoStatus.Information = sizeof(local)

Fix mínimo:

LEAK_RESPONSE response = {};

o:

RtlZeroMemory(&response, sizeof(response));

11. Leak de MSR: __readmsr controlado por usuario

rdmsr requiere ring 0. Si un driver expone un IOCTL que llama __readmsr con índice controlado por user-mode, puede filtrar registros como LSTAR (0xC0000082), que apunta a nt!KiSystemCall64 / nt!KiSystemCall64Shadow.

typedef struct _MSR_REQUEST {
    ULONG MsrIndex;
    ULONGLONG MsrValue;
} MSR_REQUEST, *PMSR_REQUEST;

static NTSTATUS VulnReadMsr(PIRP Irp)
{
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    PMSR_REQUEST request = (PMSR_REQUEST)Irp->AssociatedIrp.SystemBuffer;
    ULONG inputLength = stack->Parameters.DeviceIoControl.InputBufferLength;
    ULONG outputLength = stack->Parameters.DeviceIoControl.OutputBufferLength;

    if (request == nullptr ||
        inputLength < sizeof(MSR_REQUEST) ||
        outputLength < sizeof(MSR_REQUEST)) {
        return STATUS_INVALID_PARAMETER;
    }

    // VULNERABLE:
    // MsrIndex está controlado por user-mode.
    request->MsrValue = __readmsr(request->MsrIndex);

    Irp->IoStatus.Information = sizeof(MSR_REQUEST);
    return STATUS_SUCCESS;
}

Por qué es útil para un atacante

Si el usuario pide:

request.MsrIndex = 0xC0000082; // LSTAR

el valor devuelto cae dentro de ntoskrnl.exe. Restando el offset conocido de KiSystemCall64, se obtiene la base del kernel para ese build.

Pattern matching

En pseudocódigo:

msrIndex = *(ULONG*)SystemBuffer;
value = __readmsr(msrIndex);
*(ULONGLONG*)SystemBuffer = value;

En assembly:

mov     ecx, [rax]       ; ECX = índice controlado
rdmsr                   ; EDX:EAX = MSR[ECX]
shl     rdx, 20h
or      rax, rdx

Fix conceptual:

  • No exponer lectura arbitraria de MSR.
  • Si existe un caso legítimo, whitelistear índices específicos y exigir privilegio.

12. Mapeo inseguro de memoria física

Muchos drivers vulnerables de terceros exponen wrappers alrededor de MmMapIoSpace, MmMapIoSpaceEx, ZwMapViewOfSection(\\Device\\PhysicalMemory, ...), MmGetPhysicalAddress o MDLs. Si user-mode controla la dirección física y el tamaño, el driver puede dar lectura/escritura sobre memoria física.

typedef struct _PHYS_READ_REQUEST {
    PHYSICAL_ADDRESS PhysicalAddress;
    ULONG Length;
    UCHAR Data[0x100];
} PHYS_READ_REQUEST, *PPHYS_READ_REQUEST;

static NTSTATUS VulnPhysicalRead(PIRP Irp)
{
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    PPHYS_READ_REQUEST request = (PPHYS_READ_REQUEST)Irp->AssociatedIrp.SystemBuffer;
    ULONG inputLength = stack->Parameters.DeviceIoControl.InputBufferLength;
    ULONG outputLength = stack->Parameters.DeviceIoControl.OutputBufferLength;

    if (request == nullptr ||
        inputLength < sizeof(PHYS_READ_REQUEST) ||
        outputLength < sizeof(PHYS_READ_REQUEST)) {
        return STATUS_INVALID_PARAMETER;
    }

    // VULNERABLE:
    // PhysicalAddress y Length vienen de user-mode.
    // No hay chequeo de privilegios, rango ni tamaño máximo real.
    PVOID mapped = MmMapIoSpace(
        request->PhysicalAddress,
        request->Length,
        MmNonCached
    );

    if (mapped == nullptr) {
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    // También vulnerable si Length > sizeof(request->Data).
    RtlCopyMemory(request->Data, mapped, request->Length);

    MmUnmapIoSpace(mapped, request->Length);
    Irp->IoStatus.Information = sizeof(PHYS_READ_REQUEST);

    return STATUS_SUCCESS;
}

Chequeos que deberían existir

Chequeo Motivo
ExGetPreviousMode() == KernelMode o gating estricto Evitar que user-mode alcance la ruta
SeSinglePrivilegeCheck(SeDebugPrivilege, UserMode) Exigir privilegio explícito
PhysicalAddress >= PAGE_SIZE Bloquear páginas bajas sospechosas
Length <= MAX_ALLOWED_MAP_SIZE Evitar mapeos gigantes o overflows
Rango permitido / MMIO esperado No mapear RAM arbitraria
Driver no blocklisted por WDAC/HVCI BYOVD moderno depende de esto

Pattern matching

Strings/imports fuertes:

MmMapIoSpace
MmMapIoSpaceEx
MmGetPhysicalAddress
ZwMapViewOfSection
HalTranslateBusAddress
\\Device\\PhysicalMemory

Pseudocódigo sospechoso:

physical = request->PhysicalAddress;
size = request->Length;
mapped = MmMapIoSpace(physical, size, MmNonCached);

13. Kernel virtual arbitrary read

Este patrón es re común en drivers BYOVD: el IOCTL recibe una dirección virtual kernel y un tamaño, y el driver copia desde esa dirección hacia un buffer de salida. En otras palabras, userland logra pedirle al driver: “leeme esta VA de kernel”.

typedef struct _KERNEL_READ_REQUEST {
    PVOID KernelAddress;   // fuente controlada por user-mode
    ULONG Size;            // cantidad de bytes a leer
    UCHAR Data[0x100];     // output embebido para simplificar
} KERNEL_READ_REQUEST, *PKERNEL_READ_REQUEST;

static NTSTATUS VulnKernelRead(PIRP Irp)
{
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    PKERNEL_READ_REQUEST request = (PKERNEL_READ_REQUEST)Irp->AssociatedIrp.SystemBuffer;
    ULONG inputLength = stack->Parameters.DeviceIoControl.InputBufferLength;
    ULONG outputLength = stack->Parameters.DeviceIoControl.OutputBufferLength;

    if (request == nullptr ||
        inputLength < sizeof(KERNEL_READ_REQUEST) ||
        outputLength < sizeof(KERNEL_READ_REQUEST)) {
        return STATUS_INVALID_PARAMETER;
    }

    if (request->Size > sizeof(request->Data)) {
        return STATUS_BUFFER_TOO_SMALL;
    }

    __try {
        // VULNERABLE:
        // KernelAddress viene de user-mode. Si apunta a ntoskrnl,
        // EPROCESS, TOKEN, pool, etc., el driver lo copia al output.
        RtlCopyMemory(request->Data, request->KernelAddress, request->Size);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        return GetExceptionCode();
    }

    Irp->IoStatus.Information = sizeof(KERNEL_READ_REQUEST);
    return STATUS_SUCCESS;
}

Qué permite

Lectura Uso típico
Punteros dentro de ntoskrnl.exe Bypass de KASLR
_EPROCESS actual y de SYSTEM Preparar token stealing
Pool chunks Confirmar layout / spray / reclaim
Objetos kernel Reconstruir primitivas de R/W más estables

Pattern matching

Pseudocódigo sospechoso:

src = request->KernelAddress;
dst = request->Data;
len = request->Size;
RtlCopyMemory(dst, src, len);

Assembly típico:

mov     rdx, [rcx]       ; source = KernelAddress controlada
lea     rcx, [rcx+10h]   ; destino = output en SystemBuffer
mov     r8d, [rcx+8]     ; size controlado
call    RtlCopyMemory

Fix conceptual:

  • No aceptar VAs kernel arbitrarias desde user-mode.
  • Limitar lecturas a estructuras propias del driver.
  • Exigir privilegios y validar rangos si el feature es realmente necesario.

14. Kernel virtual arbitrary write

La contraparte de la anterior: el IOCTL recibe una dirección virtual kernel destino y bytes controlados por userland. El driver copia esos bytes a esa VA sin validar ownership ni rango.

typedef struct _KERNEL_WRITE_REQUEST {
    PVOID KernelAddress;   // destino controlado por user-mode
    ULONG Size;
    UCHAR Data[0x100];     // bytes controlados por user-mode
} KERNEL_WRITE_REQUEST, *PKERNEL_WRITE_REQUEST;

static NTSTATUS VulnKernelWrite(PIRP Irp)
{
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    PKERNEL_WRITE_REQUEST request = (PKERNEL_WRITE_REQUEST)Irp->AssociatedIrp.SystemBuffer;
    ULONG inputLength = stack->Parameters.DeviceIoControl.InputBufferLength;

    if (request == nullptr || inputLength < sizeof(KERNEL_WRITE_REQUEST)) {
        return STATUS_INVALID_PARAMETER;
    }

    if (request->Size > sizeof(request->Data)) {
        return STATUS_BUFFER_TOO_SMALL;
    }

    __try {
        // VULNERABLE:
        // Escribe bytes controlados por user-mode en cualquier kernel VA.
        RtlCopyMemory(request->KernelAddress, request->Data, request->Size);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        return GetExceptionCode();
    }

    return STATUS_SUCCESS;
}

Primitiva resultante

write_kernel(address, bytes, size)

Con un leak previo, esto puede transformarse en una primitiva de escalada:

1. Ubicar EPROCESS actual.
2. Ubicar EPROCESS de SYSTEM.
3. Copiar SYSTEM.Token sobre Current.Token.
4. Volver a userland como SYSTEM.

Pattern matching

Buscar copia donde el destino sale del input:

RtlCopyMemory(request->KernelAddress, request->Data, request->Size);

Assembly sospechoso:

mov     rcx, [rbx]       ; destino = KernelAddress controlada
lea     rdx, [rbx+10h]   ; source = bytes del usuario
mov     r8d, [rbx+8]     ; size
call    RtlCopyMemory

Diferencia con write-what-where: WWW suele escribir un qword; este patrón permite escritura arbitraria de longitud variable.


15. Read/write de memoria de procesos con MmCopyVirtualMemory

Otra familia normal: drivers “helper” o “anti-cheat/EDR/debug” que permiten leer/escribir memoria de procesos. El patrón usa PsLookupProcessByProcessId + MmCopyVirtualMemory, pero no valida permisos ni restringe PIDs.

typedef struct _PROCESS_VM_REQUEST {
    HANDLE SourcePid;
    PVOID SourceAddress;
    HANDLE TargetPid;
    PVOID TargetAddress;
    SIZE_T Size;
} PROCESS_VM_REQUEST, *PPROCESS_VM_REQUEST;

static NTSTATUS VulnProcessMemoryCopy(PIRP Irp)
{
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
    PPROCESS_VM_REQUEST request = (PPROCESS_VM_REQUEST)Irp->AssociatedIrp.SystemBuffer;
    ULONG inputLength = stack->Parameters.DeviceIoControl.InputBufferLength;

    if (request == nullptr || inputLength < sizeof(PROCESS_VM_REQUEST)) {
        return STATUS_INVALID_PARAMETER;
    }

    PEPROCESS sourceProcess = nullptr;
    PEPROCESS targetProcess = nullptr;

    NTSTATUS status = PsLookupProcessByProcessId(request->SourcePid, &sourceProcess);
    if (!NT_SUCCESS(status)) {
        return status;
    }

    status = PsLookupProcessByProcessId(request->TargetPid, &targetProcess);
    if (!NT_SUCCESS(status)) {
        ObDereferenceObject(sourceProcess);
        return status;
    }

    SIZE_T bytesCopied = 0;

    // VULNERABLE:
    // Cualquier caller que llegue al IOCTL puede copiar memoria entre procesos.
    // No hay SeDebugPrivilege, ownership check ni restricción de PID.
    status = MmCopyVirtualMemory(
        sourceProcess,
        request->SourceAddress,
        targetProcess,
        request->TargetAddress,
        request->Size,
        KernelMode,
        &bytesCopied
    );

    ObDereferenceObject(targetProcess);
    ObDereferenceObject(sourceProcess);

    return status;
}

Por qué es “acceso a kernel desde userland” indirecto

No siempre te da lectura de kernel VA directa, pero rompe el security model:

Falta de control Consecuencia
No verifica SeDebugPrivilege Usuario normal puede leer procesos ajenos
No restringe PID Puede tocar servicios privilegiados
Usa KernelMode como PreviousMode Reduce checks que existirían desde user-mode
No audita rangos Puede crashear o tocar regiones inesperadas

Pattern matching

Imports clave:

PsLookupProcessByProcessId
MmCopyVirtualMemory
ObDereferenceObject

Pseudocódigo sospechoso:

PsLookupProcessByProcessId(request->Pid, &process);
MmCopyVirtualMemory(processA, src, processB, dst, size, KernelMode, &done);

16. Mapear memoria kernel/física a userland

Más peligroso que copiar bytes: el driver puede devolver a userland un puntero mapeado a páginas físicas o kernel. El proceso caller después lee/escribe con operaciones normales de memoria.

Variante A — ZwMapViewOfSection sobre \Device\PhysicalMemory

typedef struct _MAP_PHYS_REQUEST {
    PHYSICAL_ADDRESS PhysicalAddress;
    SIZE_T Size;
    PVOID UserMappedAddress;
} MAP_PHYS_REQUEST, *PMAP_PHYS_REQUEST;

static NTSTATUS VulnMapPhysicalToUser(PIRP Irp)
{
    PMAP_PHYS_REQUEST request = (PMAP_PHYS_REQUEST)Irp->AssociatedIrp.SystemBuffer;

    UNICODE_STRING sectionName;
    OBJECT_ATTRIBUTES objectAttributes;
    HANDLE sectionHandle = nullptr;
    PVOID baseAddress = nullptr;
    SIZE_T viewSize = request->Size;
    LARGE_INTEGER sectionOffset;

    sectionOffset.QuadPart = request->PhysicalAddress.QuadPart;

    RtlInitUnicodeString(&sectionName, L"\\Device\\PhysicalMemory");
    InitializeObjectAttributes(&objectAttributes, &sectionName, OBJ_KERNEL_HANDLE, nullptr, nullptr);

    NTSTATUS status = ZwOpenSection(&sectionHandle, SECTION_ALL_ACCESS, &objectAttributes);
    if (!NT_SUCCESS(status)) {
        return status;
    }

    // VULNERABLE:
    // Mapea memoria física elegida por user-mode dentro del proceso actual.
    status = ZwMapViewOfSection(
        sectionHandle,
        ZwCurrentProcess(),
        &baseAddress,
        0,
        0,
        &sectionOffset,
        &viewSize,
        ViewShare,
        0,
        PAGE_READWRITE
    );

    ZwClose(sectionHandle);

    if (NT_SUCCESS(status)) {
        request->UserMappedAddress = baseAddress;
        Irp->IoStatus.Information = sizeof(MAP_PHYS_REQUEST);
    }

    return status;
}

Variante B — MDL + MmMapLockedPagesSpecifyCache(UserMode)

typedef struct _MAP_KERNEL_VA_REQUEST {
    PVOID KernelAddress;
    SIZE_T Size;
    PVOID UserAddress;
} MAP_KERNEL_VA_REQUEST, *PMAP_KERNEL_VA_REQUEST;

static NTSTATUS VulnMapKernelVaToUser(PIRP Irp)
{
    PMAP_KERNEL_VA_REQUEST request = (PMAP_KERNEL_VA_REQUEST)Irp->AssociatedIrp.SystemBuffer;

    // VULNERABLE:
    // KernelAddress viene de user-mode.
    PMDL mdl = IoAllocateMdl(request->KernelAddress, (ULONG)request->Size, FALSE, FALSE, nullptr);
    if (mdl == nullptr) {
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    __try {
        MmBuildMdlForNonPagedPool(mdl);

        // VULNERABLE:
        // Expone páginas kernel en el proceso caller como user-mode mapping.
        request->UserAddress = MmMapLockedPagesSpecifyCache(
            mdl,
            UserMode,
            MmCached,
            nullptr,
            FALSE,
            NormalPagePriority
        );
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        IoFreeMdl(mdl);
        return GetExceptionCode();
    }

    Irp->IoStatus.Information = sizeof(MAP_KERNEL_VA_REQUEST);
    return STATUS_SUCCESS;
}

Pattern matching

Imports fuertes:

ZwOpenSection
ZwMapViewOfSection
MmMapLockedPagesSpecifyCache
IoAllocateMdl
MmBuildMdlForNonPagedPool
\Device\PhysicalMemory

Señal roja:

AccessMode = UserMode
Protection = PAGE_READWRITE
Address/Size vienen del IOCTL

17. Virtual-to-Physical helper inseguro

Muchos drivers ofrecen un helper que convierte una VA a PA usando MmGetPhysicalAddress. Por sí solo parece menor, pero combinado con un IOCTL de mapeo físico cierra el circuito.

typedef struct _VA_TO_PA_REQUEST {
    PVOID VirtualAddress;
    PHYSICAL_ADDRESS PhysicalAddress;
} VA_TO_PA_REQUEST, *PVA_TO_PA_REQUEST;

static NTSTATUS VulnVirtualToPhysical(PIRP Irp)
{
    PVA_TO_PA_REQUEST request = (PVA_TO_PA_REQUEST)Irp->AssociatedIrp.SystemBuffer;

    if (request == nullptr) {
        return STATUS_INVALID_PARAMETER;
    }

    // VULNERABLE:
    // Userland puede consultar la PA de VAs kernel arbitrarias si ya conoce la VA.
    request->PhysicalAddress = MmGetPhysicalAddress(request->VirtualAddress);

    Irp->IoStatus.Information = sizeof(VA_TO_PA_REQUEST);
    return STATUS_SUCCESS;
}

Cadena típica:

1. Leak de kernel VA: EPROCESS / token / pool object.
2. IOCTL VirtualToPhysical(VA) -> PA.
3. IOCTL MapPhysical(PA) -> user mapping.
4. Leer/escribir estructura kernel desde userland.

18. I/O ports, PCI config y MSR write expuestos

Otra clase común: wrappers “hardware access” que exponen instrucciones privilegiadas a userland.

I/O ports (in / out)

typedef struct _IO_PORT_REQUEST {
    USHORT Port;
    ULONG Value;
} IO_PORT_REQUEST, *PIO_PORT_REQUEST;

static NTSTATUS VulnWriteIoPort(PIRP Irp)
{
    PIO_PORT_REQUEST request = (PIO_PORT_REQUEST)Irp->AssociatedIrp.SystemBuffer;

    // VULNERABLE:
    // User-mode elige cualquier puerto de I/O.
    WRITE_PORT_ULONG((PULONG)(ULONG_PTR)request->Port, request->Value);

    return STATUS_SUCCESS;
}

PCI config space

typedef struct _PCI_CONFIG_REQUEST {
    ULONG Bus;
    ULONG Slot;
    ULONG Offset;
    ULONG Value;
} PCI_CONFIG_REQUEST, *PPCI_CONFIG_REQUEST;

static NTSTATUS VulnPciConfigWrite(PIRP Irp)
{
    PPCI_CONFIG_REQUEST request = (PPCI_CONFIG_REQUEST)Irp->AssociatedIrp.SystemBuffer;

    PCI_SLOT_NUMBER slot = {};
    slot.u.AsULONG = request->Slot;

    // VULNERABLE:
    // Bus/Slot/Offset/Value controlados por user-mode.
    HalSetBusDataByOffset(
        PCIConfiguration,
        request->Bus,
        slot.u.AsULONG,
        &request->Value,
        request->Offset,
        sizeof(request->Value)
    );

    return STATUS_SUCCESS;
}

MSR write (wrmsr)

typedef struct _MSR_WRITE_REQUEST {
    ULONG MsrIndex;
    ULONGLONG MsrValue;
} MSR_WRITE_REQUEST, *PMSR_WRITE_REQUEST;

static NTSTATUS VulnWriteMsr(PIRP Irp)
{
    PMSR_WRITE_REQUEST request = (PMSR_WRITE_REQUEST)Irp->AssociatedIrp.SystemBuffer;

    // VULNERABLE:
    // Exponer wrmsr arbitrario permite tocar registros críticos del CPU.
    __writemsr(request->MsrIndex, request->MsrValue);

    return STATUS_SUCCESS;
}

Pattern matching

READ_PORT_UCHAR / WRITE_PORT_UCHAR
READ_PORT_ULONG / WRITE_PORT_ULONG
HalGetBusDataByOffset / HalSetBusDataByOffset
__writemsr / wrmsr
__readmsr / rdmsr

19. Pattern matching rápido en IDA / WinDbg

En DriverEntry

Buscar Indica
IoCreateDevice Nombre del device: \\Device\\X
IoCreateSymbolicLink Alias user-mode: \\.\\X
MajorFunction[0] IRP_MJ_CREATE (CreateFile)
MajorFunction[2] IRP_MJ_CLOSE (CloseHandle)
MajorFunction[14] IRP_MJ_DEVICE_CONTROL (DeviceIoControl)

En el dispatcher

Buscar Indica
IoGetCurrentIrpStackLocation(Irp) Inicio del parseo del IRP
Parameters.DeviceIoControl.IoControlCode Switch de IOCTLs
InputBufferLength, OutputBufferLength Tamaños controlados por el caller
Type3InputBuffer METHOD_NEITHER input raw
Irp->UserBuffer METHOD_NEITHER output raw
AssociatedIrp.SystemBuffer METHOD_BUFFERED
MdlAddress + MmGetSystemAddressForMdlSafe Direct I/O

Funciones peligrosas por categoría

Categoría Imports / llamadas
Copias sin límite memcpy, memmove, RtlCopyMemory, RtlMoveMemory, loops rep movsb
Validación user-mode ProbeForRead, ProbeForWrite, MmIsAddressValid
Pool ExAllocatePoolWithTag, ExAllocatePool2, ExFreePoolWithTag
Kernel virtual R/W RtlCopyMemory con una VA del IOCTL como source/destination
Process memory R/W PsLookupProcessByProcessId, MmCopyVirtualMemory
Memoria física / mappings MmMapIoSpace, MmMapIoSpaceEx, ZwMapViewOfSection, MmMapLockedPagesSpecifyCache, MmGetPhysicalAddress, HalTranslateBusAddress
MSR / CPU __readmsr, __writemsr, rdmsr, wrmsr
Hardware access READ_PORT_*, WRITE_PORT_*, HalGetBusDataByOffset, HalSetBusDataByOffset
Privilegios SeSinglePrivilegeCheck, ExGetPreviousMode

WinDbg útil

dt nt!_IRP @rdx
dt nt!_IO_STACK_LOCATION @r14
dt nt!_IO_STACK_LOCATION.Parameters.DeviceIoControl @r14
!pool <addr>
!poolfind <tag> -nonpaged
rdmsr c0000082
!pte <va>
!db <pa>

Decodificar un IOCTL a mano

ULONG DeviceType     = (Ioctl >> 16) & 0xFFFF;
ULONG RequiredAccess = (Ioctl >> 14) & 0x3;
ULONG FunctionCode   = (Ioctl >>  2) & 0xFFF;
ULONG Method         =  Ioctl        & 0x3;

Ejemplo estilo HEVD:

0x222003 & 3 = 3 -> METHOD_NEITHER

20. Checklist de auditoría estática

Superficie del driver

  • ¿Cuál es el DeviceName pasado a IoCreateDevice?
  • ¿Cuál es el symbolic link pasado a IoCreateSymbolicLink?
  • ¿Qué entradas de MajorFunction[] registra?
  • ¿Dónde está IRP_MJ_DEVICE_CONTROL?
  • ¿El dispatcher tiene switch/if-chain sobre IoControlCode?
  • ¿Qué IOCTLs usan METHOD_NEITHER?

Bugs de buffers

  • ¿Hay RtlCopyMemory / memcpy con size controlado por InputBufferLength?
  • ¿El destino tiene tamaño fijo menor que el input posible?
  • ¿Se valida OutputBufferLength antes de escribir output?
  • ¿Se setea IoStatus.Information con un tamaño correcto?
  • ¿Hay structs locales no inicializadas copiadas a user-mode?

Bugs de punteros

  • ¿Hay deref directa de Type3InputBuffer?
  • ¿Hay ProbeForRead / ProbeForWrite dentro de __try/__except?
  • ¿Se usan punteros user-mode dos veces sin capturar copia local?
  • ¿Hay punteros globales compartidos entre IOCTLs?

Bugs de pool / lifecycle

  • ¿ExFreePoolWithTag va seguido de nullificación del puntero?
  • ¿Hay xrefs posteriores al mismo global liberado?
  • ¿Hay call indirect (call [rax], call rcx) sobre objetos del pool?
  • ¿El objeto tiene callback/vtable en offset 0x00?
  • ¿Hay allocations del mismo tamaño/tag que puedan actuar como reclaimer?

APIs críticas

  • ¿__readmsr recibe índice desde user-mode?
  • ¿__writemsr existe en una ruta alcanzable por user-mode?
  • ¿Hay RtlCopyMemory donde source o destination sea una VA recibida por IOCTL?
  • ¿Hay MmCopyVirtualMemory con PIDs/direcciones controladas por user-mode?
  • ¿MmMapIoSpace recibe physical address o size desde input?
  • ¿ZwMapViewOfSection abre \\Device\\PhysicalMemory?
  • ¿MmMapLockedPagesSpecifyCache mapea con UserMode páginas elegidas por el caller?
  • ¿MmGetPhysicalAddress recibe una VA controlada por user-mode?
  • ¿Hay wrappers de READ_PORT_*, WRITE_PORT_* o PCI config accesibles desde IOCTL?
  • ¿Faltan ExGetPreviousMode, SeSinglePrivilegeCheck, range checks o size caps?

Resumen rápido

Bug Código vulnerable característico Primitiva típica
Stack overflow RtlCopyMemory(stackBuf, userBuf, userLen) Control de return address / crash kernel
Pool overflow RtlCopyMemory(poolObj->Data, SystemBuffer, InputLength) Corrupción de pool / objetos vecinos
UAF ExFreePool(global) sin global = NULL + uso posterior Control de callback / type confusion
WWW *(PVOID*)Where = What Kernel arbitrary write
Bad METHOD_NEITHER value = *(ULONG*)Type3InputBuffer Kernel deref / read / DoS
Double fetch Check de request->Size y uso posterior de request->Size TOCTOU de tamaño/punteros
Info leak Struct no inicializada copiada a SystemBuffer Leak de punteros kernel / KASLR bypass
MSR leak __readmsr(userControlledIndex) Leak de LSTAR / base de ntoskrnl
Physical map MmMapIoSpace(userPA, userLen, ...) Lectura/escritura de memoria física
Kernel read RtlCopyMemory(out, userKernelVA, size) Leer kernel VA arbitraria
Kernel write RtlCopyMemory(userKernelVA, data, size) Escribir kernel VA arbitraria
Process R/W MmCopyVirtualMemory(..., KernelMode, ...) Leer/escribir memoria de procesos
User mapping MmMapLockedPagesSpecifyCache(..., UserMode, ...) Mapear páginas kernel/físicas a userland
VA→PA helper MmGetPhysicalAddress(userVA) Traducir VA kernel para explotar physical map
Hardware access WRITE_PORT_*, HalSetBusDataByOffset, __writemsr Acceso privilegiado a CPU/hardware

Apunte basado en los patrones del Módulo 5 de Binary Gecko Academy: WDM, IRPs, IOCTLs, HEVD, METHOD_NEITHER, UAF, pool spraying, MSR leaks y APIs peligrosas de memoria física.