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)

  1. 目标函数是 so 内部实现,不经过导入表,PLT Hook 没有命中点。
  2. 需要拦截库内高频逻辑(编解码、渲染、加解密等)做行为观测。
  3. 需要在不改业务源码的前提下注入逻辑、定位问题或灰度防护。
  4. 需要保留原函数能力,在 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),写入前后要处理权限和缓存:

  1. 页对齐后用 mprotect 临时把代码页改为可写(通常会保持可执行权限);
  2. 写入补丁指令;
  3. 刷新 I-Cache(如 __builtin___clear_cache),确保 CPU 能执行到新指令;
  4. 恢复为只读可执行(通常 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
2
3
4
5
#if defined(__aarch64__)
// ARM64 inline hook path
#elif defined(__arm__)
// ARM32/Thumb inline hook path
#endif

寄存器与调用约定(AAPCS64)

  • 通用寄存器:X0 ~ X30W0 ~ 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 相对跳转,范围约 ±128MB
  • ADR / 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
2
3
LDR X17, [PC, #8]
BR X17
.quad hook_addr

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 步:

  1. 准备输入:拿到 target_addrhook_addr,并确定入口覆盖长度 hook_len
  2. 构建 trampoline:备份目标入口指令,复制到 trampoline,末尾追加跳回 target + hook_len
  3. 刷新 trampoline 缓存:对 trampoline 写入区执行指令缓存刷新。
  4. 改写目标入口:临时放开页写权限,写入跳到 hook 的指令,并刷新目标入口缓存。
  5. 恢复与收尾:恢复页权限并返回结果;失败时回滚原始字节并释放 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

验证清单

  1. Hook 前调用一次,记录原始返回值与日志。
  2. 安装 Hook 后再次调用,确认返回值或行为发生预期变化。
  3. 在 Hook 函数中回调原函数,确认原始逻辑仍可走通。
  4. 失败时输出 errno、目标地址、trampoline 地址,优先排查权限/时机/重定位。

Android 开发者该掌握到什么程度

  • 必会:说清 PLT HookInline Hook 的边界,以及何时必须用 Inline。
  • 实战:能看懂调用链,知道怎么验证“已生效 + 可回调原函数 + 可回滚”。
  • 加分:理解 ARM64 的固定 4 字节指令与 PC-relative 重定位为何会导致搬指令风险。
  • 进阶:指令重编码、跨架构适配、并发安全等实现细节,优先通过成熟 Hook 框架落地。

这篇文章目标主要覆盖“必会 + 实战”层级。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!