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¶
- Cómo leer estos ejemplos
- Base mínima de un driver WDM vulnerable
- IOCTLs y métodos de buffering
- Stack overflow vía
METHOD_NEITHER - Pool overflow vía
METHOD_BUFFERED - Use-After-Free con global dangling pointer
- Write-What-Where / arbitrary write
- Deref insegura de punteros user-mode
- Double fetch / TOCTOU en
METHOD_NEITHER - Information leak por memoria no inicializada
- Leak de MSR:
__readmsrcontrolado por usuario - Mapeo inseguro de memoria física
- Kernel virtual arbitrary read
- Kernel virtual arbitrary write
- Read/write de memoria de procesos con
MmCopyVirtualMemory - Mapear memoria kernel/física a userland
- Virtual-to-Physical helper inseguro
- I/O ports, PCI config y MSR write expuestos
- Pattern matching rápido en IDA / WinDbg
- 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:
En assembly x64, el DRIVER_OBJECT suele estar en RCX al entrar a DriverEntry, y la tabla MajorFunction[] empieza alrededor de +0x70:
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.LengthoParameters.Create.*en un dispatcher de IOCTLs, aplicar Select union field y elegirParameters.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:
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:
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:
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:
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:
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
ProbeForWritedentro de__try/__excepty 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:
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:
o:
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:
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:
En assembly:
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¶
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:
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:
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(§ionName, L"\\Device\\PhysicalMemory");
InitializeObjectAttributes(&objectAttributes, §ionName, OBJ_KERNEL_HANDLE, nullptr, nullptr);
NTSTATUS status = ZwOpenSection(§ionHandle, 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,
§ionOffset,
&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:
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:
20. Checklist de auditoría estática¶
Superficie del driver¶
- ¿Cuál es el
DeviceNamepasado aIoCreateDevice? - ¿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/memcpycon size controlado porInputBufferLength? - ¿El destino tiene tamaño fijo menor que el input posible?
- ¿Se valida
OutputBufferLengthantes de escribir output? - ¿Se setea
IoStatus.Informationcon un tamaño correcto? - ¿Hay structs locales no inicializadas copiadas a user-mode?
Bugs de punteros¶
- ¿Hay deref directa de
Type3InputBuffer? - ¿Hay
ProbeForRead/ProbeForWritedentro de__try/__except? - ¿Se usan punteros user-mode dos veces sin capturar copia local?
- ¿Hay punteros globales compartidos entre IOCTLs?
Bugs de pool / lifecycle¶
- ¿
ExFreePoolWithTagva 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¶
- ¿
__readmsrrecibe índice desde user-mode? - ¿
__writemsrexiste en una ruta alcanzable por user-mode? - ¿Hay
RtlCopyMemorydonde source o destination sea una VA recibida por IOCTL? - ¿Hay
MmCopyVirtualMemorycon PIDs/direcciones controladas por user-mode? - ¿
MmMapIoSpacerecibe physical address o size desde input? - ¿
ZwMapViewOfSectionabre\\Device\\PhysicalMemory? - ¿
MmMapLockedPagesSpecifyCachemapea conUserModepáginas elegidas por el caller? - ¿
MmGetPhysicalAddressrecibe 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.