Inline Hook

本节为轩辕的编程宇宙公众号《从零开始学逆向》的“Inline Hook”小节的学习笔记。详细课程内容请自行 跳转 加入查看。

在之前的 Maldev 开发课程中,已经学习过 Hook 原理及代码实现了。当时是通过 Detours 库、MinHook 库实现,或者手动写代码实现 Hook。但当学习《从零开始学逆向》的“Inline Hook”小节时,发现与之前的实现略有差异,新的代码涉及到了对偏移地址的计算,所以这里写一文来对比下与之前的差异。

学完后发现,两者的本质差异在于 JMP 指令的寻址方式。Maldev 开发课程使用的是 绝对近跳转,即通过寄存器(如 eax)直接存储目标函数的完整内存地址来完成跳转;《从零开始学逆向》则采用了 相对近跳转,即通过计算目标地址与当前指令地址之间的 偏移量(Offset)来完成跳转。

本文代码在 VS 2026 - x86 Debug 模式下编译测试完成。

1、JMP 指令细解

《从零开始学逆向》的“Inline Hook”小节中,涉及到对 JMP 指令跳转偏移量的计算。如果不理解偏移量是怎么计算的,那么大概率也很难吃透后续的代码。所以,这里花了很大的篇幅来介绍 JMP 指令。理解透了这部分,后面写 Hook 代码的时候就很容易理解了。

我们可以将 JMP 指令分为三大类:短跳转近跳转远跳转

1-1、JMP Short(短跳转)

它是最轻量级的跳转,只能向后(Forward)或者向前(Back)跳转。指令只占用 2 个字节。向后(Forward)指的是继续执行程序后续的代码指令,向前(Back)指的是程序之前已执行过的代码。

(1)指令格式

1
jmp short 标号(1字节偏移量)

(2)指令示例

1
jmp short 0x05   ; 向后(Forward)跳转 5 个字节

(3)机器码示例

1
EB XX   ; XX 是 1 字节的偏移量

(4)跳转范围

-128 到 +127 (即 -0x80 ~ 0x7F)

因为偏移量是一个有符号整数,所以这个“偏移量”可以正也可以负。

一字节的有符号整数。符号(+/-)占用 1 Bit,那么剩下的偏移量值就只有 7 bit 来表示了。那么最大正数可表示为 $2^7 - 1 = 127$,即最大向后(Forward)跳转 0x7F(127) 个字节;那么最大负数可表示为 $-(2^7)=-128$,即最大向前(Back)跳转 0x80(128) 个字节。

一个字节可以用 256 个编码来表示正数、负数,还有

  • 正数:1 到 127(占用了 127 个编码)
  • :0(占用了 1 个编码)
  • 负数:-1 到 -128(占用了 128 个编码)

因为“”在二进制中被归类为了非负数(即它的符号位是 0)。零占掉了一个本属于正数的“坑位”,那么就导致正数能表示的最大值比负数少了一个。也就是上面说的最大正数可表示为 $2^7 - 1 = 127$。

(5)跳转分析

在 x86 架构中,CPU 执行指令时,EIP/RIP(指令指针)永远指向“下一条即将执行的指令”。相对跳转就是在那个“下一条”的基础上进行加减。

假设我们的代码段从地址 0x1000 开始。

内存地址 机器码 (Hex) 汇编指令 解释
0x1000 EB 05 jmp short 0x05 当前指令EB是操作码,05是偏移量。
0x1002 90 nop 下一条指令地址。CPU读完上面那条,指针就指到这了。
0x1003 90 nop 距离下一条指令 +1 字节
0x1004 90 nop 距离下一条指令 +2 字节
0x1005 90 nop 距离下一条指令 +3 字节
0x1006 90 nop 距离下一条指令 +4 字节
0x1007 CC int 3 [+]目的地(Target) 距离下一条指令 +5 字节。

在 x86 环境下,相对跳转的计算公式如下:

目标地址 = 当前指令地址 + JMP指令长度 + 偏移量

目标地址 = 下一条指令地址 + 偏移量

带入公式:

目标地址 = 0x1000 + 2 + 0x05 (当前指令地址 + JMP指令长度 + 偏移量)

目标地址 = 0x1002 + 0x05 (下一条指令地址 + 偏移量)

目标地址 = 0x1007

(6)使用场景

常用于 if 语句、小型循环。

1-2、JMP Near(近跳转)

近跳转是指在当前代码段(CS)内部进行的跳转。根据寻址方式不同,分为相对近跳转(计算距离)和绝对近跳转(直接指定地址)。

