网上很多 Hook 教程都是直接告诉你找偏移、调用 Hook 模块即可,真正讲清楚 Hook 为什么这样实现、每一步在干什么的却比较少。
今天就从Inline Hook 的底层原理来分析整个流程。
声明:
本文仅用于技术分析与学习交流。为了保护分析对象,文中涉及的软件名称、偏移地址及部分代码均进行了修改,不能直接用于任何实际软件。
一个 Hook 调用长什么样?
很多 Hook 框架最终都是类似下面这样一个接口:
Hook( wechatwin_基址 + 偏移, 6, {96,86,232,0,0,0,0,97}, &接收消息, true );五个参数分别代表:
| 参数 | 含义 |
|---|---|
| Hook地址 | 需要Hook的位置(模块基址+偏移) |
| 备份长度 | 覆盖原指令长度 |
| 调用数据 | 写入的新代码模板 |
| 回调地址 | 我们自己的处理函数 |
| 是否前置 | 是否先执行自己的代码 |
真正的 Hook,其实就是围绕这五个参数完成的。
Hook 的整体流程
整个 Inline Hook 可以概括为下面几步:
原函数 │ │ ▼ 修改前5字节 │ ▼ jmp ---> 新申请内存(JMP区) │ ▼ 恢复被覆盖指令 │ ▼ 执行自己的回调函数 │ ▼ 恢复现场 │ ▼ jmp 回原函数剩余代码整个流程可以理解成:
"截胡 -> 做自己的事情 -> 再回去继续执行原来的代码。"
第一步:备份原始指令
首先要保存即将覆盖掉的机器码。
ReadProcessMemory( -1, Hook地址, 原字节集, 备份长度, &长度 );例如准备覆盖 6 个字节:
55 8B EC 83 EC 10这些指令稍后还要继续执行,因此必须提前备份。
否则程序执行流程就断掉了。
如下图(图1):
Hook 前,OD 中看到的是程序原始指令。
(图1)
第二步:申请一块新的可执行内存
JPM_地址 = VirtualAlloc( 0, 1024, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE );例如申请得到:
00AF0000为什么要申请?
因为:
我们的 Hook 代码不能直接写到原来的函数里面。
需要一块新的空间,用来:
- 恢复原始指令
- 调用自己的函数
- 跳回原函数
很多人把这里叫:
Trampoline或者
跳板第三步:恢复原始指令
第一件事就是把刚才备份的数据复制过去。
WriteProcessMemory( -1, JPM_地址, 原字节集, 备份长度, 0 );此时:
00AF0000 55 8B EC 83 EC 10这就是原来被覆盖掉的代码。
第四步:写入我们的 Hook 模板
例如:
{96,86,232,0,0,0,0,97}很多人第一次看到这串数据都会懵。
其实拆开以后非常简单。
60 pushad 56 push esi E8 ???????? 61 popad其中:
E8表示:
call xxxx后面的四个
00 00 00 00只是预留位置。
稍后会计算真正的 CALL 地址。
第五步:计算回调函数地址
代码里:
寻找字节集( 调用数据, {232} )实际上就是找到:
E8的位置。
例如:
96 86 232 00 00 00 00 97那么:
E8位于第3个字节。
随后:
回调偏移 += 备份长度假设:
3 + 6 = 9然后开始计算:
目标地址 - 当前地址 - 5也就是:
CALL偏移 = 回调函数 - CALL指令地址 - 5最终:
把算出的四字节偏移写回:
E8 XX XX XX XX这样:
执行到这里时,就会调用我们的函数。
如下图:
(图2)
第六步:写入跳回原函数的 JMP
自己的代码执行完以后,
程序不能停在这里。
还需要:
jmp跳回:
Hook地址 + 备份长度继续执行后面的代码。
于是:
WriteProcessMemory( JPM地址+备份长度+写入长度, {E9}+偏移, 5, 0 );此时:
跳板区域已经完整了。
流程变成:
恢复原指令 ↓ 调用自己的函数 ↓ jmp ↓ 原函数剩余代码如下图:
(图3)
第七步:修改原函数
真正开始 Hook。
把原来的代码改成:
E9 XXXXXXXX对应代码:
WriteProcessMemory( Hook地址, {E9}+偏移, 5, 0 );于是:
程序执行到这里时:
不会再继续执行原函数。
而是:
jmp ↓ 我们申请的新内存OD 中就能看到:
原来的代码已经变成:
jmp 00AF0000如下图:
(图4)
第八步:NOP 填充
因为:
jmp固定占:
5 字节如果原来覆盖的是:
6 7 8 9...那么:
多出来的字节必须:
NOP填充。
例如:
90 90 90对应代码:
WriteProcessMemory( Hook地址+5, 填充Dm, 长度, 0 );否则:
CPU 会把残留的半条指令继续执行。
程序极有可能直接崩溃。
如下图:
(图5)
最终执行流程
整个 Inline Hook 最终执行顺序如下:
程序运行 ↓ 执行 Hook 点 ↓ jmp 到跳板 ↓ 恢复被覆盖的原始指令 ↓ 执行自己的回调函数 ↓ jmp 回原函数 ↓ 继续执行剩余代码可以看到,所谓的 Inline Hook,本质上并不复杂,它只是利用了一条JMP指令改变程序执行流,再借助一块新申请的可执行内存(Trampoline)恢复现场、执行自定义逻辑,最后跳回原函数继续运行。
很多 Hook 框架(如 MinHook、Detours 等)虽然封装了大量细节,但核心思想都与本文分析的流程一致:保存原指令 → 跳转到跳板 → 执行自定义代码 → 返回原执行流。理解了这一过程,再去阅读各种 Hook 框架源码,会容易得多。
123123