游戏逆向中常用的Hook技术
一、内联Hook(InlineHook)
(1)inline hook 是什么
当我们想要拦截现有运行中的进程内某个现有的汇编函数体,最常用的办法就是 inline hook。
它可以在权限允许内,通过修改程序运行中的内存代码段汇编,以达到拦截任何函数的目的,包括系统api(只限非内核态的函数体,要hook内核函数需要进内核态),以及程序内部现有的任何函数体。
比如想拦截系统APICreateFileW的调用,修改原调用参数并继续执行CreateFileW原函数逻辑,获得返回值,或者直接拦截返回NULL失败,或者拦截程序本身代码汇编的函数体,用 inline hook都可以做到。
具体步骤如下:备份原始指令:在目标函数入口处,保存前几个字节(通常是 5 到 12 字节)。写入跳转指令:将目标函数的开头替换为一条跳转指令(通常是 JMP)。执行自己的逻辑:程序运行到目标函数时,会直接跳到你写的“钩子函数”里。跳回原函数:如果你还想让原程序继续运行,就在执行完你的逻辑后,先执行备份的原始指令,再跳回目标函数的后续位置。
(2)示例代码解析
x86示例代码如下,需要注意的是编译器如果发现 OriginalHelloWorldFunction 内容很短,且在同一个文件里,它在编译 main 函数时,将不会去执行 CALL 指令,而是直接把那句 printf 的内容复制到了 main 里面。所以需要在函数定义前加上 __declspec(noinline),通知编译器不要内联这个函数。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
|

x64示例代码:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
|
(3)原理解析
Inline Hook 是通过直接修改目标函数在内存中的机器指令来实现的。
在x86系统中。
常用 JMP(操作码 0xE9)去完成这个跳转操作,而JMP指令有两个特点:1.E9 指令后面需要跟一个 4 字节的偏移地址。2.总计 5 字节。记住这两个之后,就到了跳转地址的计算技术(重点),这是很多初学者容易卡住的地方。JMP 指令里的地址不是目标的绝对内存地址,而是相对偏移量。
计算公式:相对偏移 = 目标函数地址 - 原函数地址 - 跳转指令本身长度(5字节)。之所以这么算,是因为 CPU 执行到 JMP 时,指令寄存器(EIP/RIP)已经指向了 JMP 的下一条指令地址。所以你得把这 5 个字节抠掉,剩下的才是要跨越的距离。
假设:
目标地址 (TargetAddress):你想跳去的地方(你的钩子函数 MyCustomFunction),地址是 0x00401050。
源地址 (SourceAddress):你准备动手修改的地方(原函数 OriginalFunction),地址是 0x00401000。
套用公式:我们要计算的是填在 0xE9 后面的那 4 个字节到底是多少。相对偏移 = 0x00401050 - 0x00401000 - 5。先算地址差:0x00401050 - 0x00401000 = 0x50 (十进制的 80)。再减去指令长度:0x50 - 5 = 0x4B (十进制的 75)所以,相对偏移量就是 0x0000004B。
CPU 在执行指令时,EIP(指令指针)永远指向“下一条即将执行的指令”:CPU 读到了 0x00401000 处的 E9。在它还没开始“跳”之前,它的 EIP 已经自动增加,指向了 JMP 指令结束后的那个位置,即 0x00401005。此时 CPU 执行跳转,它会拿 当前的 EIP (0x00401005) + 你的偏移量 (0x4B)。计算结果:0x00401005 + 0x4B = 0x00401050。
如果是“往回跳”怎么办:如果你的目标地址比源地址小(比如从 0x401050 跳回 0x401000),公式依然成立。相对偏移 = 0x00401000 - 0x00401050 - 5 = -0x50 - 5 = -0x55。在 32 位计算机中,负数用补码表示。-0x55 转换成 4 字节十六进制就是 0xFFFFFFAB。你写入 E9 AB FF FF FF,CPU 同样能带你跳回去。
在x 64 位系统中。
由于内存空间太大,4 字节的偏移量(最大 ±2GB)可能跳不过去。所以常用 12 字节 的绝对跳转:
mov rax, 0x1122334455667788 ; 48 B8 ... (把 8 字节绝对地址存入寄存器) jmp rax ; FF E0
也就是把跳转地址存放到寄存器中,然后通过jmp寄存器的方式跳过去。
涉及的关键系统函数是VirtualProtect,代码段在内存里通常是“只读”的(PAGE_EXECUTE_READ)。想要修改人家的机器码,必须先用这个函数把权限改成“可读可写可执行”(PAGE_EXECUTE_READWRITE),改完后再换回去。
在 32 位(或 64 位的 E9 跳转)中,指令里存放的是“距离”。而我们在 64 位中常用的 12 字节 Hook,利用了寄存器作为中转站,直接把目标的绝对地址(Absolute Address)写进了指令里。在这种模式下,你只需要通过 &MyCustomFunction 获取钩子函数的 64 位完整地址,然后用 memcpy 直接把它塞进机器码的第 2 到第 9 个字节位置即可。没有加减法,只有搬运。当然,如果选择5 字节相对跳转,那么必须满足目标函数和原函数的距离必须在 ± 2GB 之内,也就是依旧要用到那个公式。
二、IAT Hook
熟悉PE结构的应该知道.IAT 是导入表。
对于不熟悉PE结构的人:
IAT (Import Address Table),即导入地址表。
你写了一个程序,调用了 MessageBoxA。但你的程序本身并不知道 MessageBoxA 在内存的哪个角落,因为 user32.dll 每次加载的地址可能都不一样。Windows 的做法:在你的程序(PE文件)里留一张“通讯录”。加载时:当程序启动时,Windows 加载器会找到 MessageBoxA 的真实地址,并把它填进这张表里。运行时:你的程序每次想弹窗,都会去查这张表,然后跳到表里记录的地址。
其IAT表结构如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR { |
union { |
DWORD Characteristics; |
DWORD OriginalFirstThunk; 指向INT表 4个字节一组.是RVA指向名字跟序号 |
} DUMMYUNIONNAME; |
DWORD TimeDateStamp; |
DWORD ForwarderChain; |
DWORD Name; |
DWORD FirstThunk; 在文件中跟INT表一样.这是IAT |
} IMAGE_IMPORT_DESCRIPTOR; |
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR; |
我们知道PE有两种状态.第一种.在文件中的状态. 所以才有 VA 转 FOA等等的互相转换.
在文件状态. IAT表(firstThunk)跟 INT表一样.都是指向一个很大的表.这个表里面是4个字节进行存储.存储的是Rva. 这些RVA分别指向 导入序号以及以0结尾的字符串.
如果在内存状态.则INT表跟上面说的文件状态一样指向 导入序号.以及导入的函数名字.
而IAT此时不同了.IAT此时就是保存着INT指向的导入函数的地址了.
三、VTable Hook
四、SSDT Hook
五、EPT Hook
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐




所有评论(0)