Estudando IOCTL Criando o driver (Parte 1)
Introdução
Anotações de estudo sobre IOCTL (Input/Output Control): a forma oficial e documentada do Windows para comunicação entre aplicações usermode e drivers kernel. Nesta Parte 1 focamos no driver kernel: criar o device, receber códigos IOCTL e implementar operações de ADD, READ e WRITE de memória.
Código baseado no projeto krnl-ioctl-demo.
O objetivo aqui não é só mostrar o código, mas explicar por que cada parte existe e como adaptar para outros casos.
⚠️ Aviso: Este conteúdo é exclusivamente educacional. Use apenas em ambientes controlados (VMs) e para fins de aprendizado.
O que é IOCTL?
IOCTL permite que um programa usermode envie comandos para um driver kernel através de um código numérico. Cada código representa uma operação: 0x801 = ADD (soma 1), 0x802 = READ (ler memória de processo), 0x803 = WRITE (escrever memória em processo).
Fluxo resumido
Usermode Kernel
| |
| CreateFile("\\\\.\\SimpleDriver") → IRP_MJ_CREATE
| |
| DeviceIoControl(IOCTL_READ, struct) → IRP_MJ_DEVICE_CONTROL
| | MmCopyVirtualMemory (ler processo)
| ← struct com Response preenchido |
| |
| CloseHandle() → IRP_MJ_CLOSE
Códigos IOCTL: Por que precisamos deles?
O usermode envia um número (ex: 0x801) e o driver faz um switch para decidir o que fazer. Sem esse código, o driver não saberia se deve somar, ler ou escrever. A macro CTL_CODE gera um valor único que combina vários campos; o importante é que driver e usermode usem exatamente as mesmas definições.
Parâmetros CTL_CODE
| Parâmetro | Valor | Significado |
|---|---|---|
| DeviceType | FILE_DEVICE_UNKNOWN | Device genérico (não é disco, teclado, etc.) |
| Function | 0x801–0x803 | Código da operação. 0x800–0xFFF = faixa para drivers customizados |
| Method | METHOD_BUFFERED | Kernel copia buffer user ↔ kernel (mais seguro) |
| Access | FILE_ANY_ACCESS | Qualquer nível de acesso |
Como adicionar outro IOCTL? Defina um novo #define com outro Function (ex: 0x804) e um novo case no switch do DeviceControl.
Estruturas compartilhadas: Por que kernel e usermode precisam da mesma struct?
O usermode envia bytes no buffer; o kernel recebe esses mesmos bytes. Se o layout for diferente, o driver interpreta os bytes nos offsets errados: e isso pode causar BSOD (tela azul). Por isso: mesma struct e mesmo packing.
#pragma pack(push, 1): Por que é crítico (e o que acontece se não usar) {: #pragma-pack}
Onde usamos: Nas structs KERNEL_READ_REQUEST e KERNEL_WRITE_REQUEST, tanto no driver quanto no usermode. Sempre rodeadas por #pragma pack(push, 1) e #pragma pack(pop).
O que faz: O compilador C/C++ insere padding (bytes extras) entre os campos das structs para alinhamento. CPUs acessam memória mais rápido quando os dados estão alinhados (ex: um ULONG_PTR de 8 bytes em endereço múltiplo de 8). Sem pack(1), o compilador faz isso automaticamente: e o layout da struct muda.
Exemplo: Em x64, sem pack:
KERNEL_READ_REQUEST (sem pack):
Offset 0: ProcessId (4 bytes)
Offset 4: [4 bytes de PADDING] ← compilador insere
Offset 8: Address (8 bytes)
Offset 16: Response (8 bytes)
Offset 24: Size (8 bytes)
Total: 32 bytes
Com pack(1): Nenhum padding. Campos grudados:
KERNEL_READ_REQUEST (pack 1):
Offset 0: ProcessId (4 bytes)
Offset 4: Address (8 bytes)
Offset 12: Response (8 bytes)
Offset 20: Size (8 bytes)
Total: 28 bytes
O problema: Se o usermode envia 28 bytes (pack 1) e o driver espera 32 bytes (sem pack), ele lê Address no offset 8 em vez de 4. O valor lido é lixo ou parte de outro campo. O driver pode:
- Usar um PID ou endereço inválido →
PsLookupProcessByProcessIdfalha, ou pior - Passar endereços errados para
MmCopyVirtualMemory→ acessa memória inválida - Escrever em endereços incorretos → corrupção de memória do kernel
No kernel, acesso a memória inválida ou corrupção costuma resultar em BSOD. Por isso o layout precisa ser idêntico nos dois lados.
Resumo: #pragma pack(push, 1) força alinhamento de 1 byte (sem padding). Driver e usermode passam a ver os mesmos bytes nos mesmos offsets. Sem isso, o risco de crash no kernel é alto.
#pragma pack(push, 1)
typedef struct _KERNEL_READ_REQUEST {
ULONG ProcessId;
ULONG_PTR Address;
ULONG_PTR Response;
SIZE_T Size;
} KERNEL_READ_REQUEST, * PKERNEL_READ_REQUEST;
typedef struct _KERNEL_WRITE_REQUEST {
ULONG ProcessId;
ULONG_PTR Address;
ULONG_PTR Value;
SIZE_T Size;
} KERNEL_WRITE_REQUEST, * PKERNEL_WRITE_REQUEST;
#pragma pack(pop)
Criando o Driver
Criamos um headers.h no projeto do driver para centralizar defines, structs e declarações. Assim o driver.cpp fica limpo e qualquer alteração (novo IOCTL, nova struct) fica em um só lugar.
1. headers.h (driver)
Por que um header separado? Centraliza defines e declarações. O driver.cpp só inclui headers.h e tem acesso a tudo. Se adicionar um novo IOCTL, muda em um lugar só.
#pragma once Evita que o header seja incluído mais de uma vez (problema de redefinição).
#include <ntddk.h> e <wdm.h> Headers do WDK (Windows Driver Kit). Trazem NTSTATUS, PIRP, PDEVICE_OBJECT, IoCreateDevice, etc.
IOCTLs e nomes do device Os defines precisam ser idênticos ao usermode. O L"..." indica string Unicode (wide string) exigida pelas APIs do kernel.
Declarações extern "C" Essas funções não vêm dos nossos .cpp; elas estão em ntoskrnl.exe. Declaramos para o linker encontrar. O extern "C" evita name mangling do C++ (o nome da função no binário fica exato: IoCreateDriver, PsLookupProcessByProcessId, etc.).
| Declaração | Onde está | Para que serve |
|---|---|---|
IoCreateDriver | ntoskrnl | Usada quando o driver é carregado por manual mapper (DriverObject == NULL). Cria o driver “por baixo dos panos”. |
PsLookupProcessByProcessId | ntoskrnl | Converte PID em ponteiro PEPROCESS. Precisamos disso para acessar a memória de outro processo. |
MmCopyVirtualMemory | ntoskrnl | Não documentada. Copia bytes entre espaços de memória de processos. Usada para Read/Write de memória. |
Structs Mesmo layout do usermode. Response em READ é preenchido pelo driver; o I/O Manager copia a struct de volta.
ReadProcessMemory / WriteProcessMemory São nossas funções auxiliares, implementadas em headers.h. Encapsulam a chamada a MmCopyVirtualMemory com os parâmetros na ordem correta (origem → destino).
#pragma once
#include <ntddk.h>
#include <wdm.h>
#define IOCTL_ADD CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_READ CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_WRITE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define DEVICE_NAME L"\\Device\\SimpleDriver"
#define SYMLINK_NAME L"\\DosDevices\\SimpleDriver"
extern "C" NTKERNELAPI NTSTATUS IoCreateDriver(
_In_opt_ PUNICODE_STRING DriverName,
_In_ PDRIVER_INITIALIZE InitializationFunction);
extern "C" NTKERNELAPI NTSTATUS PsLookupProcessByProcessId(
_In_ HANDLE ProcessId,
_Outptr_ PEPROCESS* Process);
extern "C" NTSTATUS NTAPI MmCopyVirtualMemory(
PEPROCESS SourceProcess,
PVOID SourceAddress,
PEPROCESS TargetProcess,
PVOID TargetAddress,
SIZE_T BufferSize,
KPROCESSOR_MODE PreviousMode,
PSIZE_T ReturnSize);
#pragma pack(push, 1)
typedef struct _KERNEL_READ_REQUEST {
ULONG ProcessId;
ULONG_PTR Address;
ULONG_PTR Response;
SIZE_T Size;
} KERNEL_READ_REQUEST, * PKERNEL_READ_REQUEST;
typedef struct _KERNEL_WRITE_REQUEST {
ULONG ProcessId;
ULONG_PTR Address;
ULONG_PTR Value;
SIZE_T Size;
} KERNEL_WRITE_REQUEST, * PKERNEL_WRITE_REQUEST;
#pragma pack(pop)
NTSTATUS ReadProcessMemory(PEPROCESS Process, PVOID SourceAddress, PVOID TargetAddress, SIZE_T Size)
{
PEPROCESS SourceProcess = Process;
PEPROCESS TargetProcess = PsGetCurrentProcess();
SIZE_T BytesWritten = 0;
NTSTATUS status = MmCopyVirtualMemory(
SourceProcess, SourceAddress,
TargetProcess, TargetAddress,
Size, KernelMode, &BytesWritten);
return status;
}
NTSTATUS WriteProcessMemory(PEPROCESS Process, PVOID SourceAddress, PVOID TargetAddress, SIZE_T Size)
{
PEPROCESS SourceProcess = PsGetCurrentProcess();
PEPROCESS TargetProcess = Process;
SIZE_T BytesWritten = 0;
NTSTATUS status = MmCopyVirtualMemory(
SourceProcess, SourceAddress,
TargetProcess, TargetAddress,
Size, KernelMode, &BytesWritten);
return status;
}
2. CreateClose
O kernel dispara IRP_MJ_CREATE quando o usermode chama CreateFile, e IRP_MJ_CLOSE quando chama CloseHandle. Em muitos drivers simples, não precisamos fazer nada especial: só retornar sucesso. Por isso usamos a mesma função para os dois evita duplicar código.
O que faz cada linha:
Irp->IoStatus.Status = STATUS_SUCCESSIndica que a operação deu certo.Irp->IoStatus.Information = 0Nenhum byte de retorno (não é leitura/escrita).IoCompleteRequestObrigatório. Sinaliza ao I/O Manager que terminamos. Sem isso, o usermode fica esperando para sempre.
NTSTATUS CreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
UNREFERENCED_PARAMETER(DeviceObject);
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
3. DeviceControl IOCTL_ADD (o caso mais simples)
IOCTL_ADD é bom para entender o fluxo: o usermode envia um int, o driver soma 1 e devolve no mesmo buffer. Com METHOD_BUFFERED, o I/O Manager já copiou os bytes para pSystemBuffer. Fazemos um cast para int*, modificamos o valor, e informamos quantos bytes retornar (cbBytesReturned). O I/O Manager copia de volta para o buffer do usermode.
case IOCTL_ADD:
{
if (cbInputBufferLength >= sizeof(int) && cbOutputBufferLength >= sizeof(int) && pSystemBuffer != NULL)
{
int* pValue = (int*)pSystemBuffer;
*pValue = *pValue + 1;
cbBytesReturned = sizeof(int);
status = STATUS_SUCCESS;
DbgPrint("[+] IOCTL_ADD: %d -> %d\n", *pValue - 1, *pValue);
}
else
status = STATUS_BUFFER_TOO_SMALL;
break;
}
4. DeviceControl IOCTL_READ (ler memória de outro processo)
O usermode envia KERNEL_READ_REQUEST com ProcessId, Address e Size. O driver:
- Obtém o
PEPROCESSdo processo alvo comPsLookupProcessByProcessId. - Chama
ReadProcessMemory(que usaMmCopyVirtualMemory) para copiar bytes do processo alvo para o campoResponseda nossa struct. - Chama
ObfDereferenceObject(Process)importante para evitar leak de referência. - Define
cbBytesReturned = sizeof(KERNEL_READ_REQUEST)para o I/O Manager copiar a struct inteira (comResponsepreenchido) de volta.
case IOCTL_READ:
{
if (cbInputBufferLength >= sizeof(KERNEL_READ_REQUEST) && cbOutputBufferLength >= sizeof(KERNEL_READ_REQUEST) && pSystemBuffer != NULL)
{
PKERNEL_READ_REQUEST ReadRequest = (PKERNEL_READ_REQUEST)pSystemBuffer;
PEPROCESS Process = NULL;
status = PsLookupProcessByProcessId((HANDLE)(ULONG_PTR)ReadRequest->ProcessId, &Process);
if (NT_SUCCESS(status))
{
status = ReadProcessMemory(Process,
(PVOID)ReadRequest->Address,
(PVOID)&ReadRequest->Response,
ReadRequest->Size);
ObfDereferenceObject(Process);
if (NT_SUCCESS(status))
{
cbBytesReturned = sizeof(KERNEL_READ_REQUEST);
}
}
}
else
{
status = STATUS_BUFFER_TOO_SMALL;
}
break;
}
5. DeviceControl IOCTL_WRITE (escrever memória em outro processo)
Similar ao READ, mas o fluxo é invertido: copiamos de nosso buffer (onde está Value) para o endereço Address no processo alvo. WriteProcessMemory usa MmCopyVirtualMemory com origem = nosso processo, destino = processo alvo.
Na demo: o usermode envia um endereço qualquer (ex: base + 0xD000) só pra testar o fluxo. Esse endereço pode ser inválido ou readonly; se o WRITE falhar, é esperado.
case IOCTL_WRITE:
{
if (cbInputBufferLength >= sizeof(KERNEL_WRITE_REQUEST) && cbOutputBufferLength >= sizeof(KERNEL_WRITE_REQUEST) && pSystemBuffer != NULL)
{
PKERNEL_WRITE_REQUEST WriteRequest = (PKERNEL_WRITE_REQUEST)pSystemBuffer;
PEPROCESS Process = NULL;
status = PsLookupProcessByProcessId((HANDLE)(ULONG_PTR)WriteRequest->ProcessId, &Process);
if (NT_SUCCESS(status))
{
status = WriteProcessMemory(Process,
(PVOID)&WriteRequest->Value,
(PVOID)WriteRequest->Address,
WriteRequest->Size);
ObfDereferenceObject(Process);
if (NT_SUCCESS(status))
{
cbBytesReturned = sizeof(KERNEL_WRITE_REQUEST);
}
}
}
else
{
status = STATUS_BUFFER_TOO_SMALL;
}
break;
}
6. ReadProcessMemory e WriteProcessMemory Encapsulando MmCopyVirtualMemory
MmCopyVirtualMemory é a API de baixo nível. Ela recebe: processo origem, endereço origem, processo destino, endereço destino, tamanho. Para Read: origem = processo alvo, destino = nosso (driver). Para Write: origem = nosso, destino = processo alvo. Criar essas funções auxiliares deixa o DeviceControl mais legível.
Por que PsGetCurrentProcess()? O driver roda no contexto do kernel; PsGetCurrentProcess() retorna o “processo” do kernel (ou do sistema). Usamos como processo de origem/destino quando os dados estão no nosso lado.
NTSTATUS ReadProcessMemory(PEPROCESS Process, PVOID SourceAddress, PVOID TargetAddress, SIZE_T Size)
{
PEPROCESS SourceProcess = Process;
PEPROCESS TargetProcess = PsGetCurrentProcess();
SIZE_T BytesWritten = 0;
NTSTATUS status = MmCopyVirtualMemory(
SourceProcess, SourceAddress,
TargetProcess, TargetAddress,
Size, KernelMode, &BytesWritten);
return status;
}
NTSTATUS WriteProcessMemory(PEPROCESS Process, PVOID SourceAddress, PVOID TargetAddress, SIZE_T Size)
{
PEPROCESS SourceProcess = PsGetCurrentProcess();
PEPROCESS TargetProcess = Process;
SIZE_T BytesWritten = 0;
NTSTATUS status = MmCopyVirtualMemory(
SourceProcess, SourceAddress,
TargetProcess, TargetAddress,
Size, KernelMode, &BytesWritten);
return status;
}
7. UnloadDriver Limpeza ao descarregar
Quando o driver é descarregado (ex: sc stop ou manual mapper unload), o sistema chama UnloadDriver. Precisamos:
- Remover o symlink primeiro (
IoDeleteSymbolicLink) senão ficam referências ao device. - Remover o device (
IoDeleteDevice). A ordem importa: symlink antes do device.
VOID UnloadDriver(PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING sym;
RtlInitUnicodeString(&sym, SYMLINK_NAME);
IoDeleteSymbolicLink(&sym);
if (DriverObject->DeviceObject != NULL)
{
IoDeleteDevice(DriverObject->DeviceObject);
}
}
8. DriverInitialize Registrar handlers e criar o device
Aqui configuramos o driver:
- MajorFunction Array de ponteiros para funções. Diz ao kernel: “quando chegar IRP_MJ_CREATE, chame CreateClose; quando chegar IRP_MJ_DEVICE_CONTROL, chame DeviceControl”.
- IoCreateDevice Cria o dispositivo
\Device\SimpleDriver. O nome é interno; o usermode não acessa diretamente. - IoCreateSymbolicLink Cria
\DosDevices\SimpleDriverapontando para o device. O usermode abre com\\.\SimpleDriver, que resolve para esse symlink. - DO_BUFFERED_IO Usa buffered I/O (compatível com METHOD_BUFFERED).
- DO_DEVICE_INITIALIZING Removemos essa flag para que o device aceite I/O. Durante a criação, ela impede acesso; depois de configurado, desligamos.
Como criar outro device? Basta alterar DEVICE_NAME e SYMLINK_NAME e usar nomes diferentes (ex: SimpleDriver2).
NTSTATUS DriverInitialize(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->MajorFunction[IRP_MJ_CREATE] = CreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = CreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DeviceControl;
DriverObject->DriverUnload = UnloadDriver;
UNICODE_STRING dev, sym;
PDEVICE_OBJECT pDevice = NULL;
RtlInitUnicodeString(&dev, DEVICE_NAME);
NTSTATUS status = IoCreateDevice(DriverObject, 0, &dev,
FILE_DEVICE_UNKNOWN, 0, TRUE, &pDevice);
if (!NT_SUCCESS(status))
return status;
RtlInitUnicodeString(&sym, SYMLINK_NAME);
status = IoCreateSymbolicLink(&sym, &dev);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(pDevice);
return status;
}
pDevice->Flags |= DO_BUFFERED_IO;
pDevice->Flags &= ~DO_DEVICE_INITIALIZING;
return STATUS_SUCCESS;
}
9. DriverEntry Ponto de entrada e suporte a manual mapping
DriverEntry é chamado quando o driver é carregado. Existem dois cenários:
- Loader tradicional (sc load, SCM) O sistema passa um
DriverObjectválido. ChamamosDriverInitializediretamente. - Manual mapper (kdmapper, etc.) O loader passa
DriverObject == NULL. Nesse caso, usamosIoCreateDriverpara criar o driver internamente; ele chama nossaDriverInitializecom um novoDriverObject.
Por que extern "C"? O loader procura o símbolo DriverEntry pelo nome exato. C++ faz name mangling (ex: DriverEntry vira ?DriverEntry@@...). Com extern "C", o nome fica DriverEntry e o loader encontra.
extern "C"
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
if (!DriverObject)
{
UNICODE_STRING driverName;
RtlInitUnicodeString(&driverName, L"\\Driver\\SimpleDriver");
return IoCreateDriver(&driverName, &DriverInitialize);
}
return DriverInitialize(DriverObject, RegistryPath);
}
DeviceControl Obtendo buffer e parâmetros
No início do DeviceControl, precisamos dos dados enviados pelo usermode:
IoGetCurrentIrpStackLocationCada driver na pilha tem uma “stack location” no IRP. Os parâmetros doDeviceIoControl(InputBufferLength, OutputBufferLength, IoControlCode) ficam lá.Irp->AssociatedIrp.SystemBufferComMETHOD_BUFFERED, o I/O Manager aloca um buffer e copia os dados do usermode para cá. É umPVOID; fazemos cast para a struct ou tipo correto.- Validar tamanhos Sempre checar
cbInputBufferLength >= sizeof(struct)epSystemBuffer != NULLantes de acessar. Buffer pequeno ou nulo pode causar BSOD.
PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation(Irp);
PVOID pSystemBuffer = Irp->AssociatedIrp.SystemBuffer;
ULONG cbInputBufferLength = pIrpStack->Parameters.DeviceIoControl.InputBufferLength;
ULONG cbOutputBufferLength = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;
ULONG IoControlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;
Ao final:
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = cbBytesReturned;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
METHOD_BUFFERED Por que usar?
METHOD_BUFFERED é o mais seguro para começar: o I/O Manager cuida de tudo. O usermode passa um buffer; o kernel copia esse buffer para memória kernel (SystemBuffer). O driver trabalha apenas com a cópia não acessa memória do usermode diretamente. Depois, o I/O Manager copia de volta (até IoStatus.Information bytes) para o buffer de saída do usermode. Se precisar de buffers grandes ou zerocopy, estude METHOD_IN_DIRECT / METHOD_OUT_DIRECT.
Como estender este driver
| Objetivo | O que fazer |
|---|---|
| Novo IOCTL (ex: obter base de DLL) | Adicione #define IOCTL_GET_BASE 0x804, crie struct com PID + nome do módulo, adicione case IOCTL_GET_BASE no switch |
| Ler/escrever em outro processo | Use o PID do processo alvo em KERNEL_READ_REQUEST / KERNEL_WRITE_REQUEST; o fluxo já suporta |
| Validar PID antes de usar | Chame PsLookupProcessByProcessId e verifique NT_SUCCESS(status) antes de qualquer acesso |
| Buffers maiores | Considere METHOD_IN_DIRECT ou METHOD_OUT_DIRECT para evitar cópia dupla |
Código completo do driver (driver.cpp)
Projeto: krnl-ioctl-demo/kernel_mode
#include "headers.h"
NTSTATUS CreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
UNREFERENCED_PARAMETER(DeviceObject);
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
NTSTATUS DeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
UNREFERENCED_PARAMETER(DeviceObject);
PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation(Irp);
PVOID pSystemBuffer = Irp->AssociatedIrp.SystemBuffer;
ULONG cbInputBufferLength = pIrpStack->Parameters.DeviceIoControl.InputBufferLength;
ULONG cbOutputBufferLength = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;
ULONG IoControlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;
NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
ULONG cbBytesReturned = 0;
switch (IoControlCode)
{
case IOCTL_ADD:
{
if (cbInputBufferLength >= sizeof(int) && cbOutputBufferLength >= sizeof(int) && pSystemBuffer != NULL)
{
int* pValue = (int*)pSystemBuffer;
*pValue = *pValue + 1;
cbBytesReturned = sizeof(int);
status = STATUS_SUCCESS;
DbgPrint("[+] IOCTL_ADD: %d -> %d\n", *pValue - 1, *pValue);
}
else
{
status = STATUS_BUFFER_TOO_SMALL;
}
break;
}
case IOCTL_READ:
{
if (cbInputBufferLength >= sizeof(KERNEL_READ_REQUEST) && cbOutputBufferLength >= sizeof(KERNEL_READ_REQUEST) && pSystemBuffer != NULL)
{
PKERNEL_READ_REQUEST ReadRequest = (PKERNEL_READ_REQUEST)pSystemBuffer;
PEPROCESS Process = NULL;
status = PsLookupProcessByProcessId((HANDLE)(ULONG_PTR)ReadRequest->ProcessId, &Process);
if (!NT_SUCCESS(status))
{
DbgPrint("[+] READ: PID %lu not found 0x%08X\n", ReadRequest->ProcessId, status);
break;
}
status = ReadProcessMemory(Process,
(PVOID)ReadRequest->Address,
(PVOID)&ReadRequest->Response,
ReadRequest->Size);
ObfDereferenceObject(Process);
if (NT_SUCCESS(status))
{
cbBytesReturned = sizeof(KERNEL_READ_REQUEST);
DbgPrint("[+] READ: PID %lu addr %p -> %llu bytes\n", ReadRequest->ProcessId, (void*)ReadRequest->Address, (unsigned long long)ReadRequest->Size);
}
else
{
DbgPrint("[+] READ: MmCopyVirtualMemory failed 0x%08X\n", status);
}
}
else
status = STATUS_BUFFER_TOO_SMALL;
break;
}
case IOCTL_WRITE:
{
if (cbInputBufferLength >= sizeof(KERNEL_WRITE_REQUEST) && cbOutputBufferLength >= sizeof(KERNEL_WRITE_REQUEST) && pSystemBuffer != NULL)
{
PKERNEL_WRITE_REQUEST WriteRequest = (PKERNEL_WRITE_REQUEST)pSystemBuffer;
PEPROCESS Process = NULL;
status = PsLookupProcessByProcessId((HANDLE)(ULONG_PTR)WriteRequest->ProcessId, &Process);
if (!NT_SUCCESS(status))
{
DbgPrint("[+] WRITE: PID %lu not found 0x%08X\n", WriteRequest->ProcessId, status);
break;
}
status = WriteProcessMemory(Process,
(PVOID)&WriteRequest->Value,
(PVOID)WriteRequest->Address,
WriteRequest->Size);
ObfDereferenceObject(Process);
if (NT_SUCCESS(status))
{
cbBytesReturned = sizeof(KERNEL_WRITE_REQUEST);
DbgPrint("[+] WRITE: PID %lu addr %p %llu bytes\n", WriteRequest->ProcessId, (void*)WriteRequest->Address, (unsigned long long)WriteRequest->Size);
}
else
{
DbgPrint("[+] WRITE: failed 0x%08X (addr fake/arbitrário na demo, pode falhar)\n", status);
}
}
else
status = STATUS_BUFFER_TOO_SMALL;
break;
}
default:
status = STATUS_INVALID_DEVICE_REQUEST;
break;
}
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = cbBytesReturned;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
VOID UnloadDriver(PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING sym;
RtlInitUnicodeString(&sym, SYMLINK_NAME);
IoDeleteSymbolicLink(&sym);
if (DriverObject->DeviceObject != NULL)
{
IoDeleteDevice(DriverObject->DeviceObject);
}
DbgPrint("[+] UnloadDriver: device removed\n");
}
NTSTATUS DriverInitialize(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->MajorFunction[IRP_MJ_CREATE] = CreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = CreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DeviceControl;
DriverObject->DriverUnload = UnloadDriver;
UNICODE_STRING dev, sym;
PDEVICE_OBJECT pDevice = NULL;
RtlInitUnicodeString(&dev, DEVICE_NAME);
NTSTATUS status = IoCreateDevice(DriverObject, 0, &dev,
FILE_DEVICE_UNKNOWN, 0, TRUE, &pDevice);
if (!NT_SUCCESS(status))
return status;
RtlInitUnicodeString(&sym, SYMLINK_NAME);
status = IoCreateSymbolicLink(&sym, &dev);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(pDevice);
return status;
}
pDevice->Flags |= DO_BUFFERED_IO;
pDevice->Flags &= ~DO_DEVICE_INITIALIZING;
DbgPrint("[+] Driver loaded\n");
return STATUS_SUCCESS;
}
extern "C"
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
if (!DriverObject)
{
UNICODE_STRING driverName;
RtlInitUnicodeString(&driverName, L"\\Driver\\SimpleDriver");
return IoCreateDriver(&driverName, &DriverInitialize);
}
return DriverInitialize(DriverObject, RegistryPath);
}
Próximo post: Estudando IOCTL: Cliente usermode (Parte 2)