安卓 - 基于异常的无痕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 模式.

已知残余向量 (检测点\不可公开)