”的本质定义:不改变 CS(代码段寄存器)。只要 CS 不变,无论是在 4GB 还是 16EB 的范围内跳,在 CPU 眼里都叫“段内跳转”,也就是“近跳转”。

1-2-1、相对近跳转(Rel)

相对近跳转 (Relative Near Jump)。机器码以 E9 开头,指令占用 5 个字节

(1)指令格式

1
jmp 标号(4字节偏移量)

(2)指令示例

1
2
jmp 0x12345678  ; 向后(Forward)跳转 0x12345678 个字节
# 注意!:0x12345678 是偏移量的值,只是看着像内存地址,实际”不是,不是,不是“程序的内存地址。

(3)机器码示例

1
E9 XX XX XX XX  ; XX 是 4 字节的有符号偏移量(小端序存储)

(4)跳转范围

-2,147,483,648 到 +2,147,483,647 (即 ±2GB)

偏移量为 4 字节(32 Bit)有符号整数。由于“零”占用了正数一个编码位,其范围遵循 $-2^{31}$到$2^{31}-1$。

(5)跳转分析

假设我们的代码段从地址 0x401000 开始。

内存地址 机器码 (Hex) 汇编指令 解释
0x401000 E9 FB 00 00 00 jmp 0xFB 当前指令地址E9 是操作码。
0x401005 ... ... 下一条指令地址 (0x401000 + 5)
0x401100 ... ... [+]目的地 (Target) 距离下一条指令 +0xFB(251) 字节。

在 x86 环境下,相对跳转的计算公式如下:

目标地址 = 当前指令地址 + JMP指令长度 + 偏移量

目标地址 = 下一条指令地址 + 偏移量

带入公式:

目标地址 = 0x401000 + 5 + 0xFB (当前指令地址 + JMP指令长度 + 偏移量)

目标地址 = 0x401005 + 0xFB (下一条指令地址 + 偏移量)

目标地址 = 0x401100

(6)使用场景

常用于函数间跳转、动态链接库调用等。

1-2-2、绝对近跳转(Abs)

绝对近跳转 (Absolute Near Jump)。机器码以 FF 开头,它不计算距离,直接跳转到寄存器或内存中存放的绝对地址

(1)指令格式

1
jmp 寄存器 ( rax/eax)

(2)指令示例

1
2
mov rax, 0x00007FF712345678
jmp rax   ; 直接跳转到 rax 存储的绝对地址

(3)机器码示例

1
FF E0     ; jmp rax 的机器码

(4)跳转范围

全内存空间 (x86 为 4GB / x64 为 16EB)

因为它直接将目标地址加载进指令指针寄存器(EIP/RIP),不受偏移量位数的限制,可以到达当前模式下 CPU 能寻址的任何地方。

(5)跳转分析

绝对跳转不需要计算偏移量,也没有“下一条指令”的基准加法。假设我们要跳往 0x7FF712345678

内存地址 机器码 (Hex) 汇编指令 解释
0x1000 48 B8 78 56 34 12 F7 7F 00 00 mov rax, 0x7FF712345678 先将 8 字节绝对地址存入寄存器
0x100A FF E0 jmp rax 当前指令地址。直接传送。
0x7FF712345678 ... ... [+]目的地 (Target)

1-3、JMP Far(远跳转)

JMP Far(远跳转)是 JMP 中最“重型”的跳转。它不仅改变指令指针(EIP/RIP),还会改变代码段寄存器(CS)

平坦内存模型:现代 Windows 系统中,所有应用程序的 CSDSSS 段基址其实都指向同一个地方(0地址)。换句话说,大家都在同一个“大段”里,很少需要到跨段需求。

(1)指令格式

1
2
3
jmp far 属性:偏移地址
; 汇编示例
jmp 0x0008:0x12345678

(2)指令示例

1
jmp ptr [mem]  ; 这里的 mem 包含 6 字节(32位模式)或 10 字节(64位模式)数据

(3)机器码示例

1
EA XX XX XX XX YY YY  ; EA 是操作码,后面跟 4 字节地址 + 2 字节段选择子

注意:在 64 位模式下,EA 指令通常被禁用,绝对远跳转多通过 FF /5 间接实现

(4)跳转范围

任意位置,超越当前段限制

它没有“距离”概念。因为它强制修改了 CS 寄存器,所以它可以跨越不同的权限级(Ring 3 切换到 Ring 0)或者不同的运行模式(32 位兼容模式切换到 64 位长模式)。

(5)跳转分析

JMP Far 与前两者最本质的区别:它会同时刷新 CS 和 EIP

假设我们要从当前的 32 位代码段跳往另一个段:

