文章总结: 本文深入解析WindowsInlineHook技术,对比x86与x64架构实现差异。x86利用5字节相对跳转实现Hook,x64则需14字节绝对跳转,易引发指令截断崩溃。文章提供完整代码演示劫持与Trampoline回溯机制,并提出使用LDE引擎或中转跳板解决截断问题,极具参考价值。 综合评分: 96 文章分类: 二进制安全,逆向分析,实战经验
精选10:Windows 安全 | Inline Hook 实战:从 x86 到 x64
八九 八九
希水涵精选录
2026年1月24日 20:19 山东
「希水涵精选录」是 ABC_123 运营的第2个公众号,主要转载经过筛选的优秀技术文章,欢迎大家积极投稿!
Part1 Hook 基础
Hook 技术主要分为两大类:
1. IAT Hook (基于 PE 结构):修改导入表,针对性强,但容易被检测。
2. Inline Hook (基于机器码修改):直接修改内存中函数的前几行指令,跳转到我们的函数。
Inline Hook 的核心三步曲:
1. 劫持 (Hook):修改原函数开头,强行把执行流引到我们的地盘。
2. 执行 (Payload):在我们的函数中做坏事(记录日志、篡改参数、拦截执行)。
3. 回溯 (Trampoline):为了保证程序不崩溃,我们通过“跳板”执行原函数被覆盖的指令,然后跳回原函数继续执行。
Part2 x86 Inline Hook
- ### 为什么32位Hook很简单?(Hotpatch 机制)
在 Windows XP/Win7 (32位) 时代,微软为了方便打补丁,在很多 API (如 MessageBoxA) 开头预留了 5 个字节 的标准序言。
标准开头机器码:
8B FF MOV EDI, EDI ; 2字节 (无意义,仅占位)55 PUSH EBP ; 1字节8B EC MOV EBP, ESP ; 2字节
总长度正好 5 字节。32 位的相对跳转指令 JMP (0xE9) 也正好是 5 字节。我们可以完美覆盖这 5 字节,不会截断任何指令。
在汇编中,jmp指令后跟着绝对地址,但这是编译器优化后的结果。真实机器码后跟着相对偏移量,而非绝对地址 $Offset = 目标地址 – 当前地址 – 5$
#include <windows.h>#include <iostream>
// ==========================================// 1. 定义函数指针与全局变量// ==========================================
// 定义 MessageBoxA 的函数原型// WINAPI (即 __stdcall) 在 32 位下非常重要,用来处理堆栈平衡typedef int (WINAPI* PMessageBoxA)(HWND, LPCSTR, LPCSTR, UINT);
// 保存原函数地址void* g_pOrgMsgBox = NULL;
// 保存跳板的内存地址 (使用 VirtualAlloc 申请)unsigned char* g_pTrampoline = NULL;
// ==========================================// 2. 我们的 Hook 函数 (Payload)// ==========================================int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) { std::cout << "[!] [HOOK] 拦截成功!正在修改弹窗内容..." << std::endl;
// 篡改参数 LPCSTR newText = "这是被 32位 Hook 篡改的内容!"; LPCSTR newCaption = "警告 (Hooked)";
// 调用跳板,执行原函数逻辑 // 必须强转为 PMessageBoxA 才能正确传递参数 PMessageBoxA pOriginal = (PMessageBoxA)g_pTrampoline; return pOriginal(hWnd, newText, newCaption, uType);}
// ==========================================// 3. 安装 Hook (32位经典逻辑)// ==========================================void InstallHook() { HMODULE hUser32 = LoadLibraryA("user32.dll"); g_pOrgMsgBox = (void*)GetProcAddress(hUser32, "MessageBoxA");
if (!g_pOrgMsgBox) { std::cout << "[-] 无法找到 MessageBoxA" << std::endl; return; }
std::cout << "[*] MessageBoxA 地址: 0x" << g_pOrgMsgBox << std::endl;
// --- 步骤 1: 准备跳板 (VirtualAlloc) ---
// 申请 4KB 内存,权限为 可读可写可执行 (PAGE_EXECUTE_READWRITE) g_pTrampoline = (unsigned char*)VirtualAlloc(NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!g_pTrampoline) { std::cout << "[-] 内存申请失败" << std::endl; return; } std::cout << "[+] 跳板内存申请成功: 0x" << (void*)g_pTrampoline << std::endl;
// --- 步骤 2: 填充跳板内容 ---
// 2.1 【备份】把原函数的前 5 字节复制到跳板开头 memcpy(g_pTrampoline, g_pOrgMsgBox, 5);
// 2.2 【回跳】在跳板第 5 字节处,写入 JMP 跳回原函数 +5 的位置 // 计算回跳偏移:(原函数地址 + 5) - (跳板地址 + 5) - 5 // 解释:目标是 (g_pOrgMsgBox + 5),当前指令在 (g_pTrampoline + 5) DWORD srcAddrInTramp = (DWORD)g_pTrampoline + 5; DWORD targetAddrInOrg = (DWORD)g_pOrgMsgBox + 5; DWORD jmpBackOffset = targetAddrInOrg - srcAddrInTramp - 5;
// 写入 JMP 指令 (E9) g_pTrampoline[5] = 0xE9; // 写入偏移量 *(DWORD*)(g_pTrampoline + 6) = jmpBackOffset;
// 现在 g_pTrampoline 里的内容是: // [原函数前5字节] + [JMP 回原函数+5]
// --- 步骤 3: 实施 Hook (修改原函数) ---
// 3.1 修改内存保护属性 (代码段通常只读) DWORD oldProtect; VirtualProtect(g_pOrgMsgBox, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
// 3.2 计算去往 MyMessageBoxA 的偏移 // 公式:目标地址 - 当前地址 - 5 DWORD targetHookAddr = (DWORD)MyMessageBoxA; DWORD srcMsgBoxAddr = (DWORD)g_pOrgMsgBox; DWORD jmpToHookOffset = targetHookAddr - srcMsgBoxAddr - 5;
// 3.3 写入 JMP 指令 unsigned char* pByte = (unsigned char*)g_pOrgMsgBox; pByte[0] = 0xE9; // JMP 操作码 *(DWORD*)(pByte + 1) = jmpToHookOffset; // 偏移量
// 3.4 恢复内存保护 刷新缓存 VirtualProtect(g_pOrgMsgBox, 5, oldProtect, &oldProtect); FlushInstructionCache(GetCurrentProcess(), g_pOrgMsgBox, 5);
std::cout << "[+] Hook 安装完成 (32-bit JMP)" << std::endl;}
// ==========================================// 4. 卸载 Hook// ==========================================void UninstallHook() { if (!g_pTrampoline || !g_pOrgMsgBox) return;
std::cout << "[*] 正在卸载 Hook..." << std::endl;
// 1. 恢复原函数头部的 5 字节 // 从跳板里把备份的那 5 字节拷回去就行了 DWORD oldProtect; VirtualProtect(g_pOrgMsgBox, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(g_pOrgMsgBox, g_pTrampoline, 5);
VirtualProtect(g_pOrgMsgBox, 5, oldProtect, &oldProtect); FlushInstructionCache(GetCurrentProcess(), g_pOrgMsgBox, 5);
// 2. 释放跳板内存 VirtualFree(g_pTrampoline, 0, MEM_RELEASE); g_pTrampoline = NULL;
std::cout << "[+] Hook 已卸载,内存已释放" << std::endl;}
int main() { // 1. 正常调用 std::cout << "--- 1. 正常调用 ---" << std::endl; MessageBoxA(NULL, "我是正常弹窗", "Original", MB_OK);
// 2. 安装 Hook InstallHook();
// 3. Hook 后调用 std::cout << "--- 2. Hook 后调用 ---" << std::endl; MessageBoxA(NULL, "我是正常弹窗", "Original", MB_OK);
// 4. 卸载 Hook UninstallHook();
// 5. 验证恢复 std::cout << "--- 3. 卸载后调用 ---" << std::endl; MessageBoxA(NULL, "我是正常弹窗", "Original", MB_OK);
return 0;}
运行结果如图所示:
Part3 x64 Inline Hook
- ### 64 位的三大变化
| 特性 | 32位 (x86) | 64位 (x64) | 影响 |
| — | — | — | — |
| 指针大小 | 4 字节 | 8 字节 | 地址必须用 uintptr_t 存储。 |
| 跳转指令 | E9 (相对) | FF 25 (绝对) | E9 只能跳 $\pm 2GB$,x64 内存太大,常用绝对跳转。 |
| Hook 长度 | 5 字节 | 14 字节 | 需要破坏更多指令,风险增加。 |
- ### 难点:14 字节绝对跳转 (Shellcode)
x64 无法使用简单的 JMP,必须构建一段 Stub 代码来实现长距离跳转。
x64 跳转机器码结构 (14 Bytes):
Opcode: FF 25 00 00 00 00 (JMP [RIP+0])Addr: 00 11 22 33 44 55 66 77 (8字节绝对地址)
完整项目代码:
#include <windows.h>#include <iostream>
// ==========================================// 1. 定义函数指针与全局变量// ==========================================
// 定义 MessageBoxA 的函数原型typedef int (WINAPI* PMessageBoxA)(HWND, LPCSTR, LPCSTR, UINT);
// 保存原函数地址void* g_pOrgMsgBox = NULL;
// 保存跳板的内存地址unsigned char* g_pTrampoline = NULL;
// 【变化1】x64 绝对跳转 Shellcode 模板 (14 字节)// 机器码含义: JMP [RIP+0] -> 读取紧随其后的 8 字节地址并跳转unsigned char g_JmpShellcode[14] = { 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00, // [0-5] JMP [RIP+0] 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // [6-13] 占位符:8字节绝对地址};
// ==========================================// 2. 我们的 Hook 函数 (Payload)// ==========================================int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) { std::cout << "[!] [HOOK] 拦截成功!(x64 模式)" << std::endl;
// 篡改参数 LPCSTR newText = "恭喜!你成功完成了 x64 Inline Hook!"; LPCSTR newCaption = "x64 Hook 演示";
// 调用跳板 // 必须强转为函数指针 PMessageBoxA pOriginal = (PMessageBoxA)g_pTrampoline; return pOriginal(hWnd, newText, newCaption, uType);}
// ==========================================// 3. 安装 Hook (x64 升级版逻辑)// ==========================================void InstallHook() { HMODULE hUser32 = LoadLibraryA("user32.dll"); g_pOrgMsgBox = (void*)GetProcAddress(hUser32, "MessageBoxA");
if (!g_pOrgMsgBox) { std::cout << "[-] 无法找到 MessageBoxA" << std::endl; return; } std::cout << "[*] MessageBoxA 地址: 0x" << g_pOrgMsgBox << std::endl;
// --- 步骤 1: 准备跳板 (VirtualAlloc) ---
// 申请内存 (代码与 32 位完全一样,VirtualAlloc 自动处理 64 位地址) g_pTrampoline = (unsigned char*)VirtualAlloc(NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!g_pTrampoline) { std::cout << "[-] 内存申请失败" << std::endl; return; } std::cout << "[+] 跳板内存申请成功: 0x" << (void*)g_pTrampoline << std::endl;
// --- 步骤 2: 填充跳板内容 ---
// 【变化2】备份字节数从 5 变成了 14 // 因为我们需要覆盖 14 字节,所以必须备份 14 字节,否则原函数后半截会丢失 memcpy(g_pTrampoline, g_pOrgMsgBox, 14);
// 【变化3】构造回跳指令 (不再计算相对偏移,直接填绝对地址) // 目标:跳回原函数 +14 的位置 unsigned char jumpBackCode[14]; memcpy(jumpBackCode, g_JmpShellcode, 14);
// 计算绝对地址:原函数地址 + 14 // 注意:这里必须用 uintptr_t (unsigned long long) 来存 64 位地址 uintptr_t targetAddrInOrg = (uintptr_t)g_pOrgMsgBox + 14;
// 将 8 字节地址填入 Shellcode 的后半部分 ([6] 到 [13]) *(uintptr_t*)(jumpBackCode + 6) = targetAddrInOrg;
// 将构造好的 JMP 指令追加到跳板后面 (接在备份的 14 字节之后) memcpy(g_pTrampoline + 14, jumpBackCode, 14);
// --- 步骤 3: 实施 Hook (修改原函数) ---
// 3.1 修改内存保护属性 DWORD oldProtect; VirtualProtect(g_pOrgMsgBox, 14, PAGE_EXECUTE_READWRITE, &oldProtect);
// 3.2 构造去往 MyMessageBoxA 的跳转指令 unsigned char jumpToHookCode[14]; memcpy(jumpToHookCode, g_JmpShellcode, 14);
// 填入我们的函数地址 uintptr_t myFuncAddr = (uintptr_t)MyMessageBoxA; *(uintptr_t*)(jumpToHookCode + 6) = myFuncAddr;
// 3.3 写入 JMP 指令 (覆盖原函数前 14 字节) memcpy(g_pOrgMsgBox, jumpToHookCode, 14);
// 3.4 恢复内存保护 & 刷新缓存 VirtualProtect(g_pOrgMsgBox, 14, oldProtect, &oldProtect); FlushInstructionCache(GetCurrentProcess(), g_pOrgMsgBox, 14);
std::cout << "[+] Hook 安装完成 (x64 Absolute Jump)" << std::endl;}
// ==========================================// 4. 卸载 Hook// ==========================================void UninstallHook() { if (!g_pTrampoline || !g_pOrgMsgBox) return;
std::cout << "[*] 正在卸载 Hook..." << std::endl;
// 恢复原函数头部的 14 字节 DWORD oldProtect; VirtualProtect(g_pOrgMsgBox, 14, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(g_pOrgMsgBox, g_pTrampoline, 14);
VirtualProtect(g_pOrgMsgBox, 14, oldProtect, &oldProtect); FlushInstructionCache(GetCurrentProcess(), g_pOrgMsgBox, 14);
// 释放跳板内存 VirtualFree(g_pTrampoline, 0, MEM_RELEASE); g_pTrampoline = NULL;
std::cout << "[+] Hook 已卸载" << std::endl;}
int main() { // 1. 正常调用 std::cout << "--- 1. 正常调用 ---" << std::endl; MessageBoxA(NULL, "我是正常弹窗", "Original", MB_OK);
// 2. 安装 Hook InstallHook();
// 3. Hook 后调用 std::cout << "--- 2. Hook 后调用 ---" << std::endl; MessageBoxA(NULL, "我是正常弹窗", "Original", MB_OK);
// 4. 卸载 Hook UninstallHook();
// 5. 验证恢复 std::cout << "--- 3. 卸载后调用 ---" << std::endl; MessageBoxA(NULL, "我是正常弹窗", "Original", MB_OK);
return 0;}
- ### 原因:指令截断
执行,vs调试存在报错:
出现这种问题的原因:指令截断。假设MessageBoxA开头有15字节的指令。
CPU 尝试在 Trampoline 中执行指令时,遇到了第 3 条指令的那个“残缺字节”。 因为它不是一条完整的指令,CPU 抛出 Illegal Instruction 异常。
Part4 思考与总结
- ### 为什么会发生截断?
答案:x64 指令长度不固定。当我们强行覆盖 14 字节时,第 3 条指令(假设 6 字节)的前 5 个字节被覆盖,剩下 1 个字节 孤零零地留在内存中。当 CPU 尝试执行这段被破坏的代码时,会因无法识别残缺的指令而抛出 非法指令异常 (Illegal Instruction),导致程序崩溃。
- ### 解决方案 A:LDE (长度反汇编引擎)
原理:在 Hook 之前,先扫描函数开头的指令长度。
1. 如果前 N 条指令总长度 < 14,就继续加下一条。
2. 比如前 3 条指令加起来是 15 字节,那就备份 15 字节。
3. 写入 14 字节 JMP,第 15 个字节用 NOP (0x90) 填充,防止留下垃圾代码。
- ### 解决方案 B:中转跳板 (Relay Trampoline)
1. 申请内存:利用 VirtualAlloc 在原函数 2GB 范围内 申请一小块内存(Relay)。
2. 二级跳转:
- 第一跳(原函数 -> Relay):因为距离 < 2GB,可以使用短小的 5 字节
E9指令。这避免了 14 字节的大面积覆盖,大大降低截断风险。 - 第二跳(Relay -> Payload):在 Relay 内存中,写入 14 字节 的
FF 25绝对跳转指令,跳向任意远的恶意函数。
下一章,会利用方案A和方案B做进一步学习。
希水涵精选录是ABC_123运营的第2个公众号,专注于转载精心筛选的优质技术文章,同时也欢迎大家积极投稿。
Contact me: 2332887682#qq.com** OR 0day123abc#gmail.com**
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:希水涵精选录 八九 八九《精选10:Windows 安全 | Inline Hook 实战:从 x86 到 x64》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论