大多数人对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框架源码,会发现大家解决的问题都差不多,区别只在于谁的处理更细致。
本文由「老小子」整理编辑,转载需注明出处
夜雨聆风