内存地址 机器码 汇编指令 解释
0x1000 EA 78 56 34 12 08 00 jmp 0x0008:0x12345678 当前指令地址EA 是操作码。
目的地 CPU 行为:
新 CS 0x0008 CPU 把 0x0008 加载进 CS 寄存器
新 EIP 0x12345678 CPU 把 0x12345678 加载进 EIP 寄存器

目标地址由指令直接给出。

1-4、JMP 指令小结

总结项 JMP Short JMP Near (Rel) JMP Near (Abs) JMP Far
字节大小 2 字节 5 字节 变长 (通常 2+) 7-11 字节
核心逻辑 相对距离 相对距离 绝对地址 绝对地址 + 段切换
修改寄存器 仅 EIP 仅 EIP 仅 EIP CS + EIP

2、基础 Inline Hook

Inline Hook 基本原理是直接修改原始函数的汇编指令为 jmp xxx。(一般在原函数开头修改,实际在原函数内任意位置修改均可)。

image-20260124210941222

本节会介绍最基础的两个 Inline Hook 示例(Hook 用户自定义函数、Hook 系统函数)。在不考虑原函数代码被再次调用的情况,直接只跳转到目标函数去执行。

2-1、Hook 用户自定义函数

Hook 用户自定义函数示例:

 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
#include <Windows.h>
#include <stdio.h>


void print_hello() {
	printf("Hello, World\n");
}

void hooked_function() {
	printf("Goodbye, World\n");
}

void install_hook1() {
	
	// 本示例先定义5个字节,满足存储 jmp 指令的最小空间。(完整Inline Hook时会调整)
	// jmp_code = {0x00, 0x00, 0x00, 0x00, 0x00};
	unsigned char jmp_code[5] = { 0 };

	// jmp 指令,E9 操作码部分
	jmp_code[0] = 0xE9;

	// jmp 指令,标号(4字节偏移量)部分。
	// 公式参考:目标地址 = 当前指令地址 + JMP指令长度 + 偏移量
	// 公式转换:偏移量 = 目标地址 - (当前指令地址 + JMP指令长度)
	// 因为我们实现 Hook 的效果是当程序执行 print_hello() 函数的时候,触发执行目标函数(hooked_function)。那么我们就在刚好进入 print_hello() 函数,还未执行第一条语句的时候,实现跳转指令即可。此时的当前指令地址直接用 print_hello() 函数首地址即可。(在 C 语言中,函数名称就代表函数的地址)
	int offset = (int)hooked_function - (int(print_hello) + 5);
	// 偏移量赋值到 jmp_code 的标号(4字节偏移量)部分。
	*(int *)&jmp_code[1] = offset;

	// 修改内存页面的权限
	DWORD dwOldProctect = NULL;
	VirtualProtect(print_hello, 4096, PAGE_EXECUTE_READWRITE, &dwOldProctect);

	// 拷贝构造的 jmp 指令到函数 print_hello 首地址处,完成指令覆盖。
	memcpy(print_hello, jmp_code, 5);
}


int main() {

	install_hook1();
	print_hello();

	return 0;

}

2-2、Hook 系统函数

Hook 系统函数示例:

 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
#include <Windows.h>
#include <stdio.h>

// 直接从 VS 的MessageBoxA() 函数定义拿即可
int WINAPI HookMessageBoxA(_In_opt_ HWND hWnd, _In_opt_ LPCSTR lpText, _In_opt_ LPCSTR lpCaption, _In_ UINT uType) {

	printf("[+] %s:%s\n", lpCaption, lpText);
	return 0;

}


void install_hook2() {

	// 本示例先定义5个字节,满足存储 jmp 指令的最小空间。(完整Inline Hook时会调整)
	// jmp_code = {0x00, 0x00, 0x00, 0x00, 0x00};
	unsigned char jmp_code[5] = { 0 };

	// jmp 指令,E9 操作码部分
	jmp_code[0] = 0xE9;

	// jmp 指令,标号(4字节偏移量)部分。
	// 公式参考:目标地址 = 当前指令地址 + JMP指令长度 + 偏移量
	// 公式转换:偏移量 = 目标地址 - (当前指令地址 + JMP指令长度)
	int offset = (int)HookMessageBoxA - (int(MessageBoxA) + 5);
	// 偏移量赋值到 jmp_code 的标号(4字节偏移量)部分。
	*(int *)&jmp_code[1] = offset;

	// 修改内存页面的权限
	DWORD dwOldProctect = NULL;
	VirtualProtect(MessageBoxA, 4096, PAGE_EXECUTE_READWRITE, &dwOldProctect);

	// 拷贝构造的 jmp 指令到函数 MessageBoxA 首地址处,完成指令覆盖。
	memcpy(MessageBoxA, jmp_code, 5);
}


