吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2400|回复: 8
收起左侧

[C&C++ 原创] tinyhook,一个非常简单的inline hook框架

[复制链接]
DEATHTOUCH 发表于 2024-1-27 20:00
本帖最后由 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 的大小,所以必须使用二次跳转的方式实现。

方法有好多种,具体如下:

; 利用rax跳转,改变寄存器
mov rax, address
jmp rax

; 借用 rax,使用 ret 跳转,不改变寄存器
push rax
mov rax, address
xchg rax, [rsp]
ret

; 压栈,使用 ret 跳转,不改变寄存器
push address.low
mov dword [rsp+4], address.high
ret

; 使用 FF 25 跳转,不改变寄存器
jmp [rip+0]  ; FF 25 00 00 00 00
db address

后面几种不改变寄存器的方案都可以,最简单的就是最后一种,我后来就改了这种(之前是倒数第二种)。

不过问题又来了,如果把一个函数头的 14 个字节改了,肯定非常容易出问题,最好是少改点,只用 5 字节先跳走,再搭个桥跳到指定的地方。当然甚至可以只改 2 字节,用一句短跳找到最近 128 字节范围内的 5 或 14 字节空地,不过这样就挺麻烦了。

然后问题是桥怎么搭,或者是 14 字节的空地怎么找。我的答案是让用户自己找吧,额,不,当然不是了。至少我是提供了一个函数给用户来找的:

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 对齐的,和文件不一样,所以一定是存在空地的,具体就是通过 VirtualAddressVirtualSize 来计算。然后对齐一下 16 字节就 OK 了。至于这片区域怎么管理,交给用户去吧!

当然如果要其他操作的话,那真的就自己找内存去中转吧。

然后就是自动 hook 的关键之找指令长度,这个具体代码由开头的 GitHub 仓库提供,我简单封装了一下下:

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, addrBR X16 的组合实现。其中的 LDR 指令相对 PC 寻址,地址占 8 字节,紧跟这两句指令,有点像 x86 里面的 FF 25 指令的逻辑。这样算下来需要 16 字节进行一次长跳转。

和 x64 一样,有些函数导入表格式的函数头部可能有两种情况,一种是 B offset 的指令,128MB 的寻址范围,另一种是以 ADRP X16, offsetLDR X16, offsetBR X16 这样形式的跳转,这两种都要跳过。其中 B 指令的符号位需要判断,符号位在立即数的最高位。

因为网上的资料很少,所以具体指令可以参阅 ARM64 的架构手册,去 arm 的官网可以下载到。

ARM® Architecture Reference Manual, ARMv8, for ARMv8-A architecture profile

剩下的就不多说了,下面的具体实现代码里可以看详细操作。

主要代码

注意:此处的代码并非最新,最新代码在开头提到的链接可以找到。

为了保持简单,我没有用堆,也不管理 hook,所以需要用户自己管理,是放全局变量还是放堆里自己决定,我就提供结构体和几个函数:

typedef struct th_info
{
    BYTE detour[32];
    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);

完整代码实现放这了:

#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, [PC + #8]
#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[8];
    memcpy(hook_jump, proc, 8);
    info->old_entry = *(LONG64*)&hook_jump;
#ifdef _CPU_X64
    DWORD old_bridge;
    BYTE jump_pattern[14] = { 0xFF,0x25,0,0,0,0,0,0,0,0,0,0,0,0 };
    *(void**)&jump_pattern[6] = 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[1] = (DWORD)((char*)bridge - (char*)proc - 5);
#endif
#ifdef _CPU_X86
    *(DWORD*)&hook_jump[1] = (char*)fk_proc - (char*)proc - 5;
#endif
    hook_jump[0] = 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[14] = { 0xFF,0x25,0,0,0,0,0,0,0,0,0,0,0,0 };
    *(void**)&jump_pattern[6] = detour_to;
    memcpy(&info->detour[entry_len], jump_pattern, 14);
#endif
#ifdef _CPU_X86
    BYTE jump_pattern[5] = { 0xE9,0,0,0,0 };
    *(DWORD*)&jump_pattern[1] = (char*)detour_to - (char*)&info->detour - entry_len - 5;
    memcpy(&info->detour[entry_len], 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[1] >> 22 == 0x3E5) { // LDR Xm, [Xn, #pimm]
            int pimm = ((insn[1] >> 10) & 0xFFF) * 8;
            detour_to = *(void**)((BYTE*)addr + pimm);
        }
        // not sure with this branch
        else if (insn[1] >> 22 == 0x284) { // ADD Xm, Xn, #imm12
            int imm12 = (insn[1] >> 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;
}

使用方法也贴一下:

#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_InitTH_GetDetour 的功能合并了,虽说设计搜索操作,但是也不会有多大的性能影响,毕竟用户写起来简单才是好的。

其具体代码是:

static inline void* FindModuleBase(void* proc)
{
    BYTE* p = (BYTE*)((INT_PTR)proc & 0xFFFFFFFFFFFF0000);
    while (p[0] != 'M' && p[1] != '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] != 0 || padding[1] != 0)
        padding += 2;
    TH_Init(info, proc, fk_proc, padding);
    TH_GetDetour(info, detour);
#endif
}

就是先根据被 hook 函数的地址去找模块的基址,注意模块是按照 64K 对齐的,逐渐往上找就能找到。然后再去找模块代码段的空白,注意不能覆盖了之前的,所以要进行判断。

当然这些代码都不太安全,毕竟没有判断内存是否可读,以及模块是否存在等情况,主要原因是按照正常的操作来,通常是不会遇到这些问题的(其实很大一部分原因是偷懒,还有就是保持代码简单)。

这样,只要一句就完成了初始化,顺便拿到了调用原始函数的指针:

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。

免费评分

参与人数 9吾爱币 +16 热心值 +9 收起 理由
beatone + 1 热心回复!
kihlh + 1 + 1 我很赞同!
langyoChina + 1 + 1 我很赞同!
苏紫方璇 + 3 + 1 抽空学习一下
wushaominkk + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
kelvar + 1 + 1 谢谢@Thanks!
deice + 1 + 1 我很赞同!
wanfon + 1 + 1 热心回复!
朱朱你堕落了 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

hszt 发表于 2024-1-27 20:57
感谢分享
头像被屏蔽
moruye 发表于 2024-1-27 21:21
鸭子咯咯哒~ 发表于 2024-1-28 22:56
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啊,必须收藏学习。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-24 17:37

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表