
Driver Reverse & Exploitation
⚠️ Aviso: Este projeto é estritamente educacional e demonstrativo. Não se destina a uso em contexto malicioso. O objetivo é aprender a metodologia de engenharia reversa e as etapas de exploração de um driver do Windows.
Explico aqui a abordagem que segui para resolver o exercício proposto por d1rk(SaadAhla) https://github.com/SaadAhla, que consiste em realizar engenharia reversa e exploração em um driver legítimo, assinado e não presente nas blocklists (HVCI, LOLBIN...). Um programa em C que permite encerrar qualquer processo ativo no sistema por meio deste Kernel-mode Driver está disponível; detalho seu funcionamento um pouco mais abaixo.

📃 Uso: DriverKiller.exe <nome_processo.exe> [-d]
Opção -d: Permite remover o serviço e o Driver do sistema após a exploração.
O modo testsigning deve estar ativado na máquina alvo, pois o certificado do Driver expirou.
Parte 1 - Engenharia reversa:
O exercício fornece um arquivo .sys, nomeado com seu hash SHA-256.
O primeiro passo é abrir esse arquivo com o IDA.
O IDA está disponível gratuitamente. Basta acessar o site da Hex-Rays para gerar uma licença e baixar o software.
Começamos listando a IAT (Import Address Table) do Driver e procurando a chamada para a API que nos interessa: ZwTerminateProcess.
Ao clicar duas vezes em ZwTerminateProcess, o IDA nos redireciona para o código compilado dessa função. Selecionando a entrada e exibindo as cross-references, obtemos a lista das funções do Driver que a chamam.
Observamos que é a função sub_12EF4, no offset 1CE, que usa ZwTerminateProcess. Após um duplo clique, o IDA exibe seu código compilado.
O código decompilado revela as chamadas a ZwOpenProcess (que abre um handle para o processo alvo) e a ZwTerminateProcess (que encerra o processo por meio desse handle).
Consultando a documentação de ZwOpenProcess (https://learn.microsoft.com/fr-fr/windows-hardware/drivers/ddi/ntddk/nf-ntddk-zwopenprocess), verificamos que o parâmetro ClientID corresponde a um ponteiro que indica o PID do processo visado.
Na linha acima, ClientId.UniqueProcess é inicializado com a variável v22. Esta é definida logo acima:
v22 = (void )((_QWORD *)i + 10);
Para entender essa atribuição, é preciso identificar a variável i e o campo +10.
Mais acima nessa função, observamos uma chamada a ZwQuerySystemInformation com o parâmetro SYSTEM_PROCESS_INFORMATION. Entendemos também que i é o iterador sobre as entradas dessa estrutura com a variável v6.
De acordo com a documentação de ZwQuerySystemInformation: (https://learn.microsoft.com/en-us/windows/win32/sysinfo/zwquerysysteminformation), essa função retorna uma matriz contendo uma entrada para cada processo ativo no sistema.
A estrutura SYSTEM_PROCESS_INFORMATION é descrita aqui: https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
KPRIORITY BasePriority;
HANDLE UniqueProcessId;
PVOID Reserved2;
ULONG HandleCount;
ULONG SessionId;
PVOID Reserved3;
SIZE_T PeakVirtualSize;
SIZE_T VirtualSize;
ULONG Reserved4;
SIZE_T PeakWorkingSetSize;
SIZE_T WorkingSetSize;
PVOID Reserved5;
SIZE_T QuotaPagedPoolUsage;
PVOID Reserved6;
SIZE_T QuotaNonPagedPoolUsage;
SIZE_T PagefileUsage;
SIZE_T PeakPagefileUsage;
SIZE_T PrivatePageCount;
LARGE_INTEGER Reserved7[6];
} SYSTEM_PROCESS_INFORMATION;
Lembrete: tamanhos de alguns tipos no Windows x64
typedef struct _UNICODE_STRING {
USHORT Length; -> 2
USHORT MaximumLength; -> + 2 = 4
PWSTR Buffer; -> + 8 = 12 (12 não é múltiplo de 8, portanto padding de 4 adicionado antes de Buffer) = 16
} UNICODE_STRING;Cálculo do offset de UniqueProcessId:
ULONG NextEntryOffset; -> 4
ULONG NumberOfThreads; -> + 4 = 8
BYTE Reserved1[48]; -> + 48 = 56
UNICODE_STRING ImageName; -> + 16 = 72
KPRIORITY BasePriority; -> + 4 = 76 (76 não é múltiplo de 8, portanto padding de 4 adicionado) = 80
HANDLE UniqueProcessId; -> + 8 = 88
O membro UniqueProcessId está, portanto, no offset 0x50 (80 decimal).
Observando a atribuição da nossa variável v22, vemos que i é convertido (cast) em ponteiro QWORD (8 octetos)
v22 = (void )((_QWORD *)i + 10);
v22 corresponde ao endereço de i + 10 * 8 = 80 octetos. Essa variável contém, de fato, o PID recuperado da estrutura SYSTEM_PROCESS_INFORMATION.
Para saber qual PID será passado para ZwTerminateProcess, é necessário analisar a condição que envolve essa atribuição.
Observamos que o nome da imagem do processo é primeiro recuperado:
v9 = (wchar_t )((_QWORD *)i + 8);
v9 = endereço de i + 8 × 8 = 64 octetos. Isso corresponde ao Buffer do membro ImageName, já que esse membro está no offset 56 + 2 (USHORT) + 2 (USHORT) + 4 (padding) = 64
Diante das manipulações e dos loops abaixo, podemos formular a hipótese de que é feita uma comparação entre o nome do processo passado como argumento (a2) e os processos ativos no sistema v9/String.
sub_1C078(String, v9, (int)v13); v17 = strupr(a2); v18 = strupr(String);
É, portanto, o parâmetro a2 que deve conter o nome do processo a ser encerrado via ZwTerminateProcess.
Notamos que a2 é um parâmetro da função sub_12EF4. Para ir mais longe, é preciso examinar as referências dessa função (renomeei-a para ZwTerminateProcessCaller para melhor legibilidade).
Observamos que ZwTerminateProcessCaller é chamada pela função sub_13624 no offset 61A.
Antes de analisar esse código decompilado, vou procurar as referências da função sub_13624 (renomeada ZwTerminateProcessCallerCaller) para garantir que esse código é realmente usado após uma chamada de API DeviceIoControl vinda do UserMode.
Observamos que ZwTerminateProcessCallerCaller é chamada pela função sub_14130 (renomeada ZwTerminateProcessCallerCallerCaller ...felizmente para nós, é a última antes do ponto de entrada 😅).
Observamos que ZwTerminateProcessCallerCallerCaller é chamada pela função sub_1A4A8 no offset 306.
Encontramos a atribuição da função ZwTerminateProcessCallerCallerCaller:
memset64(DriverObject->MajorFunction, (unsigned __int64)ZwTerminateProcessCallerCallerCaller, 0x1Cu);
Antes de voltar à função sub_13624 (alias ZwTerminateProcessCallerCaller), recuperamos o Symbolic Name e o Device Name (idênticos aqui): Viragtlt.
Voltando a ZwTerminateProcessCallerCaller, notamos que seu segundo parâmetro (portanto a2) corresponde a MasterIrp->AssociatedIrp.SystemBuffer.
Logo acima da chamada a ZwTerminateProcessCaller, encontramos o IOCTL code: -2106392528 (em hexadecimal: 0x82730030).
Graças a essas informações, podemos deduzir que, para explorar este Driver, é necessário enviar uma chamada de API DeviceIoControl ao Driver com o nome do processo a ser encerrado no SystemBuffer.
🔷 Informações recuperadas por meio da engenharia reversa:
0x82730030ViragtltViragtltParte 2 - Exploração
Para explorar este Driver (se estiver instalado e ativo na máquina alvo), é necessário abrir um handle para ele e, em seguida, fazer uma chamada de API DeviceIoControl com um Buffer contendo o nome do processo que se deseja encerrar.
Para este exercício, desenvolvi um projeto em C que:
Também adicionei uma opção -d que permite remover o serviço e o Driver do sistema após a exploração.
Este é o comportamento do programa C em seu ciclo de execução completo:
Evasão de AV/EDR
Neste caso, DriverKiller.exe não é detectado pelo Microsoft Defender, nem estaticamente nem dinamicamente. A evasão não faz muito sentido aqui, pois o Driver explorado possui um certificado expirado, tornando difícil considerar seu uso em condições reais. Mas, para maior furtividade, poderíamos ter implementado:
Detecção do driver em 29/08/2025 (resultado já existente; não enviei nada ao VirusTotal por razões óbvias):
⚠️ Este projeto foi realizado em um contexto de aprendizado. Pode conter imprecisões ou erros. Toda sugestão, correção ou discussão é bem-vinda! 😃 Obrigado a d1rk(SaadAhla): https://github.com/SaadAhla !