int main() {

	install_hook2();
	MessageBoxA(NULL, "Test Tnformation", "Info", MB_OK);

	return 0;

}

2-3、Hook 代码优化

可以将前面的 Hook 代码优化为一个通用的 Hook 函数。后续使用时直接调用即可。

 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
#include <Windows.h>
#include <stdio.h>


int WINAPI HookMessageBoxA(_In_opt_ HWND hWnd, _In_opt_ LPCSTR lpText, _In_opt_ LPCSTR lpCaption, _In_ UINT uType) {

	printf("[+] %s:%s\n", lpCaption, lpText);
	return 0;

}

void install_hook(void *hooked_function, void *target) {

	// 本示例先定义5个字节,满足存储 jmp 指令的最小空间。(完整Inline Hook时会调整)
	// jmp_code = {0x00, 0x00, 0x00, 0x00, 0x00};
	unsigned char jmp_code[5] = { 0 };

	// jmp 指令,E9 操作码部分
	jmp_code[0] = 0xE9;

	// jmp 指令,标号(4字节偏移量)部分。
	// 公式参考:目标地址 = 当前指令地址 + JMP指令长度 + 偏移量
	// 公式转换:偏移量 = 目标地址 - (当前指令地址 + JMP指令长度)
	int offset = (int)target - (int(hooked_function) + 5);
	// 偏移量赋值到 jmp_code 的标号(4字节偏移量)部分。
	*(int *)&jmp_code[1] = offset;

	// 修改内存页面的权限
	DWORD dwOldProctect = NULL;
	VirtualProtect(hooked_function, 4096, PAGE_EXECUTE_READWRITE, &dwOldProctect);

	// 拷贝构造的 jmp 指令到函数 MessageBoxA 首地址处,完成指令覆盖。
	memcpy(hooked_function, jmp_code, 5);
}

int main() {

	install_hook(MessageBoxA, HookMessageBoxA);
	MessageBoxA(NULL, "Test Tnformation", "Info", MB_OK);

	return 0;

}

3、完整 Inline Hook

前面的基础代码存在三个问题。

问题一:在 Hook 的处理函数(HookMessageBoxA)中,要调用原始函数(MessageBoxA)怎么办?

因为原始函数被我们 Hook 了。那么,如果在 Hook 的处理函数(HookMessageBoxA)中调用原始函数(MessageBoxA)就会导致死循环。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 陷入死循环
int WINAPI HookMessageBoxA(_In_opt_ HWND hWnd, _In_opt_ LPCSTR lpText, _In_opt_ LPCSTR lpCaption, _In_ UINT uType) {

	printf("[+] %s:%s\n", lpCaption, lpText);
  
  // 调用原始函数 MessageBoxA
  MessageBoxA(NULL, "Test Tnformation", "Info", MB_OK);
  
	return 0;

}

问题二:如果要调用原始函数(MessageBoxA),但原始函数(MessageBoxA)中的前几行代码已经被我们覆盖掉了,那覆盖掉的指令又如何处理?

一般情况下,我们需要将原始函数(MessageBoxA)的前几行指令备份一下。

问题三:假如被 Hook 位置的指令被 jmp 指令(5字节)拦腰截断了。那么即使备份了,也是备份的残缺不全的指令,这又如何处理?

image-20260126020124491

此时,就需要根据目标位置的实际情况,动态决定到底备份几个字节。

3-1、实现逻辑图

image-20260126020753390

第一步:备份原始函数的前几字节指令到 origin_code 处(原函数入口)。

第二步:修改原始函数(被Hook函数)的前几字节为 jmp 指令,使其跳转到 Hook 中转代码(Hook Stub)处。

第三步:jz/jnz指令均通过 nop 代码指令序列,执行到 jmp hook_handler(Hook 处理函数)中去。

第四步:当在 Hook 处理函数中要调用原始函数时,就可以直接使用保留的”原函数入口“地址。”原函数指令入口“保存的原始函数指令执行后,会继续执行原始函数剩下的其他指令(jmp origin_next_code 跳转回到原代码块实现)。

第五步:flag1、flag2 字段,可实现代码辨识度。

3-2、代码实现

相关代码实现及详解,建议还是自行去看完轩辕大佬讲解的《从零开始学逆向》的“Inline Hook”小节视频。

下面的代码为我本人对轩辕大佬实现代码的理解。加入了自己理解的一些注释,重命名了一些变量名、变量类型等操作,来方便自己的理解。下面代码仅供参考,如有标注错误或代码实现错误,均以参考的轩辕大佬实现的代码为正确实现。

(1)InlineHook.h 代码

 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
