Desenvolvendo driver para hooking de funções — Parte 2
Introdução
Este post é a continuação de “Encontrando funções no Windows com WinDbg — Hooking de funções — Parte 1”, onde aprendemos a usar o WinDbg para analisar funções do kernel. Agora, vamos mergulhar no desenvolvimento de um driver kernel que implementa a técnica de Function Hooking.
⚠️ Aviso: Este conteúdo é exclusivamente educacional. Use apenas em ambientes controlados (VMs) e para fins de aprendizado.
O que é Function Hooking?
Um function hook substitui os primeiros bytes de uma função com um jump (salto) para sua própria função. É como colocar um “desvio” no código original.
Exemplo Visual
Antes do Hook:
NtOpenCompositionSurfaceSectionInfo:
xor eax, eax ; Código original
ret ; Retorna
Depois do Hook:
NtOpenCompositionSurfaceSectionInfo:
mov rax, 0x1234567890ABCDEF ; 48 B8 [8 bytes de endereço]
jmp rax ; FF E0
; (resto sobrescrito)
Arquitetura do Projeto
O projeto consiste em dois componentes:
- Driver Kernel (
driver.sys) - Roda em Ring 0 - Aplicação Usermode (
cheat.exe) - Roda em Ring 3
Comunicação Kernel ↔ Usermode
cheat.exe → win32u.dll → Função Hookada → Driver Kernel
↓
Nosso Shellcode
↓
hook_handle()
A comunicação acontece através de uma estrutura compartilhada (NULL_MEMORY) que permite operações como:
- req_base: Obter endereço base de DLLs
- read: Ler memória de processos
- write: Escrever memória em processos
Desenvolvendo o Driver Passo a Passo
Vamos criar o driver seguindo uma ordem lógica de desenvolvimento. Cada arquivo será criado na sequência correta para evitar erros de compilação.
Passo 1: Configuração do Projeto
1.1. Criar Projeto no Visual Studio
- Abra o Visual Studio 2019/2022
- Clique em Create a new project
- Procure por “Kernel Mode Driver, Empty (KMDF)”
- Nome:
KernelCheatYT
1.2. Configurações Iniciais
- Release (ao invés de Debug)
- x64 (plataforma 64 bits)
Propriedades do Projeto:
Configuration Properties → Advanced
├── Character Set → Not Set
Configuration Properties → Driver Install
└── Run InitialCat → No
Configuration Properties → Driver Signing
└── Sign Mode → Off
Configuration Properties → Linker → Advanced
└── Entry Point → DriverEntry
Passo 2: Criando definitions.h
Este é o primeiro arquivo que vamos criar. Ele contém todas as definições, estruturas e declarações de funções não documentadas do Windows.
#pragma once
#ifndef DEFINITIONS_H
#define DEFINITIONS_H
#include <ntdef.h>
#include <ntifs.h>
#include <ntddk.h>
#include <windef.h>
#include <ntstrsafe.h>
#include <wdm.h>
#pragma comment(lib, "ntoskrnl.lib")
// Define apenas se ainda não estiver definido nos headers do sistema
#ifndef _SYSTEM_INFOMATION_CLASS_DEFINED
#define _SYSTEM_INFOMATION_CLASS_DEFINED
typedef enum _SYSTEM_INFOMATION_CLASS
{
SystemBasicInformation,
SystemProcessorInformation,
SystemPerformanceInformation,
SystemTimeOfDayInformation,
SystemPatchInformation,
SystemProcessInformation,
SystemCallCountInformation,
SystemDeviceInformation,
SystemProcessorPerformanceInformation,
SystemFlagsInformation,
SystemCallTimeInformation,
SystemModuleInformation = 0x0B
} SYSTEM_INFORMATION_CLASS,
* PSYSTEM_INFORMATION_CLASS;
#endif
#ifndef _RTL_PROCESS_MODULE_INFORMATION_DEFINED
#define _RTL_PROCESS_MODULE_INFORMATION_DEFINED
typedef struct _RTL_PROCESS_MODULE_INFORMATION
{
HANDLE Section;
PVOID MappedBase;
PVOID ImageBase;
ULONG ImageSize;
ULONG Flags;
USHORT LoadOrderIndex;
USHORT InitOrderIndex;
USHORT LoadCount;
USHORT OffsetToFileName;
UCHAR FullPathName[256];
} RTL_PROCESS_MODULE_INFORMATION,
* PRTL_PROCESS_MODULE_INFORMATION;
#endif
#ifndef _RTL_PROCESS_MODULES_DEFINED
#define _RTL_PROCESS_MODULES_DEFINED
typedef struct _RTL_PROCESS_MODULES
{
ULONG NumberOfModules;
RTL_PROCESS_MODULE_INFORMATION Modules[1];
} RTL_PROCESS_MODULES, * PRTL_PROCESS_MODULES;
#endif
extern "C" __declspec(dllimport)
NTSTATUS NTAPI ZwProtectVirtualMemomry(
HANDLE ProcessHandle,
PVOID* BaseAndress,
PULONG ProtectSize,
ULONG NewProject,
PULONG OldProject
);
extern "C" NTKERNELAPI
PVOID
NTAPI
RtlFindExportedRoutineByName(
_In_ PVOID ImageBase,
_In_ PCCH RoutineName
);
extern "C" NTSTATUS ZwQuerySystemInformation(ULONG InfoClass, PVOID Buffer, ULONG Length, PULONG ReturnLengh);
extern "C" NTKERNELAPI
PPEB
PsGetProcessPeb(
_In_ PEPROCESS Process
);
extern "C" NTSTATUS NTAPI MmCopyVirtualMemory
(
PEPROCESS FromProcess,
PVOID FromAddress,
PEPROCESS ToProcess,
PVOID ToAddress,
SIZE_T BufferSize,
KPROCESSOR_MODE PreviousMode,
PSIZE_T ReturnSize
);
#pragma warning(push)
#pragma warning(disable: 4995) // Ignora warnings de função deprecated
extern "C" NTSTATUS NTAPI ZwQueryVirtualMemory(
HANDLE ProcessHandle,
PVOID BaseAddress,
MEMORY_INFORMATION_CLASS MemoryInformationClass,
PVOID MemoryInformation,
SIZE_T MemoryInformationLength,
PSIZE_T ReturnLength
);
#pragma warning(pop)
// ========================================
// TYPEDEF: PPS_POST_PROCESS_INIT_ROUTINE
// ========================================
typedef VOID(*PPS_POST_PROCESS_INIT_ROUTINE)(VOID);
// ========================================
// STRUCT: RTL_USER_PROCESS_PARAMETERS
// ========================================
#ifndef _RTL_USER_PROCESS_PARAMETERS_DEFINED
#define _RTL_USER_PROCESS_PARAMETERS_DEFINED
typedef struct _RTL_USER_PROCESS_PARAMETERS {
BYTE Reserved1[16];
PVOID Reserved2[10];
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine;
} RTL_USER_PROCESS_PARAMETERS, * PRTL_USER_PROCESS_PARAMETERS;
#endif
// ========================================
// STRUCT: PEB_LDR_DATA
// ========================================
#ifndef _PEB_LDR_DATA_DEFINED
#define _PEB_LDR_DATA_DEFINED
typedef struct _PEB_LDR_DATA {
BYTE Reserved1[8];
PVOID Reserved2[3];
LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, * PPEB_LDR_DATA;
#endif
// ========================================
// STRUCT: PEB (Process Environment Block)
// ========================================
#ifndef _PEB_DEFINED
#define _PEB_DEFINED
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
BYTE Reserved4[104];
PVOID Reserved5[52];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved6[128];
PVOID Reserved7[1];
ULONG SessionId;
} PEB, * PPEB;
#endif
// ========================================
// STRUCT: LDR_DATA_TABLE_ENTRY
// ========================================
#ifndef _LDR_DATA_TABLE_ENTRY_DEFINED
#define _LDR_DATA_TABLE_ENTRY_DEFINED
typedef struct _LDR_DATA_TABLE_ENTRY {
PVOID Reserved1[2];
LIST_ENTRY InMemoryOrderLinks;
PVOID Reserved2[2];
PVOID DllBase;
PVOID Reserved3[2];
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName; // ADICIONADO: Nome base da DLL
BYTE Reserved4[8];
PVOID Reserved5[3];
union {
ULONG CheckSum;
PVOID Reserved6;
};
ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;
#endif
#endif // DEFINITIONS_H
Passo 3: Criando memory.h
#pragma once
#include "definitions.h"
typedef struct _NULL_MEMORY
{
void* buffer_address;
UINT_PTR address;
ULONGLONG size;
ULONG pid;
BOOLEAN write;
BOOLEAN read;
BOOLEAN req_base;
void* output;
const char* module_name;
ULONG64 base_adress;
}NULL_MEMORY;
// Encontra o endereço base de um módulo (driver) do sistema
PVOID get_system_module_base(const char* module_name);
// Encontra uma função exportada dentro de um módulo
PVOID get_system_module_export(const char* module_name, LPCSTR routime_name);
// Escreve dados em um endereço de memória
bool write_memory(void* address, void* buffer, size_t size);
// Escreve dados em memória protegida (read-only) usando MDL
bool write_to_readonly_memory(void* address, void* buffer, size_t size);
// Pega o endereço base de um módulo (DLL) carregado em um processo usermode
ULONG64 get_module_base_x64(PEPROCESS proc, UNICODE_STRING module_name);
// Lê memória de um processo
bool read_kernel_memory(HANDLE pid, UINT_PTR address, void* buffer, SIZE_T size);
// Escreve memória em um processo
bool write_kernel_memory(HANDLE pid, uintptr_t address, void* buffer, SIZE_T size);
Passo 4: Criando memory.cpp
#include "memory.h"
PVOID get_system_module_base(const char* module_name)
{
ULONG bytes = 0;
NTSTATUS status = ZwQuerySystemInformation(SystemModuleInformation, NULL, bytes, &bytes);
if (!bytes)
return NULL;
PRTL_PROCESS_MODULES modules = (PRTL_PROCESS_MODULES)ExAllocatePoolWithTag(NonPagedPool, bytes, 0x6e756c6c);
status = ZwQuerySystemInformation(SystemModuleInformation, modules, bytes, &bytes);
if(!NT_SUCCESS(status))
return NULL;
PRTL_PROCESS_MODULE_INFORMATION module = modules->Modules;
PVOID module_base = 0, module_size = 0;
for (ULONG i = 0; i < modules->NumberOfModules; i++)
{
if (_stricmp((char*)module[i].FullPathName, module_name) == NULL)
{
module_base = module[i].ImageBase;
module_size = (PVOID)module[i].ImageSize;
break;
}
}
if (modules)
ExFreePoolWithTag(modules, NULL);
if(module_base <= NULL)
return NULL;
return module_base;
}
PVOID get_system_module_export(const char* module_name, LPCSTR routime_name)
{
PVOID lpModule = get_system_module_base(module_name);
if (lpModule <= NULL)
return NULL;
return RtlFindExportedRoutineByName(lpModule, routime_name);
}
bool write_memory(void* address, void* buffer, size_t size)
{
if (!RtlCopyMemory(address, buffer, size))
{
return false;
}
else
{
return true;
}
}
bool write_to_readonly_memory(void* address, void* buffer, size_t size)
{
PMDL Mdl = IoAllocateMdl(address, size, FALSE, FALSE, NULL);
if(!Mdl)
return false;
MmProbeAndLockPages(Mdl, KernelMode, IoReadAccess);
PVOID Mapping = MmMapLockedPagesSpecifyCache(Mdl, KernelMode, MmNonCached, NULL, FALSE, NormalPagePriority);
MmProtectMdlSystemAddress(Mdl, PAGE_EXECUTE_READWRITE);
write_memory(Mapping, buffer, size);
MmUnmapLockedPages(Mapping, Mdl);
MmUnlockPages(Mdl);
IoFreeMdl(Mdl);
return true;
}
ULONG64 get_module_base_x64(PEPROCESS proc, UNICODE_STRING module_name)
{
PPEB pPeb = PsGetProcessPeb(proc);
if (!pPeb)
return NULL;
KAPC_STATE state;
KeStackAttachProcess(proc, &state);
PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)pPeb->Ldr;
if(!pLdr)
{
KeUnstackDetachProcess(&state);
return NULL;
}
// Iterar pela lista de módulos carregados no processo
for (PLIST_ENTRY list = (PLIST_ENTRY)pLdr->InMemoryOrderModuleList.Flink;
list != &pLdr->InMemoryOrderModuleList;
list = (PLIST_ENTRY)list->Flink)
{
// Pegar a entrada da tabela LDR a partir do link
PLDR_DATA_TABLE_ENTRY pEntry = CONTAINING_RECORD(list, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
// Comparar o nome da DLL
if (RtlCompareUnicodeString(&pEntry->BaseDllName, &module_name, TRUE) == 0)
{
ULONG64 base_address = (ULONG64)pEntry->DllBase;
KeUnstackDetachProcess(&state);
return base_address;
}
}
KeUnstackDetachProcess(&state);
return NULL;
}
bool read_kernel_memory(HANDLE pid, UINT_PTR address, void* buffer, SIZE_T size)
{
DbgPrint("[MEMORY] === LEITURA DE MEMORIA ===\n");
DbgPrint("[MEMORY] PID: %d | Endereco: 0x%llx | Tamanho: %d bytes\n", pid, address, size);
if (!address || !buffer || !size)
{
DbgPrint("[MEMORY] ERRO: Parametros invalidos\n");
return false;
}
SIZE_T bytes = 0;
NTSTATUS status = STATUS_SUCCESS;
PEPROCESS process;
DbgPrint("[MEMORY] Procurando processo PID: %d\n", pid);
status = PsLookupProcessByProcessId((HANDLE)pid, &process);
if (!NT_SUCCESS(status))
{
DbgPrint("[MEMORY] ERRO: Processo nao encontrado: 0x%X\n", status);
return false;
}
DbgPrint("[MEMORY] Processo encontrado! Executando MmCopyVirtualMemory...\n");
status = MmCopyVirtualMemory(process, (void*)address, (PEPROCESS)PsGetCurrentProcess(), (void*)buffer, size, KernelMode, &bytes);
if (!NT_SUCCESS(status))
{
DbgPrint("[MEMORY] ERRO: Falha na leitura: 0x%X | Bytes lidos: %d\n", status, bytes);
return false;
}
else
{
DbgPrint("[MEMORY] SUCESSO: Leitura concluida! Bytes lidos: %d\n", bytes);
return true;
}
}
bool write_kernel_memory(HANDLE pid, uintptr_t address, void* buffer, SIZE_T size)
{
DbgPrint("[MEMORY] === ESCRITA DE MEMORIA ===\n");
DbgPrint("[MEMORY] PID: %d | Endereco: 0x%llx | Tamanho: %d bytes\n", pid, address, size);
if (!address || !buffer || !size)
{
DbgPrint("[MEMORY] ERRO: Parametros invalidos\n");
return false;
}
SIZE_T bytes = 0;
NTSTATUS status = STATUS_SUCCESS;
PEPROCESS process;
DbgPrint("[MEMORY] Procurando processo PID: %d\n", pid);
status = PsLookupProcessByProcessId((HANDLE)pid, &process);
if (!NT_SUCCESS(status))
{
DbgPrint("[MEMORY] ERRO: Processo nao encontrado: 0x%X\n", status);
return false;
}
DbgPrint("[MEMORY] Processo encontrado! Anexando ao contexto do processo...\n");
KAPC_STATE state;
KeStackAttachProcess((PEPROCESS)process, &state);
MEMORY_BASIC_INFORMATION info;
DbgPrint("[MEMORY] Verificando informacoes de memoria no endereco: 0x%llx\n", address);
status = ZwQueryVirtualMemory(ZwCurrentProcess(), (PVOID)address, MemoryBasicInformation, &info, sizeof(info), NULL);
if (!NT_SUCCESS(status))
{
DbgPrint("[MEMORY] ERRO: Falha ao obter informacoes de memoria: 0x%X\n", status);
KeUnstackDetachProcess(&state);
return false;
}
DbgPrint("[MEMORY] Regiao de memoria: Base=0x%llx | Tamanho=%d | Protecao=0x%X\n",
info.BaseAddress, info.RegionSize, info.Protect);
if (((uintptr_t)info.BaseAddress + info.RegionSize) < (address + size))
{
DbgPrint("[MEMORY] ERRO: Regiao insuficiente para escrita\n");
KeUnstackDetachProcess(&state);
return false;
}
if(!(info.State & MEM_COMMIT) || (info.Protect & (PAGE_GUARD | PAGE_NOACCESS)))
{
DbgPrint("[MEMORY] ERRO: Regiao nao commitada ou sem acesso\n");
KeUnstackDetachProcess(&state);
return false;
}
if ((info.Protect & PAGE_EXECUTE_READWRITE) || (info.Protect & PAGE_EXECUTE_WRITECOPY)
|| (info.Protect & PAGE_READWRITE) || (info.Protect & PAGE_WRITECOPY))
{
DbgPrint("[MEMORY] Protecao valida! Executando RtlCopyMemory...\n");
RtlCopyMemory((void*)address, buffer, size);
DbgPrint("[MEMORY] SUCESSO: Escrita concluida!\n");
KeUnstackDetachProcess(&state);
return true;
}
else
{
DbgPrint("[MEMORY] ERRO: Protecao de memoria nao permite escrita: 0x%X\n", info.Protect);
}
KeUnstackDetachProcess(&state);
return false;
}
Passo 5: Criando hook.h
#pragma once
#include "definitions.h"
#include "memory.h"
namespace nullhook
{
bool call_kernel_function(void* kernel_function_address);
NTSTATUS hook_handle(PVOID called_param);
}
Passo 6: Criando hook.cpp
#include "hook.h"
bool nullhook::call_kernel_function(void* kernel_function_address)
{
if (!kernel_function_address)
{
return false;
}
PVOID* function = reinterpret_cast<PVOID*>(get_system_module_export("\\SystemRoot\\System32\\drivers\\dxgkrnl.sys",
"NtOpenCompositionSurfaceSectionInfo"));
if (!function)
{
return false;
}
BYTE orig[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
// isso é um jump e isso é um rax
BYTE shell_code[] = { 0x48, 0xB8 };
// isso e um jum rax -> ele vai pular para nossa function
BYTE shell_code_end[] = { 0xFF, 0xE0 };
RtlSecureZeroMemory(orig, sizeof(orig));
memcpy((PVOID)((ULONG_PTR)orig), &shell_code, sizeof(shell_code));
uintptr_t hook_address = reinterpret_cast<uintptr_t>(kernel_function_address);
memcpy((PVOID)((ULONG_PTR)orig + sizeof(shell_code)), &hook_address, sizeof(void*));
memcpy((PVOID)((ULONG_PTR)orig + sizeof(shell_code) + sizeof(void*)), &shell_code_end, sizeof(shell_code_end));
bool result = write_to_readonly_memory(function, &orig, sizeof(orig));
return result;
}
NTSTATUS nullhook::hook_handle(PVOID called_param)
{
// Validar ponteiro
if (!called_param)
{
return STATUS_INVALID_PARAMETER;
}
NULL_MEMORY* instructions = (NULL_MEMORY*)called_param;
if (instructions->req_base == TRUE)
{
ANSI_STRING AS;
UNICODE_STRING ModuleName;
RtlInitAnsiString(&AS, instructions->module_name);
NTSTATUS status = RtlAnsiStringToUnicodeString(&ModuleName, &AS, TRUE);
if (!NT_SUCCESS(status))
{
return status;
}
PEPROCESS process = NULL;
status = PsLookupProcessByProcessId((HANDLE)instructions->pid, &process);
if (!NT_SUCCESS(status) || !process)
{
RtlFreeUnicodeString(&ModuleName);
return status;
}
ULONG64 base_address64 = 0;
base_address64 = get_module_base_x64(process, ModuleName);
instructions->base_adress = base_address64;
ObDereferenceObject(process);
RtlFreeUnicodeString(&ModuleName);
return STATUS_SUCCESS;
}
if (instructions->write == TRUE)
{
instructions->pid, instructions->address, instructions->size);
if (instructions->address < 0x7FFFFFFFFFFF && instructions->address > 0)
{
PVOID kernelBuff = ExAllocatePool(NonPagedPool, instructions->size);
if (!kernelBuff)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
if (!memcpy(kernelBuff, instructions->buffer_address, instructions->size))
{
return STATUS_UNSUCCESSFUL;
}
PEPROCESS process;
PsLookupProcessByProcessId((HANDLE)instructions->pid, &process);
bool write_result = write_kernel_memory((HANDLE)instructions->pid, instructions->address, kernelBuff, instructions->size);
ExFreePool(kernelBuff);
}
else
{
DbgPrint("[HOOK] ERRO: Endereco invalido: 0x%llx\n", instructions->address);
}
}
if(instructions->read == TRUE)
{
instructions->pid, instructions->address, instructions->size);
if (instructions->address < 0x7FFFFFFFFFFF && instructions->address > 0)
{
bool read_result = read_kernel_memory((HANDLE)instructions->pid, instructions->address, instructions->output, instructions->size);
}
else
{
DbgPrint("[HOOK] ERRO: Endereco invalido: 0x%llx\n", instructions->address);
}
}
return STATUS_SUCCESS;
}
Passo 7: Criando main.cpp
#include "hook.h"
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING reg_path)
{
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(reg_path);
bool hook_result = nullhook::call_kernel_function(&nullhook::hook_handle);
return STATUS_SUCCESS;
}
### Passo 8: Compilando o Projeto
**Se der erro de compilação:**
1. Botão direito no projeto → **Properties**
2. **C/C++** → **General**
3. **Treat Warnings As Errors** → **No (/WX-)**
**Compilar:**
1. **Build** → **Build Solution** (Ctrl+Shift+B)
2. O driver será gerado em: `x64\Release\KernelCheatYT\KernelCheatYT.sys`
## Desafios e Soluções (Lições Aprendidas)
Durante o desenvolvimento, enfrentei alguns desafios importantes. Compartilho aqui as soluções que encontrei:
### 1. Validação e Error Handling
Aprendi da forma difícil: **sempre validar ponteiros** e usar `__try/__except` para código perigoso. Um erro no kernel pode causar uma tela azul (BSOD)!
```cpp
// Sempre validar PEPROCESS e liberar com ObDereferenceObject!
PEPROCESS process = NULL;
NTSTATUS status = PsLookupProcessByProcessId((HANDLE)pid, &process);
if (NT_SUCCESS(status) && process)
{
// Usar o process...
ObDereferenceObject(process); // CRÍTICO: Evita memory leak!
}
// Usar __try/__except para código perigoso
__try
{
// Código que pode crashar
PPEB pPeb = PsGetProcessPeb(proc);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
// Tratar o erro sem crashar o sistema
return 0;
}
// Sempre validar ponteiros antes de usar!
if (!MmIsAddressValid(pPeb))
{
KeUnstackDetachProcess(&state);
return 0;
}
2. KernelCallbackTable
Descobri que o usermode travava ao chamar o hook. A solução foi carregar user32.dll ANTES de qualquer coisa no main():
int main()
{
// CRÍTICO: Sem isso, o programa trava!
// Deve ser a PRIMEIRA coisa no main()
LoadLibraryA("user32.dll");
// ... resto do código
}
Por que é necessário?
O user32.dll inicializa a KernelCallbackTable no PEB (Process Environment Block). Esta tabela é essencial para a comunicação entre usermode e kernel através de callbacks. Sem ela, quando nosso programa tenta chamar a função hookada, o sistema não consegue fazer a transição para o kernel mode, causando travamento.
IMPORTANTE: Deve ser carregado no início do main(), antes de qualquer outra operação que possa usar callbacks do kernel.
3. Prevenir Loops Infinitos
Em loops que interagem com o kernel, é crucial adicionar um contador para evitar travamentos:
ULONG max_iterations = 500;
ULONG current_iteration = 0;
for (...; ...; current_iteration++)
{
if (current_iteration >= max_iterations)
break; // Prevenir loop infinito!
}
4. Debug com DbgPrint
Para entender o que está acontecendo no kernel, DbgPrint é seu melhor amigo. As mensagens aparecem no WinDbg:
DbgPrint("[HOOK] req_base request for: %s (PID: %d)\n",
module_name, pid);
DbgPrint("[HOOK] Returning base: 0x%llx\n", base_address);
Ambiente de Desenvolvimento
Ferramentas Utilizadas
- Visual Studio 2022 com WDK (Windows Driver Kit)
- WinDbg para debugging kernel
- kdmapper para injetar o driver
- VirtualBox para testes em VM
Setup do Ambiente
-
Conectar WinDbg ao kernel da VM
- Compilar o driver:
Build → Build Solution (Ctrl+Shift+B) - Injetar com kdmapper:
kdmapper.exe driver.sys
kdmapper: https://github.com/TheCruZ/kdmapper
Testando o Hook
1. Verificar no WinDbg
Após injetar o driver com kdmapper, você pode verificar o hook no WinDbg:
kd> .reload /f dxgkrnl.sys
view -> disassembly
Procure pela sua função: NtOpenCompositionSurfaceSectionInfo
Se você ver mov rax + jmp rax, o hook está ativo!
2. Testar Comunicação Usermode ↔ Kernel
Execute a aplicação usermode (cheat.exe) na VM. Você deverá ver uma saída similar a esta:
[*] Loading user32.dll...
[+] user32.dll loaded!
=== TESTE COM NOTEPAD ===
[+] Found notepad.exe with PID: 12345
[DEBUG] Requesting base for: ntdll.dll (PID: 12345)
[DEBUG] Calling hook at: 0x7FFB...
[DEBUG] Hook returned: 0x0
[DEBUG] Returned base: 0x7ffb95870000
[+] ntdll.dll base address: 0x7ffb95870000
✅ Se aparecer endereços hexadecimais reais: ESTÁ FUNCIONANDO!
Resumo do Fluxo Completo
┌─────────────────────────────────────────────────────────────────┐
│ APLICAÇÃO USERMODE │
│ (user_mode.exe) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. LoadLibraryA("user32.dll"); ← CRÍTICO! │
│ └─> Inicializa KernelCallbackTable │
│ │
│ 2. process_id = get_process_id("cs2.exe"); │
│ └─> Encontra PID do processo alvo │
│ │
│ 3. base = get_module_base_address("client.dll"); │
│ │ │
│ ├─> Cria struct NULL_MEMORY │
│ │ └─> pid = process_id │
│ │ └─> req_base = TRUE │
│ │ └─> module_name = "client.dll" │
│ │ │
│ ├─> call_hook(&instructions); │
│ │ │ │
│ │ ├─> LoadLibraryA("win32u.dll") │
│ │ ├─> GetProcAddress(hWin32u, "NtOpen...") │
│ │ └─> func(&instructions); ← Chama função hookada! │
│ │ │ │
└─────┼───────┼───────────────────────────────────────────────────┘
│ │
│ ↓
┌─────┼───────────────────────────────────────────────────────────┐
│ │ WIN32U.DLL (usermode) │
│ │ │
│ └─────> NtOpenCompositionSurfaceSectionInfo() │
│ │ │
│ │ (syscall para kernel) │
└───────────────────────┼─────────────────────────────────────────┘
│
↓
┌───────────────────────┼────────────────────────────────────────────┐
│ │ KERNEL MODE │
│ │ │
│ ┌───────────────▼─────────────────┐ │
│ │ dxgkrnl!NtOpen... (hookado) │ │
│ ├─────────────────────────────────┤ │
│ │ 48 B8 [addr] ; mov rax, addr │ ← NOSSO SHELLCODE! │
│ │ FF E0 ; jmp rax │ │
│ └───────────────┬─────────────────┘ │
│ │ │
│ ↓ │
│ ┌───────────────────────────────────────┐ │
│ │ hook_handle(instructions) │ │
│ ├───────────────────────────────────────┤ │
│ │ │ │
│ │ if (req_base == TRUE) │ │
│ │ { │ │
│ │ 1. PsLookupProcessByProcessId() │ │
│ │ 2. get_module_base_x64() │ │
│ │ └─> Itera PEB/LDR do proc │ │
│ │ └─> Compara nome das DLLs │ │
│ │ 3. instructions->base_adress = X │ │
│ │ 4. ObDereferenceObject(process) │ │
│ │ } │ │
│ │ │ │
│ │ return STATUS_SUCCESS; │ │
│ └───────────────┬───────────────────────┘ │
│ │ │
└───────────────────────┼────────────────────────────────────────────┘
│
↓ (retorna para usermode)
┌───────────────────────┼────────────────────────────────────────────┐
│ │ APLICAÇÃO USERMODE │
│ ▼ │
│ │
│ 4. base = instructions.base_adress; ← RECEBE O RESULTADO! │
│ └─> 0x7FF612340000 │
│ │
│ 5. int hp = Read<int>(base + 0x1234); ← Usar o endereço! │
│ │
└────────────────────────────────────────────────────────────────────┘
Conceitos Aprendidos
1. Assembly x64
| Opcode | Instrução | Descrição |
|---|---|---|
48 B8 | MOV RAX, imm64 | Move valor para RAX |
FF E0 | JMP RAX | Pula para endereço em RAX |
33 C0 | XOR EAX, EAX | Zera EAX |
C3 | RET | Retorna da função |
2. MDL (Memory Descriptor List)
Estrutura que descreve páginas de memória física:
MDL → Páginas Físicas → Mapeamento Virtual → Escrita
3. Função Escolhida: NtOpenCompositionSurfaceSectionInfo
Escolhi esta função porque:
- Está no
dxgkrnl.sys(DirectX Graphics Kernel) - Raramente é chamada (menos chance de crash)
- É exportada (fácil de encontrar)
- Boa para aprendizado
Outras opções:
NtQueryCompositionSurfaceHDRMetaDataNtOpenCompositionSurfaceSectionInfoNtOpenCompositionSurfaceDirtyRegion
Evite:
- Funções com “SecureCookie” (causam BSOD)
- Funções em regiões críticas
Conclusão
Este projeto demonstra conceitos avançados de programação em kernel mode:
- Function Hooking - Modificação de código em tempo de execução
- Comunicação Kernel ↔ Usermode - Através de funções hookadas
- Manipulação de Memória Protegida - Usando MDL
- Shellcode em x64 - Assembly de baixo nível
- Arquitetura de Drivers Windows - WDM/KMDF
- Processos e DLLs - PEB, LDR, module enumeration
- espero voltar daqui uns anos e ver como eu tava no inicio de tudo.
Principais lições:
- Sempre trabalhe em VM - bsod é seu pior inimigo!
- Valide tudo - Um ponteiro inválido = BSOD
- Use WinDbg - É sua melhor ferramenta
- Estude assembly - Entender os opcodes é fundamental (nao sei quase nada preciso estudar mt)
Próximo post: Desenvolvendo cliente usermode para comunicação com driver — Parte 3