tinyhook,一个非常简单的inline hook框架
本帖最后由 DEATHTOUCH 于 2024-2-3 23:32 编辑# tinyhook
一个极其简单的 inline hook 框架,最早是为了 hook 《文明 6》这款游戏调用的一些 socket 函数而设计的,后来由于功能过于简陋,但又没有解决办法而逐渐放弃。
但是前几天在论坛得到热心坛友 deice 提供的代码(原始出处为 <https://github.com/oblique/insn_len>),实现了曾经未完成的功能(自动判断指令长度实现对原始函数的调用功能),所以在经过一段时间的重构之后,得以实现之前缺失的功能。
有关 inline hook 的具体实现原理,论坛上有不少优秀的帖子,我就不重复这些了。
不过由于代码极其简单,所以对于一些复杂的情况就不能使用了,目前仅实现了基础的导入函数的跳转绕过功能,对于函数一开头就进行其他情况的跳转就无能为力了,实测对 Windows API 应该还是比较友好的。
有复杂需求的推荐用 Detours 或者 minhook 这样成熟的框架,而我这个主要特点是结构简单代码体积小,编译完的代码极小,仅几 KB 的体积。
完整代码在 GitHub 仓库:<https://github.com/DrPeaboss/tinyhook>
还上传了一份到 gitee,如果 GitHub 不方便访问:<https://gitee.com/peazomboss/tinyhook>
由于 x86 的 hook 实现比较简单,所以主要介绍一下 x64 和 ARM64 的实现。
实现了 ARM64 的原因是我有一台搭载了骁龙 8cx gen3 的设备,运行 Windows on ARM,所有代码以及本文都是在这台设备写的。
## x64 hook 简介
众所周知机器码为 `E9 ?? ?? ?? ??` 的跳转指令只有 2GB 的寻址范围,而对于 Windows 的一些模块其地址空间和用户地址空间明显超过了 2GB 的大小,所以必须使用二次跳转的方式实现。
方法有好多种,具体如下:
```asm
; 利用rax跳转,改变寄存器
mov rax, address
jmp rax
; 借用 rax,使用 ret 跳转,不改变寄存器
push rax
mov rax, address
xchg rax,
ret
; 压栈,使用 ret 跳转,不改变寄存器
push address.low
mov dword , address.high
ret
; 使用 FF 25 跳转,不改变寄存器
jmp ; FF 25 00 00 00 00
db address
```
后面几种不改变寄存器的方案都可以,最简单的就是最后一种,我后来就改了这种(之前是倒数第二种)。
不过问题又来了,如果把一个函数头的 14 个字节改了,肯定非常容易出问题,最好是少改点,只用 5 字节先跳走,再搭个桥跳到指定的地方。当然甚至可以只改 2 字节,用一句短跳找到最近 128 字节范围内的 5 或 14 字节空地,不过这样就挺麻烦了。
然后问题是桥怎么搭,或者是 14 字节的空地怎么找。我的答案是~~让用户自己找吧~~,额,不,当然不是了。至少我是提供了一个函数给用户来找的:
```cpp
void* TH_GetModulePadding(HMODULE hmodule)
{
BYTE* p = (BYTE*)hmodule;
p += ((IMAGE_DOS_HEADER*)p)->e_lfanew + 4; // PE header
p += sizeof(IMAGE_FILE_HEADER) + ((IMAGE_FILE_HEADER*)p)->SizeOfOptionalHeader; // skip optional
int sections = ((IMAGE_FILE_HEADER*)p)->NumberOfSections;
for (int i = 0; i < sections; i++) {
IMAGE_SECTION_HEADER* psec = (IMAGE_SECTION_HEADER*)p;
if (memcmp(psec->Name, ".text", 5) == 0) {
BYTE* offset = (BYTE*)hmodule + psec->VirtualAddress + psec->Misc.VirtualSize;
offset += 16 - (INT_PTR)offset % 16; // align 16
return (void*)offset;
}
p += sizeof(IMAGE_SECTION_HEADER);
}
return NULL;
}
```
方法就是利用 PE 头去找。比如要 hook 的函数是 `MessageBoxA`,它在模块 ` user32.dll `,那就去里面找呗。众所周知啊,Windows 加载的模块都是有 PE 头的,而代码是在 `.text` 区段的,而且啊由于内存是按 4K 对齐的,和文件不一样,所以一定是存在空地的,具体就是通过 `VirtualAddress` 和 `VirtualSize` 来计算。然后对齐一下 16 字节就 OK 了。至于这片区域怎么管理,交给用户去吧!
当然如果要其他操作的话,那真的就自己找内存去中转吧。
然后就是自动 hook 的关键之找指令长度,这个具体代码由开头的 GitHub 仓库提供,我简单封装了一下下:
```cpp
static int GetEntryLen(void* addr)
{
int len = 0;
BYTE* p = (BYTE*)addr;
do {
len += insn_len((void*)(p + len));
} while (len < 5);
return len;
}
```
至少读 5 个字节,因为 hook 要改 5 个字节。然后把这些多余的字节拷走,用来实现调用原始函数的功能,也即 detour。
还有一个细节,就是有些 API 就是一个导入表的格式,这个是 Windows 为了兼容性留下的,需要辨别。比如 `user32.dll` 里的 `GetForegroundWindow` 函数头部就是一句 `FF 25` 开头的跳转指令,这个需要跳过。
## ARM64 hook 简介
ARM64 和 x86 这些非常不一样,它的指令是固定 32 位长的,然后在一个 32 位的字里面划分里面的立即数、寄存器等,和 x86 的 Mod/RM 还有 SIB 不一样,没有固定的格式。
具体 hook 的思路和 x64 差不多,需要中转操作,因为能少改头部的指令就少改。
所以我选择的方案是改一句指令,把头部指令改成一条无条件跳转指令,寻址范围是 ±128MB,基本找模块的空白区域是足够了。
该指令的格式为 `000101XXXX...` 也就是 6 字节的标识和 26 字节的立即数。
然后中转处的跳转采用 `LDR X16, addr` 和 `BR X16` 的组合实现。其中的 LDR 指令相对 PC 寻址,地址占 8 字节,紧跟这两句指令,有点像 x86 里面的 `FF 25` 指令的逻辑。这样算下来需要 16 字节进行一次长跳转。
和 x64 一样,有些函数导入表格式的函数头部可能有两种情况,一种是 `B offset` 的指令,128MB 的寻址范围,另一种是以 `ADRP X16, offset`、`LDR X16, offset` 和 `BR X16` 这样形式的跳转,这两种都要跳过。其中 `B` 指令的符号位需要判断,符号位在立即数的最高位。
因为网上的资料很少,所以具体指令可以参阅 ARM64 的架构手册,去 arm 的官网可以下载到。
> ARM® Architecture Reference Manual, ARMv8, for ARMv8-A architecture profile
剩下的就不多说了,下面的具体实现代码里可以看详细操作。
## 主要代码
**注意:此处的代码并非最新,最新代码在开头提到的链接可以找到。**
为了保持简单,我没有用堆,也不管理 hook,所以需要用户自己管理,是放全局变量还是放堆里自己决定,我就提供结构体和几个函数:
```cpp
typedef struct th_info
{
BYTE detour;
void* proc;
#if defined(_CPU_X64) || defined(_CPU_X86)
LONG64 hook_jump;
LONG64 old_entry;
#elif defined(_CPU_ARM64)
long hook_jump;
long old_entry;
#endif
} TH_Info;
#if defined(_CPU_X64) || defined(_CPU_ARM64)
void* TH_GetModulePadding(HMODULE hmodule);
#endif
void TH_Init(TH_Info* info, void* proc, void* fk_proc, void* bridge);
void TH_Hook(TH_Info* info);
void TH_Unhook(TH_Info* info);
void TH_GetDetour(TH_Info* info, void** detour);
```
完整代码实现放这了:
```cpp
#include "tinyhook.h"
#if defined(_CPU_X86) || defined(_CPU_X64)
#include "insn_len.h"
static int GetEntryLen(void* addr)
{
int len = 0;
BYTE* p = (BYTE*)addr;
do {
len += insn_len((void*)(p + len));
} while (len < 5);
return len;
}
static inline void* SkipFF25(void* proc)
{
DWORD offset = *(DWORD*)((BYTE*)proc + 2);
#ifdef _CPU_X64
return *(void**)((BYTE*)proc + offset + 6);
#endif
#ifdef _CPU_X86
return *(void**)offset;
#endif
}
#elif defined(_CPU_ARM64)
#define LDR_X16_NEXT2 0x58000050 // LDR X16,
#define BR_X16 0xD61F0200 // BR X16
#define LONG_JUMP_X16 ((LONG64)BR_X16 << 32 | LDR_X16_NEXT2)
#endif
#if defined(_CPU_X64) || defined(_CPU_ARM64)
void* TH_GetModulePadding(HMODULE hmodule)
{
BYTE* p = (BYTE*)hmodule;
p += ((IMAGE_DOS_HEADER*)p)->e_lfanew + 4; // PE header
p += sizeof(IMAGE_FILE_HEADER) + ((IMAGE_FILE_HEADER*)p)->SizeOfOptionalHeader; // skip optional
int sections = ((IMAGE_FILE_HEADER*)p)->NumberOfSections;
for (int i = 0; i < sections; i++) {
IMAGE_SECTION_HEADER* psec = (IMAGE_SECTION_HEADER*)p;
if (memcmp(psec->Name, ".text", 5) == 0) {
BYTE* offset = (BYTE*)hmodule + psec->VirtualAddress + psec->Misc.VirtualSize;
offset += 16 - (INT_PTR)offset % 16; // align 16
return (void*)offset;
}
p += sizeof(IMAGE_SECTION_HEADER);
}
return NULL;
}
#endif
void TH_Init(TH_Info* info, void* proc, void* fk_proc, void* bridge)
{
info->proc = proc;
#if defined(_CPU_X64) || defined(_CPU_X86)
BYTE hook_jump;
memcpy(hook_jump, proc, 8);
info->old_entry = *(LONG64*)&hook_jump;
#ifdef _CPU_X64
DWORD old_bridge;
BYTE jump_pattern = { 0xFF,0x25,0,0,0,0,0,0,0,0,0,0,0,0 };
*(void**)&jump_pattern = fk_proc;
VirtualProtect(bridge, 14, PAGE_EXECUTE_READWRITE, &old_bridge);
memcpy(bridge, jump_pattern, 14);
VirtualProtect(bridge, 14, old_bridge, &old_bridge);
*(DWORD*)&hook_jump = (DWORD)((char*)bridge - (char*)proc - 5);
#endif
#ifdef _CPU_X86
*(DWORD*)&hook_jump = (char*)fk_proc - (char*)proc - 5;
#endif
hook_jump = 0xE9;
info->hook_jump = *(LONG64*)&hook_jump;
#elif defined(_CPU_ARM64)
DWORD old_bridge;
DWORD hook_jump = 0x14000000;
info->old_entry = *(long*)proc;
VirtualProtect(bridge, 16, PAGE_EXECUTE_READWRITE, &old_bridge);
*(LONG64*)bridge = LONG_JUMP_X16;
*((LONG64*)bridge + 1) = (LONG64)fk_proc;
VirtualProtect(bridge, 16, old_bridge, &old_bridge);
hook_jump |= (long)((long*)bridge - (long*)proc) & 0x3FFFFFF;
info->hook_jump = hook_jump;
#endif
}
void TH_Hook(TH_Info* info)
{
DWORD old;
#if defined(_CPU_X64) || defined(_CPU_X86)
VirtualProtect(info->proc, 8, PAGE_EXECUTE_READWRITE, &old);
InterlockedCompareExchange64((volatile LONG64*)info->proc, info->hook_jump, info->old_entry);
VirtualProtect(info->proc, 8, old, &old);
#elif defined(_CPU_ARM64)
VirtualProtect(info->proc, 4, PAGE_EXECUTE_READWRITE, &old);
InterlockedCompareExchange((volatile long*)info->proc, info->hook_jump, info->old_entry);
VirtualProtect(info->proc, 4, old, &old);
#endif
}
void TH_Unhook(TH_Info* info)
{
DWORD old;
#if defined(_CPU_X64) || defined(_CPU_X86)
VirtualProtect(info->proc, 8, PAGE_EXECUTE_READWRITE, &old);
InterlockedCompareExchange64((volatile LONG64*)info->proc, info->old_entry, info->hook_jump);
VirtualProtect(info->proc, 8, old, &old);
#elif defined(_CPU_ARM64)
VirtualProtect(info->proc, 4, PAGE_EXECUTE_READWRITE, &old);
InterlockedCompareExchange((volatile long*)info->proc, info->old_entry, info->hook_jump);
VirtualProtect(info->proc, 4, old, &old);
#endif
}
void TH_GetDetour(TH_Info* info, void** detour)
{
DWORD old;
VirtualProtect(info->detour, 32, PAGE_EXECUTE_READWRITE, &old);
#if defined(_CPU_X64) || defined(_CPU_X86)
int entry_len;
void* detour_to;
WORD* pentry = info->proc;
if (*pentry == 0x25FF) {
entry_len = 0;
detour_to = SkipFF25(pentry);
}
else {
entry_len = GetEntryLen(info->proc);
memcpy(info->detour, info->proc, entry_len);
detour_to = (char*)info->proc + entry_len;
}
#ifdef _CPU_X64
BYTE jump_pattern = { 0xFF,0x25,0,0,0,0,0,0,0,0,0,0,0,0 };
*(void**)&jump_pattern = detour_to;
memcpy(&info->detour, jump_pattern, 14);
#endif
#ifdef _CPU_X86
BYTE jump_pattern = { 0xE9,0,0,0,0 };
*(DWORD*)&jump_pattern = (char*)detour_to - (char*)&info->detour - entry_len - 5;
memcpy(&info->detour, jump_pattern, 5);
#endif
#elif defined(_CPU_ARM64)
DWORD* insn = (DWORD*)info->proc;
void* detour_to = insn + 1;
DWORD* pdetour = (DWORD*)info->detour;
if (*insn >> 26 == 5) { // B imm with +- 128MB offset
int diff = *insn & 0x3FFFFFF;
if (diff & 0x2000000) // check negative
diff |= 0xFC000000;
detour_to = insn + diff;
}
else if ((*insn & 0x9F000000) == 0x90000000) { // ADRP Xn, PC+imm with +- 4GB offset
DWORD imm = ((*insn >> 29) & 3) | ((*insn >> 3) & 0xFFFFC);
void* addr = (void*)(((LONG64)insn & 0xFFFFFFFFFFFFF000) + ((LONG64)imm << 12));
if (insn >> 22 == 0x3E5) { // LDR Xm,
int pimm = ((insn >> 10) & 0xFFF) * 8;
detour_to = *(void**)((BYTE*)addr + pimm);
}
// not sure with this branch
else if (insn >> 22 == 0x284) { // ADD Xm, Xn, #imm12
int imm12 = (insn >> 10) & 0xFFF;
detour_to = (BYTE*)addr + imm12;
}
}
if (detour_to == insn + 1) { // if not skip any branch
*pdetour = *insn;
pdetour++;
}
*(LONG64*)pdetour = LONG_JUMP_X16;
*(void**)(pdetour + 2) = detour_to;
#endif
*detour = (void*)info->detour;
}
```
使用方法也贴一下:
```cpp
#include <Windows.h>
#include <stdio.h>
#include "tinyhook.h"
HWND(WINAPI *dt_GetForegroundWindow) (void);
int (WINAPI *dt_MessageBoxA) (HWND, LPCSTR, LPCSTR, UINT);
HWND WINAPI fk_GetForegroundWindow()
{
printf("Change %d to %d\n", (int)dt_GetForegroundWindow(), 123456);
return (HWND)123456;
}
int WINAPI fk_MessageBoxA(HWND hwnd, LPCSTR text, LPCSTR title, UINT flags)
{
printf("%s: %s\n", title, text);
return dt_MessageBoxA(hwnd, "Hooked!", "ERROR", MB_ICONERROR | MB_TOPMOST);
}
TH_Info hook_gfw;
TH_Info hook_mba;
int main()
{
int i = 0;
HMODULE h_user32 = GetModuleHandleA("user32.dll");
#ifdef _CPU_X86
// x86不用管bridge
TH_Init(&hook_gfw, GetProcAddress(h_user32, "GetForegroundWindow"), fk_GetForegroundWindow, NULL);
TH_Init(&hook_mba, GetProcAddress(h_user32, "MessageBoxA"), fk_MessageBoxA, NULL);
#else
char* padding = TH_GetModulePadding(h_user32);
// 注意两次bridge参数的区别
TH_Init(&hook_gfw, GetProcAddress(h_user32, "GetForegroundWindow"), fk_GetForegroundWindow, padding);
TH_Init(&hook_mba, GetProcAddress(h_user32, "MessageBoxA"), fk_MessageBoxA, padding + 16);
#endif
TH_GetDetour(&hook_gfw, (void**)&dt_GetForegroundWindow);
TH_GetDetour(&hook_mba, (void**)&dt_MessageBoxA);
TH_Hook(&hook_gfw);
printf("Fake GetForegroundWindow: %d\n", (int)GetForegroundWindow());
TH_Unhook(&hook_gfw);
printf("Real GetForegroundWindow: %d\n", (int)GetForegroundWindow());
TH_Hook(&hook_mba);
MessageBoxA(NULL, "Hello, World!", "Title", 0);
TH_Unhook(&hook_mba);
MessageBoxA(NULL, "Not hooked.", "hmm", 0);
}
```
## 偷懒之自动找内存
后来我想,反正运行速度挺快,搞一个自动 hook 也不会有多少性能影响,于是就加了一个 `TH_LazyInit` 函数,把 `TH_Init` 和 `TH_GetDetour` 的功能合并了,虽说设计搜索操作,但是也不会有多大的性能影响,毕竟用户写起来简单才是好的。
其具体代码是:
```cpp
static inline void* FindModuleBase(void* proc)
{
BYTE* p = (BYTE*)((INT_PTR)proc & 0xFFFFFFFFFFFF0000);
while (p != 'M' && p != 'Z')
p -= 0x10000;
return p;
}
void TH_LazyInit(TH_Info* info, void* proc, void* fk_proc, void** detour)
{
#if defined(_CPU_X86)
TH_Init(info, proc, fk_proc, NULL);
TH_GetDetour(info, detour);
#elif defined(_CPU_X64) || defined(_CPU_ARM64)
LONG64* padding = TH_GetModulePadding(FindModuleBase(proc));
while (padding != 0 || padding != 0)
padding += 2;
TH_Init(info, proc, fk_proc, padding);
TH_GetDetour(info, detour);
#endif
}
```
就是先根据被 hook 函数的地址去找模块的基址,注意模块是按照 64K 对齐的,逐渐往上找就能找到。然后再去找模块代码段的空白,注意不能覆盖了之前的,所以要进行判断。
当然这些代码都不太安全,毕竟没有判断内存是否可读,以及模块是否存在等情况,主要原因是按照正常的操作来,通常是不会遇到这些问题的(~~其实很大一部分原因是偷懒,还有就是保持代码简单~~)。
这样,只要一句就完成了初始化,顺便拿到了调用原始函数的指针:
```cpp
TH_LazyInit(&hook_xxx, GetProcAddress(h_xxx, "xxx"), fk_xxx, (void**)&dt_xxx);
```
## 最后
吐槽一下 Windows 的 bug,转译运行 x64 代码的时候第一次运行有 bug 导致 hook 失败,让我以为是 gcc 的优化的 bug,实在是可恶啊,浪费了半天。主要是 vs 跑 x64 的没有 hook 失败的问题,我也搞不懂。
更新:ARM64 已经实现,接下来就没啥新的能搞了,等着修 bug 吧。
更新:新加了一个更方便使用的初始化函数,顺便修了点 bug。 感谢分享 期待arm的inline hook 感谢分享 非常感谢楼主的分享 捞一下,ARM64下的hook已经实现了。 跟大佬学些学习最新的知识 大佬NB啊,必须收藏学习。
页:
[1]