#pragma once

#include <Windows.h>

typedef unsigned char byte;
typedef signed char int8_t;
typedef unsigned char uint8_t;
typedef short int16_t;
typedef unsigned short uint16_t;
typedef int int32_t;
typedef unsigned int uint32_t;
typedef long long int64_t;
typedef unsigned long long uint64_t;
typedef uint32_t boolean_t;


typedef struct _HOOK_STUB {
	BYTE jz[2];
	BYTE jnz[2];
	BYTE flag1[4];
	BYTE nop[5];
	BYTE jmp_handler[5];
	BYTE Original_Code[15];
	BYTE jmp_Original_Func_Next[5];
	BYTE flag2[4];
}HOOK_STUB, *PHOOK_STUB;



class CInlineHook {

public:
	CInlineHook();
	~CInlineHook();

	BOOL InstallHook(void *Hooked_Function, void *HandlerFunction);
	BOOL InstallHook(const char *ModuleName, const char *ApiName, void* HandlerFunction);
	
	BOOL UninstallHook();

	PVOID GetOriginalFunction() {
		return m_OriginalFunction;
	}

protected:
	// 获取汇编指令长度,用于HOOK
	static int InstructLen(void *Address);
	static const uint8_t c_opinfo[256];

protected:
	BOOL InitHookStub(PHOOK_STUB HookStub);
	
	INT m_HookLen;
	PHOOK_STUB m_HookStub;
	VOID *m_OriginalFunction;
	VOID *m_Hooked_Function;
	
};

(2)InlineHook.cpp 代码

  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
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
#include <Windows.h>
#include <stdio.h>
#include "InlineHook.h"


/**
* info structure:
*	0x26 means: 6 2 (high-low, low-high)
*
* Bit 0:
*	1 - has ModR/M
*	0 - no ModR/M byte
* Bit 1~3:
*	0 - no imm
*	1 - Ib, Jb
*	2 - Iw
*	3 - Iv, Iz, Jz
*	4 - Ib + Iw
*
* special cases:
*	1. group f6xx, f7xx: nnn = 000,001 -- uses Iz
*	2. 9a, ea: Ap (xxxx:xxxxxxxx), 6-byte imm
*	3. Ob, Ov: 4-byte long offset
*/

const uint8_t CInlineHook::c_opinfo[256] = {
	/*        0 1  2 3  4 5  6 7  8 9  A B  C D  E F  */
	/*       ---------------------------------------  */
	/* 00 */ 0x11, 0x11, 0x26, 0x00, 0x11, 0x11, 0x26, 0x00,
	/* 10 */ 0x11, 0x11, 0x26, 0x00, 0x11, 0x11, 0x26, 0x00,
	/* 20 */ 0x11, 0x11, 0x26, 0x00, 0x11, 0x11, 0x26, 0x00,
	/* 30 */ 0x11, 0x11, 0x26, 0x00, 0x11, 0x11, 0x26, 0x00,
	/* 40 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	/* 50 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	/* 60 */ 0x00, 0x11, 0x00, 0x00, 0x67, 0x23, 0x00, 0x00,
	/* 70 */ 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
	/* 80 */ 0x37, 0x33, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
	/* 90 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	/* A0 */ 0x00, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00,
	/* B0 */ 0x22, 0x22, 0x22, 0x22, 0x66, 0x66, 0x66, 0x66,
	/* C0 */ 0x33, 0x40, 0x11, 0x37, 0x80, 0x40, 0x02, 0x00,
	/* D0 */ 0x11, 0x11, 0x22, 0x00, 0x11, 0x11, 0x11, 0x11,
	/* E0 */ 0x22, 0x22, 0x22, 0x22, 0x66, 0x02, 0x00, 0x00,
	/* F0 */ 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x11,
	/*        0 1  2 3  4 5  6 7  8 9  A B  C D  E F  */
	/*       ---------------------------------------  */
	0x11, 0x11, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, /* 0F 00 */
	0x11, 0x11, 0x11, 0x11, 0x10, 0x00, 0x00, 0x01, /* 0F 10 */
	0x11, 0x11, 0x10, 0x10, 0x11, 0x11, 0x11, 0x11, /* 0F 20 */
	0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, /* 0F 30 */
	0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, /* 0F 40 */
	0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, /* 0F 50 */
	0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, /* 0F 60 */
	0x33, 0x11, 0x11, 0x10, 0x00, 0x00, 0x11, 0x11, /* 0F 70 */
	0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, /* 0F 80 */
	0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, /* 0F 90 */
	0x00, 0x01, 0x31, 0x00, 0x00, 0x01, 0x31, 0x11, /* 0F A0 */
	0x11, 0x11, 0x11, 0x11, 0x00, 0x11, 0x11, 0x11, /* 0F B0 */
	0x11, 0x31, 0x33, 0x31, 0x00, 0x00, 0x00, 0x00, /* 0F C0 */
	0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, /* 0F D0 */
	0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, /* 0F E0 */
	0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x10  /* 0F F0 */
	/*        0 1  2 3  4 5  6 7  8 9  A B  C D  E F  */
	/*       ---------------------------------------  */
};

