首页
社区
课程
招聘
[原创]游戏逆向中常用的Hook技术
发表于: 6小时前 140

[原创]游戏逆向中常用的Hook技术

6小时前
140

依旧是复习过去学过的知识。

本文的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),通知编译器不要内联这个函数。

#include <windows.h>
#include <stdio.h>

// --- 被 HOOK 的目标函数 ---

__declspec(noinline) void OriginalHelloWorldFunction() {
    printf("[执行] 原始的 Hello World 函数\n");
}

// --- 我们自定义的钩子函数 (Hook 函数) ---
void MyCustomHelloWorldFunction()
{
    printf("[拦截] 成功进入了我们的钩子函数!\n");
}

// --- 核心:执行 Inline Hook 的逻辑 ---
void SetupInlineHookJmp32()
{
    // 1. 定义跳转机器码结构 (E9 + 4字节偏移量)
    // 机器码格式:E9 XX XX XX XX
    BYTE JumpInstruction[5] = { 0xE9, 0, 0, 0, 0 };

    // 2. 计算跳转偏移量 (公式:目标地址 - 原地址 - 指令长度)
    // 注意:跳转是相对于当前指令下一条地址开始计算的
    DWORD RelativeOffset = (DWORD)MyCustomHelloWorldFunction - (DWORD)OriginalHelloWorldFunction - 5;

    // 将计算好的 4 字节偏移填充到机器码中
    *(DWORD*)(JumpInstruction + 1) = RelativeOffset;

    // 3. 修改目标内存属性为“可读写执行”,否则修改代码会引发崩溃 (Access Violation)
    DWORD OldMemoryProtection;
    VirtualProtect(OriginalHelloWorldFunction, 5, PAGE_EXECUTE_READWRITE, &OldMemoryProtection);

    // 4. 正式写入机器码 (覆盖原函数开头的 5 个字节)
    memcpy(OriginalHelloWorldFunction, JumpInstruction, 5);

    // 5. 恢复内存原始保护属性 (养成良好的安全习惯)
    VirtualProtect(OriginalHelloWorldFunction, 5, OldMemoryProtection, &OldMemoryProtection);

    printf("[系统] Inline Hook 已部署完毕。\n");
}

int main()
{
    printf("====== 32位 Inline Hook 测试开始 ======\n\n");

    // 第一步:测试 Hook 之前的函数行为
    printf("1. 尝试直接调用函数(此时未 Hook):\n");
    // 注意:这里如果先调用,可能会被编译器内联,测试建议直接开始 Hook
    OriginalHelloWorldFunction();
    // 第二步:部署 Hook
    SetupInlineHookJmp32();

    // 第三步:再次调用原函数名,观察输出
    printf("\n2. 再次尝试调用原函数:\n");
    OriginalHelloWorldFunction();

    printf("\n====== 测试结束 ======\n");
    getchar(); // 暂停程序查看结果
    return 0;
}

x64示例代码:

#include <windows.h>
#include <stdio.h>

// 告诉编译器不要内联这些函数
__declspec(noinline) void OriginalHelloWorldFunction()
{
    printf("[执行] 原始的 x64 Hello World 函数\n");
}

void MyCustomHelloWorldFunction()
{
    printf("[拦截] 成功进入了 x64 钩子函数!\n");
}

void SetupInlineHookX64()
{
    // 1. 定义 12 字节的绝对跳转指令
    // 48 B8 [8字节地址] FF E0 (jmp rax)
    BYTE JumpInstruction[12] = { 
        0x48, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, // mov rax, <address>
        0xFF, 0xE0                         // jmp rax
    };

    // 2. 将 64 位绝对地址写入机器码
    void* TargetAddress = &MyCustomHelloWorldFunction;
    memcpy(&JumpInstruction[2], &TargetAddress, 8);

    // 3. 修改内存属性(注意:这次需要 12 字节空间)
    DWORD OldMemoryProtection;
    if (!VirtualProtect((LPVOID)OriginalHelloWorldFunction, 12, PAGE_EXECUTE_READWRITE, &OldMemoryProtection)) {
        printf("[错误] 内存权限修改失败!\n");
        return;
    }

    // 4. 写入 Hook
    memcpy((LPVOID)OriginalHelloWorldFunction, JumpInstruction, 12);

    // 5. 还原属性
    VirtualProtect((LPVOID)OriginalHelloWorldFunction, 12, OldMemoryProtection, &OldMemoryProtection);

    printf("[系统] x64 绝对跳转 Hook 已部署完毕。\n");
}

int main()
{
    printf("====== 64位 Inline Hook 测试开始 ======\n\n");

    printf("1. 尝试直接调用函数:\n");
    OriginalHelloWorldFunction();

    SetupInlineHookX64();

    printf("\n2. 再次尝试调用原函数:\n");
    OriginalHelloWorldFunction();

    printf("\n====== 测试结束 ======\n");
    system("pause");
    return 0;
}

(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





参考视频: c0cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2T1K9h3I4A6j5X3W2D9K9g2)9J5k6h3y4G2L8g2)9J5c8Y4k6A6k6r3g2G2i4K6u0r3b7W2j5I4h3U0S2r3M7r3g2p5c8i4g2W2i4K6u0r3i4K6y4r3M7$3S2S2M7X3g2Q4y4h3k6K6L8%4g2J5j5$3g2Q4x3@1c8U0L8%4m8&6i4K6g2X3N6$3g2T1i4K6t1$3j5h3#2H3i4K6y4n7N6X3c8Q4y4h3k6K6L8%4g2J5j5$3g2Q4x3@1b7&6x3r3u0U0y4e0W2S2y4X3q4X3y4U0V1#2x3o6y4U0k6h3b7J5k6U0l9$3z5o6j5&6k6X3g2V1x3X3j5I4j5b7`.`.


[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回