乐于分享
好东西不私藏

看完SandHook源码,我发现了Inline Hook的隐藏逻辑

看完SandHook源码,我发现了Inline Hook的隐藏逻辑

大多数人对Inline Hook的理解,止步于"改函数开头几条指令"。但真正看SandHook的源码,你会发现它做的事远不止于此——它要解决的问题是:怎么在改指令的同时,保证整个调用链不出问题。

为什么要看SandHook

SandHook是个很特殊的存在。它既能Hook Java层方法,也能Hook Native层函数,而且走的是纯Inline Hook路线。相比Xposed那种修改ArtMethod入口点的方式,Inline Hook更底层,也更接近"真正的"函数拦截。

但Inline Hook有一个致命问题:改的是原始机器码。一旦出问题,轻则崩溃,重则死循环。看懂SandHook怎么绕开这些坑,才算真正理解Inline Hook。

入口点修改的三重门

SandHook的Native Hook部分,核心代码在 trampoline_manager.cpp 里。Hook一个函数要过三道关:

第一关:内存属性

代码段在Android里是只读的。想改指令?先得调用mprotect把目标地址改成可写:

// 原始代码(简化)
void* getMemoryBlock(void* target) {
    // 计算页大小对齐的地址
    uintptr_t page_start = (uintptr_t)target & ~(PAGE_SIZE - 1);
    // 修改内存属性为可读可写可执行
    mprotect((void*)page_start, PAGE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC);
    return page_start;
}

这一步看似简单,但很多Inline Hook实现就栽在这里。PAGE_SIZE对齐、权限组合、恢复属性,缺一不可。

第二关:指令备份

改指令之前,必须把原始的几条指令先复制出来,否则Hook之后就没法调用原函数了:

// 典型的指令备份
void backupInstructions(void* target, uint8_t* backup_area, size_t len) {
    // 复制目标地址的原始指令
    memcpy(backup_area, target, len);
    // 在备份区域的末尾添加跳转指令,回到原函数继续执行
    // 这里是关键:跳回原函数+被覆盖的指令长度
}

备份多少条指令?ARM64下通常是12-16字节,刚好够放一条跳转指令。这个数字怎么定的?要看目标函数的指令分布——够放跳转指令就行。

第三关:Trampoline生成

这是最核心的部分。SandHook会在一块新分配的内存里生成蹦床代码:

// 蹦床的结构示意
struct Trampoline {
    // 第一块:原始指令(被覆盖的那几条)
    uint8_t original_instructions[16];
    // 第二块:跳回原函数继续执行
    uint8_t jump_back[12];  // b original_addr + offset
    // 实际实现会更复杂,需要处理寄存器恢复
};

为什么别人Hook容易崩,SandHook相对稳

Inline Hook崩溃的主要原因就几个:指令对齐、寄存器冲突、跨模块跳转。

SandHook的解决方案很直接:

针对指令对齐问题:它会扫描目标函数开头,识别出完整的指令边界。ARM64的指令有16位和32位两种,混在一起如果截错位置,CPU会解析出乱码。SandHook会找到一条"安全切割线",确保备份的指令是完整的。

针对寄存器冲突:Hook函数和原函数用的是同一套寄存器。SandHook在蹦床里会先保存寄存器状态,Hook函数执行完再恢复。这就像给两个交接班的工人各准备一套工具箱,不会混用。

针对跨模块跳转:蹦床跳回原函数时,用的是相对跳转。如果跳的目标太远(超过128MB),普通的b指令就够不着了。SandHook会改用ldr+jump组合,绕过这个限制。

实际Hook长什么样

看一个简化的Hook流程:

// 1. 找到要Hook的函数地址
void* target = dlsym(handle, "target_function");

// 2. 分配蹦床内存(可执行)
void* trampoline = allocateExecutableMemory();

// 3. 备份原始指令
backupInstructions(target, trampoline, 16);

// 4. 在目标函数开头写入跳转
writeJumpInstruction(target, hook_function);

// 5. 蹦床末尾跳回原函数(跳过被覆盖的16字节)
writeJumpBack(trampoline + 16, target + 16);

这五步走完,每次调用target_function,实际执行的都是hook_function。如果Hook函数想调用原逻辑,跳到蹦床地址就行。

高版本Android的额外挑战

从Android 8.0开始,Google对内存属性做了更严格的限制。代码段默认是不可写的,即使你调用了mprotect也可能被SELinux拦截。

SandHook的解法是:不碰原始代码段,而是把整个Hook逻辑搬到别的地方执行。它用一块单独的内存区域(RWX权限)来做蹦床,这样既不违反内存属性,也避免触发系统的安全机制。

技术要点速记

  • Inline Hook的本质是修改函数的机器码开头
  • 三步走:改内存属性、备份指令、生成蹦床
  • 蹦床内存需要可执行权限,通常用mmap分配
  • ARM64下要处理指令对齐问题,否则会解析出乱码
  • 高版本Android需要绕过SELinux限制,不能直接改代码段
  • Hook稳定性的关键在于寄存器保护和跨模块跳转的处理

搞懂SandHook的实现,再去看其他Hook框架源码,会发现大家解决的问题都差不多,区别只在于谁的处理更细致。

本文由「老小子」整理编辑,转载需注明出处