enum cpu_mode_t { cm_legacy = -1, cm_compat = 0, cm_64bit = 1 };

// 获取汇编指令长度
int CInlineHook::InstructLen(void *Address)
{
	enum cpu_mode_t cm = cm_compat;
	register uint8_t *p = (uint8_t *)Address;
	register uint8_t b = 0;
	register boolean_t pre_66 = false;
	uint8_t info;
	register int tbl_fixup = 0;
	register uint8_t i_info = 0;
	register uint8_t modrm = 0;

	if (!Address)
		return -1;

	while (1)
	{
		b = *p++;

		if (b >= 0x40 && b <= 0x4f && cm == cm_64bit)
			continue;
		else if (b == 0x66) {
			pre_66 = true;
			continue;
		}
		else if (b == 0xf0 || b == 0xf2 || b == 0xf3 ||
			b == 0x26 || b == 0x2e || b == 0x36 ||
			b == 0x3e || b == 0x64 || b == 0x65 ||
			b == 0x67)
			continue;
		break;
	}

	if (b == 0x0f) {
		b = *p++;
		tbl_fixup = 128;
	}

	info = c_opinfo[(b >> 1) + tbl_fixup];
	info = ((b % 2) ? info : (info >> 4)) & 0x0f;
	i_info = (info >> 1) & 7;

	if (info & 0x01) {
		/* has modrm */
		modrm = *p++;
		do {
			register uint8_t sib = 0;
			boolean_t has_sib = false;
			register uint8_t tmp = 0;

			if ((modrm & 0xc0) == 0xc0)	/* mod == 3 */
				break;

			if (cm != cm_legacy && (modrm & 0x07) == 4) {
				has_sib = true;
				sib = *p++;
			}
			/* displacement */
			tmp = has_sib ? sib : modrm;
			if (!(modrm & 0xc0)) {	/* mod == 00b */
				if ((tmp & 0x07) == 5)
					p += (cm == cm_legacy) ? 2 : 4;
			}
			else if ((modrm & 0xc0) == 0x40) {	/* mod == 01b */
				++p;
			}
			else {	/* mod == 0x10b */
				p += (cm == cm_legacy) ? 2 : 4;
			}
		} while (0);
	}

	/* special cases */
	do
	{
		register uint8_t tmp = (modrm & 0x38) >> 3;	/* nnn */
		if (tmp == 0 || tmp == 1) {
			if (b == 0xf6)
				i_info |= 1;
			else if (b == 0xf7)
				i_info |= 3;	/* Iz */
		}
		if (b == 0x9a || b == 0xea)	/* Ap */
			p += 6;
		if (b >= 0xa0 && b <= 0xa3)
			p += 4;

	} while (0);

	/* take care of immediate value */
	switch (i_info) {
	case 0:	break;
	case 1:	++p; break;
	case 2:	p += 2;	break;
	case 3:	p += pre_66 ? 2 : 4; break;
	case 4:	p += 3;	break;
	}

	return (int)(p - (uint8_t *)Address);
}


CInlineHook::CInlineHook() {
	m_HookLen = NULL;
	m_HookStub = NULL;
	m_OriginalFunction = NULL;
	m_Hooked_Function = NULL;
}

CInlineHook::~CInlineHook() {
	if (m_HookStub != NULL) {
		delete m_HookStub;
		m_HookStub = NULL;
	}
}

