项目地址:1ddK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6U0K9r3g2F1P5s2k6T1i4K6u0r3k6f1c8n7c8H3`.`.
搞 Android 逆向的都知道,ptrace 调试器(frida、IDA remote dbsrv)一上来就容易被检测,各种反调试手段层出不穷。花了大半年时间断断续续踩坑,我终于把 eDBG 这套基于 eBPF 的调试 + trace 回放工作流跑通了,感觉确实好用,写篇文章记录一下。
eDBG 是什么
eDBG 是一款基于 eBPF 的轻量级 CLI 调试器,专门针对 Android ARM64 平台。它不使用 ptrace 来附加进程,而是通过内核态的 eBPF uprobe 机制来实现断点和调试,因此能够几乎完全无视用户态的反调试检测。
这个魔改 fork 版 Unicorn Trace 功能 —— 在断点处 dump 上下文,用 Unicorn 引擎做指令级模拟执行,生成 Tenet 兼容的 trace 日志。这意味着你可以在 IDA 里像动态调试一样前进后退地回放执行过程。
为什么用它
相比传统方案(frida stalker、IDA trace、qiling),eDBG 版本的 tracer 有以下好处:
- 基于 eBPF,基本完全无视反调试 —— uprobe 工作在内核态,用户态的 ptrace 检测、
/proc/self/status TracerPid 检查等手段全部失效
- 下断运行更轻量,崩溃少,内嵌 dump 更快 —— 不需要注入 so,不需要 attach 进程,断点触发后直接在内核态采集寄存器和内存
- 可以 Unicorn 回放,直接丢给 AI 看 trace —— dump 出来的
regs.json + 内存 bin 可以直接给 LLM 分析执行流(eDBG 本身也支持 MCP 模式直接对接 AI agent)
- 支持 Tenet 回放调试,相当于 IDA 里伪动调,但更舒服 —— 在 IDA 里可以前进/后退逐条指令查看,还支持内存断点,比真动调更自由
效果展示
Unicorn Trace Dump 全流程
参考仓库图片
Tenet 回放调试

Tenet 内存断点
这是 eDBG + Tenet 最惊艳的能力之一:你可以在 Tenet 的回放中对任意内存地址下断点,当该地址被读写时自动定位到对应的指令。这对于分析加密算法中的 buffer 变化、追踪关键数据流向非常高效。

