蜘蛛断点:一个运行在 Android 内核里的 ARM64 硬件断点 KPM 模块
如果你也折腾过 Android 上的运行时分析,大概会有过类似的经历:想看某个函数什么时候被调用、某个内存地址被谁读写了、某个系统调用返回值能不能偷偷改掉——但用户态的 ptrace 太重,Frida 又容易被检测,而且都进不了真正的内核上下文。
于是我写了一个内核模块:蜘蛛断点(Spider HWBreak)。它基于 KernelPatch 的 KPM 机制,直接运行在 ARM64 的 EL1 特权级,通过操作 CPU 自带的硬件调试寄存器(Hardware Breakpoint / Watchpoint)来实现断点和监视点。本篇介绍它的设计、能力,以及怎么用。
它到底是什么
一句话:蜘蛛断点是一个 ARM64 EL1 硬件断点 / 硬件监视点 KPM 模块。
| 项目 | 值 |
|---|---|
| 模块名 | 蜘蛛断点 |
| 版本 | 1.10.68 |
| 产物 | arm64_hwbreak.kpm |
| KernelPatch | 0.13.2 |
| 构建 | Windows 本机(Android NDK)或 Ubuntu |
硬件断点(Breakpoint)和硬件监视点(Watchpoint)是 ARMv8 架构里 CPU 原生支持的调试能力:你往 DBGBVR/DBGBCR(断点值/控制)或 DBGWVR/DBGWCR(监视点值/控制)这组系统寄存器里写入地址和控制位,当 CPU 执行到那个地址、或读写那个内存范围时,硬件就会主动抛出一个 debug 异常。它不需要改代码、不需要插桩、对目标执行路径几乎没有侵入,这正是它相比软件方案最大的优势。
蜘蛛断点做的事情,就是把这个底层能力封装成一个可被 kpatch 命令行驱动的内核模块:配置槽位、接管 debug 异常、记录命中现场、按需采集寄存器/内存/字符串/Binder,甚至改写返回值。
适用场景
- 本地调试:在没有源码、不方便上 GDB 的环境下定位某段逻辑。
- 逆向分析:追踪某个地址的执行流、某块内存的读写来源与方式。
- 运行时定位:确认「这个函数到底有没有被调到」「这个字段被谁改了」。
- 行为干预(慎用):临时改掉某个返回值或寄存器,验证假设。
⚠️ 它是 root / 内核级工具,会改变系统运行行为,只建议在自己拥有且可控的设备上做研究,风险分层见文末。
整体架构
模块的目录很清晰,核心逻辑分散在 src/ 下,各司其职:
| 文件 | 职责 |
|---|---|
main.c | KPM 入口、命令分发、版本号 |
slots.c | 断点槽位管理、命中处理、rearm / limit 策略 |
debug_chain.c | debug 异常链的 hook 与接管 |
hwreg.c | ARM64 调试寄存器的读写、MDSCR 开关 |
step.c | 单步恢复与 fastforward 路径 |
actions.c | 命中后的寄存器 / 内存 / 字符串 / Binder 采集与改写 |
insn.c | ARM64 load/store 指令解码(读写反查) |
smp_sync.c | 多 CPU 同步、online mask 变化自动补齐 |
events.c | 命中事件环形缓存(ring buffer) |
1. 接管内核的 debug 异常链
模块启动后通过 hook 命令,用 KernelPatch 的 hook_wrap3 包裹内核的:
breakpoint_handler—— 执行断点命中watchpoint_handler—— 数据监视点命中single_step_handler—— 单步恢复hw_breakpoint_thread_switch—— 线程切换时把活跃槽位恢复到当前 CPU
关键点在于「取舍」:before_breakpoint_handler / before_watchpoint_handler 里会判断这次异常是不是本模块配置的槽位触发的,只吞掉自己识别的硬件断点,其余 debug 异常原样交还内核处理,避免和内核自带的 hw_breakpoint 框架打架。
2. 直接操作调试寄存器
hwreg.c 是所有能力的地基。它把 16 组调试寄存器用 READ_SYSREG/WRITE_SYSREG 宏封装,并负责:
- 通过
ID_AA64DFR0_EL1探测本机实际的BRPs(断点数量)和WRPs(监视点数量); - 构造控制字:监视点用 BAS 位按 8 字节窗口编码访问范围,断点用 4 字节对齐;
- 通过内核
enable_debug_monitors / disable_debug_monitors正确开关MDSCR_EL1的MDE位(真正的「总开关」)。
3. 事件环形缓存
每次命中都会在 events.c 的 ring buffer 里落一条结构化记录,包含:事件序号、纳秒时间戳、CPU、槽位、类型、FAR、PC、PSTATE、ESR、命中次数、任务的 pid/tgid/comm、watchpoint 访问方向、指令解码结果,以及该槽位配置的各种采集动作输出。后续用 events:all 一次性读出,或者配合 Host 脚本导出。
断点类型与作用域
| 类型 | 含义 | 长度 |
|---|---|---|
x | 执行断点 | 固定 4 字节 |
r | 读监视点 | 1 / 2 / 4 / 8 字节 |
w | 写监视点 | 1 / 2 / 4 / 8 字节 |
rw | 读写监视点 | 1 / 2 / 4 / 8 字节 |
作用域(scope)决定了断点在哪个特权级生效:
el1:仅内核态(默认)。注意它不包含普通 APP 的 EL0 访问。el0:仅用户态(普通 APP 的地址用它)。all:EL0 + EL1 同时监视。
监视点允许非对齐地址,但监视范围必须留在同一个 8 字节窗口内,且同一窗口同一时刻只能被一个数据监视点占用——模块在 stage 时会主动检查窗口冲突。
命中后的采集与改写
这是模块最有意思的部分。命中之后,除了基础现场,你还可以给槽位挂各种「动作」:
只读采集(不改运行逻辑):
regs:打印x0~x30、sp、pc、pstatesimd:记录 SIMD/浮点状态peek:读reg + offset处的 64 位值mem:dumpreg + offset开始的用户态内存(最多 128 字节)memabs:dump 绝对用户态地址(最多 128 字节)code:从pc - before抓before + after字节的代码片段str:从reg + offset读 NUL 结尾字符串binder:解析binder_write_read,提取BC_TRANSACTION/BC_REPLY基础字段
改写动作(会改变运行结果,慎用):
lock:watchpoint 命中后,把访问相关的寄存器改成锁定值。模块会解码当前 PC 的 ARM64LDR/STR unsigned immediate指令——读操作直接把目标寄存器替换为锁定值并跳过本次 load,写操作把参与写入的寄存器值改掉。它不是后台持续扫描内存,只在命中瞬间介入。ret:执行断点命中后,把x0设为指定值并直接返回。setreg:执行断点命中后写一个通用寄存器(拒绝写pstate)。
采集动作只对 EL0 用户态上下文有意义(内核态槽会拒绝),这也符合「用户态内存才能被安全地读」的约束。
读写反查:这次命中到底是谁干的
数据监视点最麻烦的是「知道被访问了,但不知道是谁访问的」。蜘蛛断点在 EL0 watchpoint 命中时会尝试解码触发点的 ARM64 load/store 指令,把这些信息一并打印出来:
- 指令机器码、访问大小、目标寄存器、基址寄存器、偏移
- 计算后的地址、访问值
- 如果是 pair 指令,还有第二地址 / 第二值
- register offset 的索引寄存器和 shift
换句话说,你能直接看到「0x7100a31aa8 这个地址,是被 ldr x3, [x8, #0x10] 在读,读出来的值是 0x1234」。这对逆向定位读写来源极有帮助。
过滤与策略
真实场景里监视点可能高频触发,所以模块提供了一组收敛手段:
pid/comm:只记录指定进程或comm名的命中。policy:oneshot-cpu——每个 CPU 单次命中后自动 rearm(适合「统计每个核各命中几次」)。oneshot-global——全局只命中一次。
limit:达到指定命中次数后不再 rearm,相当于「只观察前 N 次」。
所有槽位默认是 oneshot-cpu 策略,命中即 trip(关闭 enable 位),需要时手动或自动 rearm。模块还维护了一个软件状态机(slots.c),并对多 CPU 做了 sync / verify / 自动补齐——发现 CPU online mask 变化(比如大小核调度、热插拔)会自动把活跃槽位重写到新上线的核。
自测
模块内置了一组自测,方便在真机上快速验证功能是否正常工作:
selftest:addr # 地址相关自测selftest:data # 数据相关自测selftest:call # 调用路径自测selftest:read # 读监视点自测selftest:write # 写监视点自测selftest:rw # 读写监视点自测selftest:all # 全部核心功能自测selftest:stress # 压力测试构建与部署
Windows 本机构建
现在不需要 Ubuntu,直接在 Windows 上用 Android NDK 就能编译(默认用 NDK 27.1.12297006):
cd C:\Users\77006\Desktop\Code\31_Android_KPM_Arm64_HwBreakpowershell -ExecutionPolicy Bypass -File .\build-windows.ps1 -Clean产物是根目录下的 arm64_hwbreak.kpm。
推送到手机
只使用 /data/adb 目录,不碰 /data/local/tmp:
adb shell "su -c 'mkdir -p /data/adb/kpms && chmod 700 /data/adb/kpms'"adb push .\arm64_hwbreak.kpm /data/adb/kpms/arm64_hwbreak.kpmadb shell "su -c '/data/adb/modules/KPatch-Next/bin/kpatch kpm load /data/adb/kpms/arm64_hwbreak.kpm'"adb shell "su -c '/data/adb/modules/KPatch-Next/bin/kpatch kpm ctl0 蜘蛛断点 status'"一个最小工作流示例
# 1. 安装 hook,接管内核 debug 异常链kpatch kpm ctl0 蜘蛛断点 hook
# 2. 在用户态地址 0x7100a31aa8 上布置一个 8 字节读写监视点kpatch kpm ctl0 蜘蛛断点 replace:0:rw:0x7100a31aa8:8:el0
# 3. 打开寄存器、代码片段采集kpatch kpm ctl0 蜘蛛断点 regs:0:onkpatch kpm ctl0 蜘蛛断点 code:0:32:96
# 4. 启用并等待命中kpatch kpm ctl0 蜘蛛断点 enable:0
# 5. 读取命中事件kpatch kpm ctl0 蜘蛛断点 events:all
# 6. 用完清理kpatch kpm ctl0 蜘蛛断点 disable:allkpatch kpm ctl0 蜘蛛断点 clear:allkpatch kpm ctl0 蜘蛛断点 unhook风险与安全提示
模块对动作做了明确的风险分层,使用时心里要有数:
| 风险 | 功能 |
|---|---|
| 低 | status events dump verify sync pid comm policy limit |
| 低~中 | regs peek mem memabs code str(只读采集) |
| 中 | binder、r/w/rw 高频监视点(异常开销大) |
| 高 | lock(改写访问值) |
| 高 | ret setreg(改写返回值/寄存器,改变运行结果) |
只读采集一般不改变用户态逻辑;lock / ret / setreg 会直接改变运行结果;高频监视点会带来明显的异常处理开销。调试完记得 disable/clear/unhook 一键归位。
还有一套 Host 工具链
除了内核模块本身,仓库里还附了一组 Windows 侧 PowerShell 脚本,把「采集 → 标注 → 导出」串成工作流:
hwbp_host.ps1:事件解析、/proc/<pid>/maps标注、JSONL/CSV 输出annotate_events.ps1:给事件PC/FAR标注模块与偏移break_module_offset.ps1:按「进程名 + 模块名 + offset」自动换算 ASLR 地址并布点load_profile.ps1/watch_profile.ps1:从 JSON profile 批量配置、循环监听、自动 rearm 导出export_trace.ps1:导出 IDA / Ghidra bookmark CSV、trace summaryexport_binder_trace.ps1/analyze_app_scan_trace.ps1/export_capture_blobs.ps1:各种专项导出
可导出格式包括 text、JSONL、CSV、IDA/Ghidra 书签、Binder CSV、app scan CSV,以及把 mem/code/binder 原始字节导出成 .bin。
蜘蛛断点是我近段时间折腾 Android 内核调试的一个小结。它把 ARM64 硬件调试能力从「要看一大堆寄存器手册」变成了「几条命令就能用」,对本地调试和逆向分析确实好用。后续如果有人感兴趣,我可以单独写一篇讲讲 lock 的指令解码实现,或者 Host 脚本如何对接 IDA/Ghidra。
项目基于 KernelPatch 0.13.2,源码与构建脚本在本地工程 31_Android_KPM_Arm64_HwBreak 中。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时