BOOL CInlineHook::InitHookStub(PHOOK_STUB HookStub) {

	if (HookStub == NULL) {
		return FALSE;
	}

	RtlSecureZeroMemory(HookStub, sizeof(HOOK_STUB));
	
	// 初始化的 HookStub 结构模板
	// 填充 JZ : 74 是 Opcode, 06 是相对偏移
	HookStub->jz[0] = 0x74;
	HookStub->jz[1] = 0x06;   // 目标:下一条指令 + 6 字节 => nop 指令位置
	// 填充 JNZ : 75 是 Opcode, 04 是相对偏移
	HookStub->jnz[0] = 0x75;
	HookStub->jnz[1] = 0x04;  // 目标:下一条指令 + 4 字节 => nop 指令位置
	// 填充特征标识符1
	memcpy(HookStub->flag1, "test1", sizeof(HookStub->flag1));
	// 填充 nop 指令
	memset(HookStub->nop, 0x90, sizeof(HookStub->nop));
	// 填充 Hook 处理函数的 jmp 跳转指令(操作码+偏移量(暂不填充))
	HookStub->jmp_handler[0] = 0xE9;
	// 填充 原始函数的前几字节指令(先全使用nop填充,后续根据指令实际字节数精确覆盖。精确覆盖后剩下的nop指令,起到程序正常继续向后执行指令的效果)
	memset(HookStub->Original_Code, 0x90, sizeof(HookStub->Original_Code));
	// 填充 跳转到原始函数剩余指令处的 jmp 跳转指令(操作码+偏移量(暂不填充))
	HookStub->jmp_Original_Func_Next[0] = 0xE9;
	// 填充特征标识符2
	memcpy(HookStub->flag2, "test2", sizeof(HookStub->flag2));

}


BOOL CInlineHook::InstallHook(const char *ModuleName, const char *ApiName, void *HandlerFunction) {

	BOOL bRet = FALSE;
	HMODULE hModule = NULL;
	PVOID Hooked_Function = NULL;

	if (ModuleName == NULL || ApiName == NULL || HandlerFunction == NULL) {
		return FALSE;
	}

	// 尝试获取模块句柄,若未加载则尝试加载
	hModule = GetModuleHandleA(ModuleName);
	if (hModule == NULL) {
		hModule = LoadLibraryA(ModuleName);
	}
	if (hModule == NULL) {
		// 模块未找到
		return FALSE;
	}

	// 获取函数地址
	Hooked_Function = GetProcAddress(hModule, ApiName);
	if (Hooked_Function == NULL) {
		// 函数地址获取失败
		return FALSE;
	}

	// 获取函数地址成功后,执行 InstallHook() 两个参数的函数版本
	bRet = InstallHook(Hooked_Function, HandlerFunction);

	return bRet;

}


BOOL CInlineHook::InstallHook(void *Hooked_Function, void *HandlerFunction) {

	BOOL bRet = FALSE;
	DWORD dwOldProtection1 = NULL;
	DWORD dwOldProtection2 = NULL;

	if (Hooked_Function == NULL || HandlerFunction == NULL) {
		return FALSE;
	}

	// 计算 Hook 的字节长度
	int hook_len = 0;
	uint8_t *start_address = (uint8_t *)Hooked_Function;
	while (hook_len < 5)
	{
		int instruct_len = InstructLen(start_address);
		if (instruct_len <= 0 || instruct_len >= 10)
			break;

		hook_len += instruct_len;
		start_address += instruct_len;
	}

	// 在堆内存中为 HOOK_STUB 结构体申请空间,并赋予它“执行权限”。
	m_HookStub = new HOOK_STUB();
	if (m_HookStub == NULL) {
		return FALSE;
	}
	if (!VirtualProtect(m_HookStub, sizeof(HOOK_STUB), PAGE_EXECUTE_READWRITE, &dwOldProtection1)) {
		printf("[!] VirtualProtect Failed With Error : %d\n", GetLastError());
		delete m_HookStub;
		m_HookStub = NULL;
		return FALSE;

	}

	// [i] 初始化 m_HookStub 结构
	InitHookStub(m_HookStub);
	
	// 将原函数开头的 hook_len 字节指令,填充到 m_HookStub 结构对应的位置
	memcpy(m_HookStub->Original_Code, Hooked_Function, hook_len);
	
	// 填充 跳转到 Hook处理函数(jmp_handler) 位置的 jmp 指令偏移量部分。
	// 偏移量 = 目标地址 - (当前指令地址 + JMP指令长度)
	*(uint32_t *)(&m_HookStub->jmp_handler[1]) = (uint32_t)HandlerFunction - ((uint32_t)&m_HookStub->jmp_handler + 5);

	// 填充 跳转回 原始函数剩余指令(jmp_Original_Func_Next) 位置的 jmp 指令偏移量部分。
	// 偏移量 = 目标地址 - (当前指令地址 + JMP指令长度)
	// 目标地址 = 原函数入口地址 + 备份的指令(原始函数的前几字节)长度
	*(uint32_t *)(&m_HookStub->jmp_Original_Func_Next[1]) = ((uint32_t)Hooked_Function + hook_len) - ((uint32_t)&m_HookStub->jmp_Original_Func_Next + 5);
	
	// [+] m_HookStub 结构填充完成


	// [i] 准备 Hook 原始函数入口跳转指令 (jmp_entry)
	// 实际的 JMP 指令只占 5 字节,但我们计算出的 hook_len(需要覆盖的长度)可能是 5、6、7 甚至更多。
	// 如果 hook_len 是 7,而我们只写了 5 个字节,剩下的 2 个字节会变成残缺的指令。CPU 执行到残缺指令会直接崩溃。
	// 通过预填 0x90,我们确保了这 5 字节之后的所有空间都是安全的“空指令”,CPU 会像滑滑梯一样(NOP Sled)顺着 NOP 滑到我们的 JMP 后方或被直接跳转带走。
	unsigned char jmp_entry[20] = { 0 };

	// 先填充满 nop 指令
	memset(jmp_entry, 0x90, sizeof(jmp_entry));

	// jmp 指令,E9 操作码部分
	jmp_entry[0] = 0xE9;

	// jmp 指令,标号(4字节偏移量)部分。
	// 偏移量 = 目标地址 - (当前指令地址 + JMP指令长度)
	// 目标地址 = 前面填充好的 m_HookStub 的地址
	*(uint32_t *)(&jmp_entry[1]) = (uint32_t)m_HookStub - ((uint32_t)(Hooked_Function) + 5);


	// [i] 开始安装 Hook
	// 修改原始函数内存页面的权限
	VirtualProtect(Hooked_Function, hook_len, PAGE_READWRITE, &dwOldProtection2);

	// 拷贝构造的 jmp 指令到原始函数首地址处,完成指令覆盖。
	memcpy(Hooked_Function, jmp_entry, hook_len);
	
	// 指令覆盖完成后,还原原始函数内存页面的权限
	VirtualProtect(Hooked_Function, hook_len, dwOldProtection2, &dwOldProtection2);
	
	// [+] Hook 安装完成

	// 保存状态
	m_OriginalFunction = &m_HookStub->Original_Code;
	m_Hooked_Function = Hooked_Function;
	m_HookLen = hook_len;

	return TRUE;
}