工作流
一个典型的 eDBG trace 分析流程如下:
┌─────────────────────────────────────────────────────────┐
│ 1. IDA 静态分析,确定目标函数入口和出口偏移 │
│ 找到 libxxx.so 中目标函数: 入口 0x1234, 出口 0x5678 │
└───────────────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 2. 手机端启动 eDBG,设置断点 │
│ ./eDBG -p com.target.app -l libxxx.so -b 0x1234 │
└───────────────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 3. 启动/操作目标 APP,触发断点 │
│ 程序命中 0x1234,eDBG 暂停 │
└───────────────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 4. 执行 trace,dump + Unicorn 模拟执行 │
│ (eDBG) trace libxxx.so+0x5678 ./trace_out --tenet │
│ │
│ eDBG 自动: │
│ ① dump 当前寄存器 + 内存段 │
│ ② Unicorn 模拟执行每条指令 │
│ ③ 遇到外部调用 → 切回真实调试器执行 → 重新 dump │
│ ④ 生成 uc.log(反汇编日志)+ tenet.log(回放日志) │
└───────────────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 5. 将 trace 产物 pull 到 PC │
│ adb pull /data/local/tmp/trace_out ./ │
└───────────────────────────┬─────────────────────────────┘
│
┌───────────┴───────────┐
▼ ▼
┌──────────────────────┐ ┌──────────────────────────────┐
│ 6a. IDA + Tenet │ │ 6b. 直接看 uc.log / │
│ 加载 tenet.log │ │ 丢给 AI 分析 trace │
│ 前进后退回放 │ │ 或用 local_emu.py 回放 │
│ 内存断点追踪数据流 │ │ │
└──────────────────────┘ └──────────────────────────────┘
命令速查
./eDBG -p com.target.app -l libxxx.so -b 0x1234
(eDBG) c
(eDBG) s / n
(eDBG) info r
(eDBG) x SP 128
(eDBG) trace libxxx.so+0x5678 ./trace_out --tenet
(eDBG) trace x30 ./trace_out --tenet
(eDBG) dump 0x7abc0000 0x4000 mem.bin
(eDBG) write 0x7abc0000 deadbeef
原理简介
eBPF 断点机制
传统调试器(GDB/LLDB/IDA dbsrv)通过 ptrace(PTRACE_ATTACH, pid) 附加到目标进程,这个动作会被用户态的反调试手段轻松检测到(读 TracerPid、检测 SIGTRAP handler 等)。
eDBG 走了完全不同的路线:
传统方案 eDBG 方案
──────── ─────────
用户态 ptrace attach 内核态 eBPF uprobe
↓ ↓
/proc/pid/status 可见 内核态操作,用户态不可见
SIGTRAP 信号可检测 无信号,uprobe callback
进程会被 stop uprobe handler 中暂停线程
eBPF uprobe 的原理是在内核中对目标文件的指定偏移注册探针。当任何进程执行到该偏移对应的指令时,内核会触发 uprobe handler,在 handler 中可以读取寄存器、暂停执行等。因为整个过程发生在内核态,用户态程序几乎没有感知。
eDBG 使用 文件+偏移 的断点注册方式,不需要知道目标进程的 PID 或内存布局,可以在目标 APP 启动之前就准备好断点。
Unicorn Trace 的多轮同步机制
单纯的 Unicorn 模拟有个致命问题:遇到外部函数调用(比如 strlen、malloc)就会因为缺少实现而失败。eDBG 的解法非常优雅:
Unicorn 模拟执行
│
├── 指令在 bound 范围内 → 继续模拟,记录 trace
│
└── PC 跳出 bound(外部调用)→ 暂停模拟
│
├── 在 LR 处设置临时断点
├── 用真实调试器 continue,让程序真实执行外部函数
├── 断点命中后重新 dump 寄存器 + 内存
└── 开始新一轮 Unicorn 模拟(Round N+1)
每一轮的 dump 和 trace 日志都会保存,最终合并成完整的 uc_combine.log 和 tenet_combine.log。这个设计让 Unicorn Trace 可以处理几乎所有包含外部调用的真实代码。
Tenet 内存断点
Tenet 是 IDA 的一个时间旅行调试插件。加载 eDBG 生成的 tenet.log 后,你可以:
- 前进/后退逐条指令回放
- 查看每条指令执行前后的寄存器和内存变化
- 对任意内存地址设置读/写断点 —— 当回放到该地址被访问时自动停下
这个内存断点能力是分析加密/混淆代码的利器。比如你知道明文在地址 A,密文在地址 B,对这两个地址下内存断点,就能快速定位到加密函数的关键操作。
推荐使用特化版 Tenet:343K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6U0K9r3g2F1P5s2k6T1i4K6u0r3g2r3g2F1k6i4c8Q4x3X3c8u0c8p5p5&6i4K6u0W2x3l9`.`. ,支持 dump 内存加载,在 IDA 中还原完整动调体验。
小技巧
1. 先断后调,永远记住 -b
eDBG 不支持随时暂停,必须 通过 -b 或运行时 b 命令在目标位置先下断点。养成习惯:在 IDA 里确定好偏移再启动。
2. trace 整个函数用 x30
(eDBG) trace x30 ./trace_out --tenet
x30 就是 LR(链接寄存器),trace 到 LR 相当于 trace 整个当前函数。这是最常用的 trace 方式。
3. 手动指定 bound 避免不必要的外部调用同步
如果目标代码跨多个段,自动 bound 可能不准,导致频繁触发外部调用同步(每次同步都会慢一些)。手动指定 --bound 可以减少中断:
(eDBG) trace 0x5678 ./out --tenet --bound 0x1000 0x9000
4. dump 产物可以离线回放
trace 产出的 dump_*/regs.json + 内存 bin 文件可以用 Unicorn-Trace 的 local_emu.py 离线回放,不需要手机,不需要再跑一次。适合反复调试同一段代码:
from local_emu import run_all_continuous
run_all_continuous(dump_path="./trace_out/dump_xxx", debug_switch=True)
5. 调试会跳过系统库
对 libc.so、libart.so 这类被大量进程加载的库会自动跳过,但会局限在本库中,可以过滤很多无用逻辑且提高效率
缺点与局限
说优点也要说缺点,eDBG 并不完美:
- 异常类反调试会有问题 —— eDBG 无视的是常规的 ptrace/signal 检测,但如果目标使用了基于异常处理流程的反调试(比如故意触发 SIGSEGV 走 signal handler 来执行关键逻辑),eDBG 的 uprobe 机制可能会干扰异常传递链路,导致行为异常
- 过多的外部调用会很慢 —— 每次 Unicorn 遇到外部调用都需要切回真实调试器执行、重新 dump 内存、重启模拟,如果目标函数密集调用外部函数(比如大量
strlen/memcpy/JNI 调用),trace 速度会急剧下降
- 在外部调用少的 VM trace 场景很有优势 —— 反过来说,对于自定义 VM、纯计算类混淆(ollvm handler、白盒加密循环等)这种外部调用很少的代码,eDBG 的 trace 效率非常高,这是它的最佳使用场景
- 仅支持 Android ARM64 —— 需要 root + 内核 5.10+,覆盖面有限
相关工具与参考
延伸阅读:
如果觉得有用,去 GitHub 给 eDBG 点个 Star 吧。
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!