DEATHTOUCH 发表于 2024-1-27 20:00

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。

hszt 发表于 2024-1-27 20:57

感谢分享

moruye 发表于 2024-1-27 21:21

鸭子咯咯哒~ 发表于 2024-1-28 22:56

期待arm的inline hook

nojon 发表于 2024-1-28 23:31

感谢分享

turmasi1234 发表于 2024-1-30 07:39

非常感谢楼主的分享

DEATHTOUCH 发表于 2024-1-30 21:40

捞一下,ARM64下的hook已经实现了。

蔚为大观2008 发表于 2024-2-2 13:21

跟大佬学些学习最新的知识

满不懂 发表于 2024-3-31 11:04

大佬NB啊,必须收藏学习。
页: [1]
查看完整版本: tinyhook,一个非常简单的inline hook框架