BOOL CInlineHook::UninstallHook() {

	DWORD dwOldProtection = NULL;

	if (m_HookStub == NULL) {
		return FALSE;
	}

	// 恢复原始函数
	VirtualProtect(m_Hooked_Function, m_HookLen, PAGE_READWRITE, &dwOldProtection);
	memcpy(m_Hooked_Function, m_HookStub->Original_Code, m_HookLen);
	VirtualProtect(m_Hooked_Function, m_HookLen, dwOldProtection, &dwOldProtection);

	delete m_HookStub;
	m_HookStub = NULL;

	printf("[+] UnHook Success.\n");
	
	return TRUE;
}

(3)Hooks-InlineHook.cpp 代码

 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
#include <Windows.h>
#include <stdio.h>
#include "InlineHook.h"

#include <map>
#include <string>
using namespace std;

// 全局钩子管理器 HookTable
std::map<std::string, CInlineHook*> HookTable;
// 键 (Key): std::string  - 钩子的唯一标识(通常是函数名)
// 值 (Value): CInlineHook* - 指向具体 Hook 对象的指针,记录了原始字节和跳转逻辑


// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxa
typedef int (WINAPI *fnMessageBoxA)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);

// Used as a unhooked MessageBoxA in `MyMessageBoxA`
fnMessageBoxA g_pMessageBoxA = NULL;


int WINAPI HookMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {

	// 自定义逻辑,可实现任意自定义的操作
	printf("[+] %s:%s\n", lpCaption, lpText);
	printf("[+] Hook Success.\n");

	// 从全局管理表中,通过函数名取回对应的 Hook 管理对象
	CInlineHook *Hook = HookTable["MessageBoxA"];

	// 获取被 Hook 之前的原始函数入口(通常是经过备份或中转的地址)
	g_pMessageBoxA = (fnMessageBoxA)Hook->GetOriginalFunction();

	// 执行原本的 MessageBoxA 逻辑,让程序看起来“一切正常”
	g_pMessageBoxA(hWnd, lpText, lpCaption, uType);

	return 0;

}



int main(int argc, char *argv[]) {

	CInlineHook Hook;

	Hook.InstallHook("User32.dll", "MessageBoxA", HookMessageBoxA);

	// 向全局钩子管理器表 HookTable 中添加一条记录
	// 语法:表名.插入(键值对<键类型, 值类型>("函数名", 对象地址));
	HookTable.insert(pair<string, CInlineHook*>("MessageBoxA", &Hook));

	// 测试函数 Hook 的效果
	MessageBoxA(NULL, "Test Tnformation", "Info", MB_OK);

	// 卸载 Hook
	Hook.UninstallHook();

	return 0;
}
updatedupdated2026-02-252026-02-25