安卓 - 基于异常的无痕Hook
言简意赅, 源代码地址: https://gist.github.com/fuqiuluo/c7a6468a6fcc056c51f874c34aab717e
核心原理
ARM64 页表项有一个 UXN(Unprivileged eXecute Never)位。将目标代码页的 PTE 设置 UXN=1 后,用户态执行该页任何指令都会触发 Instruction Abort(EC=0x20, DFSC=Permission Fault),陷入 EL1。
内核侧通过 inline hook do_mem_abort() 注册自定义 handler,在异常到达信号投递之前拦截。handler 检查 fault 地址是否属于已注册的断点页,是则修改 pt_regs->pc 重定向到预编译的 recomp(重编译)页,返回用户态后 CPU 从recomp 页继续执行。整个过程对用户态透明:没有 ptrace、没有 BRK 指令、没有信号投递。
重编译引擎
对目标页的全部 1024 条指令(4KB / 4 = 1024)做静态重编译:
两趟扫描
- 布局计算:遍历每条指令,分类并计算 expansion 大小,构建
offset_map[insn_idx] → output_offset映射表 - 代码生成:根据分类结果 emit 重编译指令到 output buffer
指令分类与改写规则
| 指令类型 | 页内 (inpage) | 页外 (outpage) |
|---|---|---|
| 普通指令 | 1:1 直通 | — |
| B (无条件跳转) | 重算 offset | nearby: 直接 B / far: 压栈+跳回原页触发 fault |
| BL (带链接跳转) | LR=原始下一条, B 页内 | LR=原始下一条, B/BR 外部目标 |
| B.cond / CBZ / TBZ | 重算 imm | 条件跳+8, B skip, far redirect |
| ADRP / ADR | LDR Xd, [PC, #8]; B skip; .quad addr | — |
| LDR literal | 用 X17 做中转加载原始数据地址 | — |
关键设计:所有 PC-relative 指令都被改写为绝对寻址,保证在 recomp 页新位置上语义不变。
recomp 页以 VM_READ | VM_EXEC | VM_MIXEDMAP | VM_DONTDUMP 映射到目标进程用户空间,由 insert_vm_struct() 直接插入 VMA。
三种断点模式
BP_MODE_DBI(轻量断点,12 slot prefix)
slot 0: STP X30, X17, [SP, #-16]! 保存 LR 和 X17
slot 1: MOVZ X17, #magic 加载 handler-return magic
slot 2: STP XZR, X17, [SP, #-16]! push magic 帧
slot 3: LDR X30, [PC, #12] X30 = bp_addr(原始页地址)
slot 4: LDR X17, [PC, #16] X17 = handler
slot 5: BR X17 跳转 handler
slot 6-7: .quad bp_addr
slot 8-9: .quad handler
--- handler RET → bp_addr → UXN fault → fault handler 重定向 ---
slot 10: LDP XZR, X17, [SP], #16 pop magic
slot 11: LDP X30, X17, [SP], #16 恢复 LR 和 X17
slot 12+: 原始指令(重编译后)
handler 签名:void handler(void),可以读写寄存器但需自行保存。
BP_MODE_INSTRUMENT(全上下文,89 slot prefix)
slot 0: SUB SP, SP, #0x310 分配 784 字节栈帧
slot 1-17: STP/STR x0-x30 保存 31 个 GP 寄存器
slot 18-19: 保存原始 SP
slot 20-23: MRS/STR NZCV, FPSR 保存状态寄存器
slot 24-39: STP q0-q31 保存 32 个 SIMD 寄存器
slot 40: MOV X0, SP X0 = &WuwaInstrumentContext
slot 41-49: push magic + LR=bp_addr + BR handler
slot 50: pop magic
slot 51-66: 恢复 SIMD
slot 67-70: 恢复 NZCV/FPSR
slot 71-87: 恢复 GP
slot 88: ADD SP, SP, #0x310
slot 89+: 原始指令
handler 签名:void handler(WuwaInstrumentContext *ctx),可通过 ctx 读写所有寄存器(GP + SIMD + NZCV + FPSR),修改会反映到恢复后的执行。
BP_MODE_INLINE_HOOK(函数替换,4 slot prefix)
nearby: B handler 直接跳转,不碰任何寄存器
far: LDR X16, [PC, #8] 用 X16(不是 X17)
BR X16
.quad handler
不保存 LR,handler 看到原始调用者的 LR。适合函数级替换。
Fault Handler 调度逻辑
do_mem_abort() → bp_mem_abort_handler()
│
├─ 非用户态 or 非权限异常 → NOT_HANDLED
│
├─ dbi_find_by_page(tgid, PC & PAGE_MASK) → 未找到 → NOT_HANDLED
│
├─ Handler Return 检测(PC == bp_addr 且 [SP+8] == MAGIC)
│ └─ 重定向到 recomp 页 slot 10/50(restore 代码)
│
├─ Far-mode 跳板检测(X17 == PC 且 [SP+8] == FAR_MAGIC)
│ └─ 从栈帧读取真实目标 PC,恢复 X17
│
└─ 正常 translate:dbi_translate_pc(fault_pc) → recomp_pc
└─ 修改 pt_regs->pc = recomp_pc,返回用户态
Nearby vs Far 模式
recomp 页分配时用 get_unmapped_area(hint=原始页地址) 尝试就近分配:
- Nearby(±120MB 内):outpage 分支直接用
B imm26(±128MB 范围),零寄存器污染 - Far(超出范围):压 32 字节栈帧(saved X17 + magic + target PC),跳回原始 UXN 页触发 fault,fault handler 从栈帧取出真实目标地址
BP_FLAG_NO_NEARBY 可强制 far 模式。
抗检测措施
已实现
| 检测向量 | 对策 |
|---|---|
| 栈回溯发现 recomp 返回地址 | handler 的 LR 设为 bp_addr(原始页),RET 触发 fault 后用 magic 帧区分并重定向 |
| X17 寄存器泄漏 | inline hook far 模式用 X16;DBI/INSTRUMENT 的 X17 在 prefix 内保存恢复 |
| BL 指令 LR 泄漏 | 所有 BL 重写都将 LR 设为原始页下一条地址,不暴露 recomp 地址 |
已知残余向量
| 向量 | 风险等级 | 原因 |
|---|---|---|
| 时序侧信道 | 低 | UXN fault 路径比直接执行慢,但噪声大 |
| LDXR/STXR | 高 | 异常事件 |
| 低功耗待机模式的指令 | 高 | 异常事件 |
生命周期管理
- 进程退出:
sched_process_exittracepoint → 自动清理该进程所有 DBI 条目 - 手动移除:
breakpoint_remove()→ 取消 delayed work → 移除 filter → 清除 UXN → 释放 recomp 页 - 资源隔离:每个 DBI 条目独立持有 recomp_kpages、offset_map、orig_copy