Android中的Hook-InlineHook
为什么需要 Inline Hook
本文定位:面向 Android 开发者的 Inline Hook 原理入门,重点是理解“为什么要用、怎么工作、如何验证生效”。
文章以
arm64+ Demo 讲解,不展开完整线上化实现(如全量指令重定位、复杂并发场景、全机型兼容适配)。
在 Android中的Hook-PLTHook 里,PLT Hook 的切入点是“导入方模块的 GOT 表项”。
这意味着它拦截的是调用路径,不是函数本体。
与 PLT Hook 的边界差异
PLT Hook 生效的前提是:目标调用必须经过动态链接导入表(PLT/GOT)。因此:
- 能拦截:跨 so 的外部符号调用。
- 常拦不到:同一 so 内部直接调用、
static函数、被内联后不存在的调用点。 - 可能失效:
-Bsymbolic、LTO、符号裁剪、符号版本不匹配等场景。
Inline Hook 的切入点是“目标函数入口指令”。
只要执行流进入该函数入口,就会先跳转到 Hook 函数,因此覆盖范围通常更大。
典型适用场景(为什么必须用 Inline)
- 目标函数是 so 内部实现,不经过导入表,PLT Hook 没有命中点。
- 需要拦截库内高频逻辑(编解码、渲染、加解密等)做行为观测。
- 需要在不改业务源码的前提下注入逻辑、定位问题或灰度防护。
- 需要保留原函数能力,在 Hook 函数中按条件回调原逻辑。
一句话:PLT Hook 改“调用入口”,Inline Hook 改“函数入口”。
Inline Hook 核心原理
从 Android 开发者角度,Inline Hook 可以抽象成三个动作:改写入口、搭建跳板、回跳原流程。先理解这条主链路,不必一开始深入到汇编位级细节。
1. 函数入口改写(Patch Prologue)
在目标函数起始地址覆盖若干条机器指令,写入“跳转到 Hook 函数”的指令序列。
改写后,任何进入该函数入口的执行流都会先进入 Hook 函数。
关键点:
- 覆盖长度必须满足最小跳转需求,并按指令边界覆盖(AArch64 固定 4 字节)。
- 不能覆盖半条指令,否则通常直接崩溃。
2. 跳板函数(Trampoline)
被覆盖的原始指令不能丢弃,否则“调用原函数”会断链。
通常会申请一段可执行内存作为 trampoline,写入:
1) 被覆盖的原始指令(必要时重定位修复);
2) 一条回跳指令,跳回 target + patched_len 继续执行。
这样 Hook 函数就可以通过 trampoline 调用原函数剩余逻辑。
3. 指令重定位(概念了解)
Inline Hook 最难的不是“写跳转”,而是“搬指令”。
如果被搬走的指令包含 PC 相对寻址(如 ADR/ADRP/LDR literal/B/BL),直接复制到 trampoline 会因为地址变化而语义错误。
入门阶段先记住结论:实现里通常要做“指令重定位”,把这类指令在 trampoline 中改写成等价逻辑,保证地址语义不变。
4. 内存权限与缓存一致性
代码段通常是只读可执行(RX),写入前后要处理权限和缓存:
- 页对齐后用
mprotect临时把代码页改为可写(通常会保持可执行权限); - 写入补丁指令;
- 刷新 I-Cache(如
__builtin___clear_cache),确保 CPU 能执行到新指令; - 恢复为只读可执行(通常
RX)。
否则会出现“写入成功但仍执行旧指令”或权限异常。
5. 调用链闭环
一个完整调用链通常是:
caller -> target(入口已改写) -> hook -> (可选) trampoline -> target+N -> return
如果继续做工程化,通常还会补这些能力:
- 防递归(TLS guard),避免 Hook 内再次命中自己;
- 线程安全(安装/卸载加锁);
- 可回滚(保存原始字节,支持 unhook)。
ARCH_ARM64(arm64-v8a / AArch64)最小知识集
在 Android 上,ARCH_ARM64 对应 ABI arm64-v8a,执行状态是 AArch64。
Inline Hook 在该架构下的实现与 ARMv7(A32/Thumb) 有明显差异,必须分开理解。
这一节只保留和 Android 开发面试/排障最相关的概念。
1. AArch64 基础模型
执行状态与 ABI
arm64-v8a是 Android NDK 的 64 位 ABI,对应 CPU 运行在AArch64状态,执行A64指令。armeabi-v7a是 32 位 ABI(A32/Thumb),Inline Hook 的很多细节不可直接复用。- 工程里常用
__aarch64__/__arm__做编译分支(有些实现会抽象成ARCH_ARM64宏):
1 | |
寄存器与调用约定(AAPCS64)
- 通用寄存器:
X0 ~ X30(W0 ~ W30是低 32 位视图)。 X0 ~ X7:参数与返回值(返回通常在X0)。X30:链接寄存器(LR,保存返回地址),SP需要 16 字节对齐。
对 Hook 的意义:保存/恢复现场、构造 trampoline、回调原函数都依赖这些约定。
2. 指令集差异(A64 vs A32/Thumb)
| 维度 | ARM64 (A64) | ARMv7 (A32/Thumb) | 对 Inline Hook 的影响 |
|---|---|---|---|
| 指令长度 | 固定 4 字节 | Thumb 2/4 字节混合 | ARM64 更容易按指令边界覆盖入口 |
| 状态位 | 无 Thumb | 需要区分 ARM/Thumb(函数地址 bit0 常携带状态) | ARM64 不需要处理 Thumb bit |
| PC 相对指令 | ADR/ADRP/LDR literal/B/BL 很常见 |
编码与语义不同 | trampoline 的重定位逻辑不能复用 ARM32 |
3. ARM64 中与 Hook 最相关的指令
Inline Hook 里常见且需要优先记住的就是下面三类:
B/BL:分支/调用指令,PC 相对跳转,范围约±128MBADR/ADRP:生成地址(常用于取全局/常量地址),属于 PC 相对寻址LDR (literal):从常量池取值/地址,属于 PC 相对寻址
其它如 B.cond / CBZ / TBZ 等本质也是“短距 PC-relative 分支”,搬运到 trampoline 时同样需要重定位处理。
这些指令一旦被“搬到 trampoline”,若不重算偏移,通常会直接跑偏。
4. 指令差异对 Inline Hook 的直接影响
入口改写(Patch Prologue)
ARM64 固定 4 字节指令,让入口覆盖更可控,但仍要满足:
patch_len必须是 4 的倍数;- 必须覆盖完整跳转模板;
- 不能覆盖半条指令。
近跳与远跳策略
- 目标地址在
±128MB内,可用单条B(4B)近跳; - 超出范围通常使用“寄存器间接跳转”模板(常见 16B 或 20B)。
示意(远跳):
1 | |
Trampoline 不是“简单复制”
被覆盖指令若包含 ADR/ADRP/LDR literal/B/BL/...,直接复制会失效。
正确做法是:按指令类型重定位,保证在新地址执行时仍指向原目标。
32 位时代常见坑在 ARM64 的变化
- ARM32/Thumb 常见“bit0 表示 Thumb 状态”,ARM64 不存在该问题;
- 但 ARM64 对 PC-relative 指令使用更频繁,重定位工作量反而更集中在“偏移修复”。
5. 进阶注意点(可选展开)
以下内容偏工程化,入门阶段可以先略过:
- Hook 框架通常会处理更多边界(并发安装、递归保护、cache flush、回滚);
- 自研实现建议先收敛
arm64 + 最小指令集子集,再扩展到复杂指令重定位。
InlineHook基础流程(原理向)
基础可运行的 Inline Hook,可以压缩为 5 步:
- 准备输入:拿到
target_addr、hook_addr,并确定入口覆盖长度hook_len。 - 构建 trampoline:备份目标入口指令,复制到 trampoline,末尾追加跳回
target + hook_len。 - 刷新 trampoline 缓存:对 trampoline 写入区执行指令缓存刷新。
- 改写目标入口:临时放开页写权限,写入跳到 hook 的指令,并刷新目标入口缓存。
- 恢复与收尾:恢复页权限并返回结果;失败时回滚原始字节并释放 trampoline。
InlineHook开发实践(Demo 验证)
Demo 结构(按模块理解)
- App 层:加载目标 so 与 hook so,并触发待 Hook 函数调用。
- 目标 so:提供待 Hook 的 native 函数(建议
noinline,避免被编译器优化掉调用点)。 - Hook so:安装 Inline Hook,保存原函数地址,并在 Hook 函数里按需回调原函数。
- 日志层:统一输出安装结果、旧/新地址、调用前后返回值,便于定位问题。
最小调用链
Java/Kotlin -> JNI wrapper -> target_func(入口已改写) -> hook_func -> (可选) trampoline/original -> target+N -> return
验证清单
- Hook 前调用一次,记录原始返回值与日志。
- 安装 Hook 后再次调用,确认返回值或行为发生预期变化。
- 在 Hook 函数中回调原函数,确认原始逻辑仍可走通。
- 失败时输出
errno、目标地址、trampoline 地址,优先排查权限/时机/重定位。
Android 开发者该掌握到什么程度
- 必会:说清
PLT Hook与Inline Hook的边界,以及何时必须用 Inline。 - 实战:能看懂调用链,知道怎么验证“已生效 + 可回调原函数 + 可回滚”。
- 加分:理解 ARM64 的固定 4 字节指令与 PC-relative 重定位为何会导致搬指令风险。
- 进阶:指令重编码、跨架构适配、并发安全等实现细节,优先通过成熟 Hook 框架落地。
这篇文章目标主要覆盖“必会 + 实战”层级。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!