首页
社区
课程
招聘
[原创]不同真机 trace 工具性能对比、优化点分享、xfQtrace使用说明
发表于: 15小时前 651

[原创]不同真机 trace 工具性能对比、优化点分享、xfQtrace使用说明

15小时前
651

2万字长文,写了给我累死了,本来可以直接随便写写就发的,但是还是写好一些吧,唉,我这算是完美主义者么?

本文分三大部分:

新手老手都能看,希望对你有用。如果是 星球 用户可以直接移步第三部分,当然建议前面也看看;

我知道大家最想看啥,所以把对比放到前面来了;我把5个trace工具在同一个目标函数上跑一遍最直观。bench 用的 demo:

当然有人会杠说我测试不够多啥的,对此我只能说事实胜于雄辩,不信就自己测试。

哦因为介绍了我自己的trace工具,可能还会有人说软广啥的,随便说,好的工具值得拿出来宣传,而且我自己也把优化点分享出来了(我觉得做到这一步已经很好了),有动手能力的都能自己写一个trace工具。

结果:

注:AndroidPrybar 崩溃位置在 libtrace.so 偏移 0x510a2c–0x5666c8 之间 8 帧(SIGSEGV / SEGV_MAPERR / fault addr 0x0),作者 README 也直接写了"代码让ai改崩溃了,一些功能不太稳定"。

vmtrace 接口和其他几家不同:它要求"Interceptor.replace 目标函数 → callback 里调 qbdi_vm_call(targetAddr, argsPtr, argNum, logPath, dumpMem) → 返回结果",等于把"切入 trace VM"和"调用目标"绑成了一次调用。某种程度上更直观(一行 trace 一个函数),但只能从函数边界进入,且接入方需要自己组装参数指针数组(参数个数填错会崩)。

几个观察点:

当然这就是 demo 上的单点数据,实战中不同样本(深递归、大量子线程、自定义 linker、JNI 调用密集型等)相对差距会变。bench 脚本和 demo 已经放在仓库 trace_compare/bench/ 下,欢迎自己换样本复现。

为什么会有这么大的差距?这就涉及指令级 trace 到底在做什么、各家选了什么底层引擎、还有 xfQTrace 在 QBDI 之上做了哪些工程化改动 —— 接下来一层层说清楚。

先来一大段背景,可跳过:

最近一两个月,先是休息了半个月,打打游戏啥的,然后实在是压力太大了,一天不逆向浑身难受啊,跟蚂蚁在爬一样,然后就开始逆向了。最近一个月在学习密码学经典算法的设计原理相关的内容,感觉学起来还是有点难度的,很有意思。后续会用remotion来创作高质量视频,然后发布到b站给大家补点相关基础,放心讲的会非常非常详细,设计原理/手搓实现/汇编特征/魔改怎么逆等等;

然后平时偶尔逆向一下实战案例,一些安全SDK那种,过程中发现很多案例用unidbg来补会有一些问题,虽然中等难度的app大多都可以解决,但是稍微再难的就不方便处理了,与其耗时在深度魔改unidbg让其更像一个android,不如直接写一个优秀的真机trace工具;反正一个信息很全的trace日志,ai只需要使用ripgrep就可以暴力分析了,然后我们拿到分析结果之后就可以开始复现,这样可以利用ai当老师然后不断追问来进行学习,哪怕是vmp的案例也是照样逆完学习;

所以就会有明显痛点了,开源的真机trace好像有很多问题,而且直接用别人开源的成品好像原理啥的我也掌握不到位呀,俺是来学习的又不是来直接用成品的。所以一周前我开始学习qbdi,然后顺便开始开发我自己用的真机trace工具,连续3天睡眠加起来不超过14小时吧,也是非常抽象了,也是压根睡不着*几发还是一样哈哈哈哈;好在最近几天已经完工了,可以美美休息几天了。

然后感觉自己做的比开源的工具对比下来会有很多明显优点吧,自己也倾注了很多心血,而且后续还会支持俺的ttd;然后我认为单独收费卖感觉不太好,因为其实技术含金量不是很高,重要的是怎么思考到这些优化点,优化的点都是我一步步思考出来的; 那为啥我不打算免费发出来呢?

基本上就是这三点,寒了多少愿意开源的朋友的心。所以仔细考虑下来,就打算把工具免费丢到 星球 了。当然你也可以认为我是为了引流吧,我承认有这一部分原因,但更多的是心确实已经寒了。(另外其实我在b站已经送出去很多朋友 星球 了,都是愿意分析文章或者工具或者都愿意点拨我的)

那发到 星球 还有什么好处?

因为诚实来说俺也没啥稳定收入,平时也不接点单,也没工作,现在就靠 星球 赚点生活费,能靠这样能引流几个人进去也算不错了;我个人认为就这样处理最妥当,如果还是被喷的话那就随他吧,反正这圈子遇到的神人确实还挺多的哈哈哈哈;

但是!!有很多朋友仍然非常支持我,我觉得我也要做点什么!所以我会把这个工具的实现以及优化思考全都放出来吧,这样即使各位不愿意进 星球 的朋友依旧可以拿本文喂给ai让他对开源的真机trace进行改造,具体见后文介绍(点击跳转);

安卓逆向里,trace 是绕不开的环节。先说啥是 trace。

trace 就是"跟踪"。我对 trace 的抽象理解是:站在不同角度去观察一件事的发展

举个现实生活的例子:你看到一条新闻,它就一定是正确的吗?我个人认为现在的网络环境下,很多人会拿着单方或者疑似权威的说明就当作绝对正确。

总结就是:视角越深、维度越多,你看到的事实就越接近真实。

trace 在逆向里也是这个道理 —— 你站在哪一层看,决定了你能看到的事实有多深。

但也不是越底层越好的,要综合评估,如性能开销还有实际效果,最底层的trace并不是适用于任何场景。

trace 其实有非常多种,只是现在大家提到 trace 默认指的是"指令级 trace"。下面我会从最浅到最深列一遍各级 trace,希望能帮大家拓宽一点思维 —— 这对于理解整个安卓逆向工具链的作用和发展史会很有帮助。

OK,假设你拿到一个 app,目标是分析清楚它"登录"按钮背后到底做了什么,从最浅到最深一层一层下去:

点击登录按钮 → loading 弹窗 → 界面跳转 → 中间可能藏着各种日志上报、设备信息收集、风控埋点。这一层我们关心"app 对外露出来什么"——不进 app 内部,只看它在外部留下的痕迹。

下面是一些常见的"黑盒观测 trace":

logcat:app 自身打的日志,很多 SDK / app 默认会留一堆 tag,可以选择一些常见的进行尝试,运气好可以抓到关键点,不过意义不大(一般只有在报错时会有error日志有点用)

抓包:登录涉及把账号密码上传服务器,所以一定走网络协议。

直接跑下来你会看到一堆 HTTP / TLS over TCP 的包,少数 app 会上 HTTP/3(也就是 QUIC over UDP)。这里就不展开抓包对抗的东西了。总之这一步价值非常的高,比如如果是http协议,你就能看到他的关键报文了;

文件 IO trace:很多 app 会把 token / device id / 缓存 / dex 副本 / so 副本写到 /data/data/<pkg>/ 或 sdcard,看它读写哪些文件能直接定位关键流程(特别是脱壳、找加密本地存储的密钥)

系统调用 / 内核 trace

UI 行为分析

进程 / 服务 状态

UI 层用的是 Java / Kotlin(绝大部分情况),那就把 apk / dex 拿出来反编译看,常见工具如下:

加壳的话需要先脱壳,方案很多,常见的有:

上面的话推荐直接用网站,不行的话就得自己找脱壳机了,实在不行就找一个类似f利姬的热心 脱壳姬哈哈哈哈(俗称人脉脱壳),还是不行的话就得自己魔改咯。这里不过多展开。

拿到代码之后一般直接看 Java / Kotlin 源码 —— 高级语言可读性高,定位关键函数很快,如果遇到一些特殊情况,比如反编译失败或者混淆严重可以选择查看更底层的语言:smali汇编,在这里不多赘述;

总之通过这一步很容易找到关键函数,亦或者你用前面黑盒观测 trace 中分析出来的关键信息比如报文 header、xml 文件名等等定位也可以!

知道哪个函数是关键之后,基本都需要做动态校验:到底有没有真的被调用?参数 / 返回值长什么样?主流两种方案:调试 / hook,本人基本不用调试,因为太麻烦啦!下面只聊 hook。

hook 思路:注入进程 → 改写函数入口出口 → 打印参数返回值 / 改写参数返回值。下面给一些我个人认为值得尝试的 hook工具:

这里我觉得很有必要提醒一点:很多没列出的工具也很好用,但大多本质都是基于 frida / lsposed 的封装,目的都是省时间。这类封装对新手是降维打击,无脑使用即可;但对老手来说,遇到新样本时往往会感到束手束脚 —— 封装给你省了入门时间,但也屏蔽了底层细节。所以新手如果一上来就把大量时间花在二次封装工具上,那就错失了学习底层工具的窗口。

每个时代都会涌现大量二次封装工具,那些出名且能长期存活下来的项目,而且一直有人时不时维护,我认为这种才值得长期使用,或者你有改源码的能力;如果没有,此类项目一旦时间拉长,最终也基本只剩下源码学习的意义(比如学习如何封装)。对作者本人来说,很多项目只是为了解决自己在某一时期的需求,时间一拉长,就会有更好的解决方案了。

正所谓: 器本自通流,制器有来由,他铸合他求,探源得真枢,握得本根在,随心任去留。

综合下来,这里建议直接学 frida / lsp 就行了。但是有开发能力的朋友建议多思考多动手,万一你能做出更优秀的工具呢!!

我自己更喜欢灵活性高的方案,直接 frida cli + JS。哪怕是为了省时间,我也会给 ai 链上 jadx 的 mcp,让它自行查找关键函数,然后让它快速使用 frida cli + JS 验证:通过 hook 快速验证是否触发,以及打印获取调用栈。

到这一步大部分中等强度的 app 已经能搞定,比如本场景:抓密码 → hash → 拼报文header如sign → 发包模拟登录。但深度混淆(dex 加固 / Java 转 native)的 app 就没这么轻松了。

Java 反编译失败、或者 jadx 出来全是 <error: failed to decompile>,亦或者需要做混淆验证 —— 那就深入本质,降到 ART 虚拟机字节码层面,也就是 smali。这时候要么静态硬啃 smali(jadx 直接切到 smali 视图即可),要么动态分析。

如果静态分析压力大,或者需要动态验证,那就要么动态调试 smali,要么对解释器插桩把"真实执行的 smali 汇编trace 出来:

这一步建议熟读各类工具源码,会有很深的体会;

另一个常被忽略的视角是 binder trace,这部分高可用的工具也少。Android 应用与系统服务(SystemServer / mediaserver / surfaceflinger 等)的所有 IPC 都走 binder,位置 / 传感器 / 设备 ID / 通讯录 / 短信 / 包管理 / 网络状态 …… 这些数据无一例外都要跨进程拿。在 Java 函数 trace 看到的 TelephonyManager.getDeviceId() 之类,在系统框架层就是一次 IPCThreadState::transact,能抓到完整的 Parcel 序列化数据。

这一层 trace 的核心价值在于:

常见工具:

实战里 binder trace 经常和 Java/native trace 配合着看:先在 Java 层 frida-trace 找到可疑调用 → 在 binder 层确认到底走的哪个系统服务接口 → 必要时下到 native 层抓 Parcel 详细字段。

如果 Java 函数最终调进 native(JNI 桥),SO 在 lib/arm64-v8a/ 下。拿出来反编译:

常用插件(拿来辅助识别加解密 / 哈希):

由于反编译工具编写难度很大,所以推荐闭源的IDA/Ninja;

接下来说说Java层发现native函数之后,去定位 native 函数的几种情况:

定位完对函数做静态分析。混淆轻的伪 C 看个大概就能猜个八九不离十;混淆重的、或与 JNI 桥交互复杂的,就得上动态工具辅助理解了。

定位到 native 函数的真实地址之后,hook 拿入参出参;如果函数本身比较复杂,还可以对函数内的关键调用点(子函数、libc 调用、JNI 调用)做 hook 辅助观测(调试方案下面单独讲)。主流工具:

绝大多数 Java → native 的关键函数(指纹采集、风控上报、加解密)都会再通过 JNI 桥反过来调用一堆 Java 方法去拿设备信息(IMEI / IDFA / sensor / SharedPreferences / SystemProperties),追这一层能极快地理解 native 函数的语义:

这一层非常香,很多看起来"全 native 实现"的算法其实只是个胶水,真正干活的是它通过 JNI 反调的 Java 函数。比如很多 app 的设备指纹,逆向半天发现核心逻辑大部分就是 JNI 反调拿到一堆设备指纹,然后 RSA 加密上报获得 token。

但是俺用jnitrace经常崩溃,我又懒得去改源码,所以就直接在后面的trace中实现这个功能就好了;

伪 C 混淆太重 / 直接看 ARM 汇编时,光静态读寄存器猜不出来,hook 拿入参出参也只能看到边界,这时候就需要调试手法在任意位置看寄存器 / 内存:

调试的好处:可以下断点、看任意时刻寄存器 / 内存。坏处:

所以本人不太推荐。能不调试就不调试,把状态全 dump 出来离线分析,在大部分场景下往往更高效 —— 这就是下一节的指令级 trace。

调试不稳定、hook 粒度又太浅怎么办?把底层执行的汇编全部记录下来,顺便把汇编涉及到的所有副作用 —— 每一次寄存器变化、每一次内存读写、每一次文件访问、每一次系统调用都"录"下来,事后离线分析。这就是指令级 trace。

实现思路本质上都是插桩:在原汇编里塞回调,每条指令执行前后采集状态。按载体可以分两类:

模拟器路线(不依赖真机)

封装比较好的基本上只有 unidbg 一家:

真机插桩路线(依赖真机)

按内核 / 原理大致这几条路:

上面提到的 GumTrace / QTrace / sktrace / AndroidPrybar,以及那些没列名字、靠零散代码片段把原理铺出来的所有真机 trace 项目都值得致敬,感谢你们的分享!

以上问题都是我在实战场景中遇到的,可能因为我懒吧不想思考方案,我就想全量trace,且包含完整内存修改内容;还有像是自定义linker的案例,现在在很多业务代码,如国内知名小说app:菠萝包轻小说 的签名算法在比较新的版本中都出现了(这里埋一个坑,算法还原已经在 星球 发过了,我还用unidbg补了这种linker的案例,有人感兴趣可以评论我会考虑写文章分享一下思路和代码);

为啥我决定自己再写一个真机trace?因为我在做自己的时间旅行调试器(TTD)工具,除了 unidbg 这条线之外还需要一个真机 trace 后端把指令流喂给 TTD 渲染。最开始参考了看雪上的 QBDI 原理详解QTrace 介绍文章 + zgy0x01/QTrace 源码,先在 QTrace 上魔改了一天多,但是感觉扩展性比较差而我又有二进制trace需求于是放弃;等我把 QBDI 工作原理彻底吃透之后,干脆从头自己撸了一遍 —— 这就是 xfQTrace。gumtrace还没有阅读过源码,因为有些朋友跟我说qbdi速度更快,所以就先去改了qbdi的。如果后续有场景需要gumtrace,那我也可能会考虑再适配一手;

我做的工具核心一直是给人类辅助分析使用的,AI 只算附加项,所以会在 trace 中加上大量额外的方便人类阅读的信息。下面说一下 xfQTrace 在主流真机 trace 工具基础上做的核心改进。

xfQTrace 的输出格式从一开始就是按"逆向工程师怎么读"设计的,每一条指令都自带上下文,不用反复回滚对照。下面用一个真实子线程 trace 片段举例。

格式依旧和unidbg的差不多,这样迁移压力会小一点;

一行同时拿到"输入 + 输出",不用上下翻 trace 自己脑补寄存器变化。这个的实现方案就是上面提到的"单回调延迟输出":第 N 条指令的 PRE 回调里同时把 N-1 条的 POST 也吐出来。

这一点是我觉得给人阅读必不可少的;

很多 trace 工具只输出地址 + 值,没有 hex 视图和 ASCII,人类分析的时候就丢失了根据值直接搜索定位的方法了,xfQTrace 的格式让你**直接在 trace 里用 grep 搜字符串/小端字节/大端字节/真实写入地址 **就能定位到关键位置。

跨模块 / 跨函数跳转时输出独立 banner,参数按 ABI 顺序展开(默认 x0~x7):

这个的好处是,你跟到类似x0这种参数,发现签名有个函数call的banner的时候你就可以在ida中快速找到对应函数,然后跳转进行分析;

语义 Provider 命中时,sub_0xXXXX 直接被替换成符号化签名 + 参数命名 + 类型解析:

cstr 自动读字符串、句柄类参数(fd / 文件路径)符号化、返回值负数自动 errno 化。trace 里一眼就能看到"哦这里调了 open 但是 -1,去看下 flags 0x242 是啥"。

很多反 hook / 反检测 SDK 故意绕开 libc,直接 svc #0 触发 syscall。xfQTrace 的 QBDI VM 检测到 svc #0 时输出独立 banner,覆盖 NR 0~291 主流arm64 syscall 全部命名:

参数按 syscall 签名格式化(fd / cstr / size / int 等),返回值负数自动按 errno 显示。

主线程 trace 中 pthread_create 被拦截后,子线程 trace 文件第一行是启动 banner:(这里后续会支持利用clone系统调用创建的线程还有通过jni反射调用java创建的线程)

fork_insn_index 标记父线程在第几条指令处 fork —— 跟主 trace 完美对齐,方便分析父子时序。

同目录下还有 .meta 文件记录关系:

py 解析器读这个文件渲染线程树,多线程指纹/加密场景一图看清父子调用关系。

把上面几样组合起来,一段真实 trace 大概长这样(节选自 libxftest 子线程):

指令级 trace + 函数 banner + 内存 r/w + libc/syscall 语义全部在同一份 trace 里按时序穿插。逆向时基本不再需要在 IDA 和 trace 之间反复横跳。

单回调延迟输出:传统方案每条指令需要 PRE + POST 两次 QBDI 回调。xfQTrace 只注册 PREINST 一个回调,第 N 条指令的 PRE 里同时输出第 N-1 条的 POST(此时 N-1 已执行完,寄存器变化可读)。QBDI 上下文切换开销直接砍半。这一点跟unidbg学的,不得不说unidbg真的是很好的一个工具;

Opcode-keyed 指令缓存:ARM64 指令固定 4 字节,opcode 严格决定反汇编、操作数、分支属性。用 opcode 做 key 全局复用 —— 4M 槽位直接映射,命中时跳过 QBDI getInstAnalysis(OPERANDS) + capstone 反汇编。实测命中率:短函数 67%,循环密集 90%+。这一点是我自己魔改的unidbg中 提出的方案,实测效果很好;

双缓冲异步日志

128MB × 2 = 256MB 总内存(mmap 分配),trace 线程永不阻塞 IO(除非两个 buffer 都满)。

最终部分实测效果如下(设备pixel5,可以说非常垃圾了):

有个8GB的样本忘记了,好像是xhs来着?

trace 写盘的同时进行 LZ4 frame 压缩:

trace 期间被调用的 pthread_create 会被 ParseEngine 拦截,由 ThreadTraceManager 接管:

跨模块跳转或跳到匿名内存(JIT / unpacked dex / 动态生成代码)时,InstrumentationManager 触发 throttled (200ms) 模块刷新,把对应 range 加进 QBDI instrumented 集合 —— 不需要预先全量 instrument,遇到自定义 linker / 内存中解密的 SO / unpacked dex 这种场景特别好用。

xfQTrace 把语义解析做成可扩展的 Provider,每一类函数有自己的规则表 + 格式化器,规则注册之后 ParseEngine 在热路径上查表 → ArgFormatter 渲染:

如 QBDI VM 检测到 svc #0 时输出独立 banner,包含 syscall 名 + 参数(按 syscall 签名格式化:fd / cstr / size / int 等)+ 返回值(负数自动按 errno 显示)。覆盖 NR 0~291,主流 syscall 全部命名:

很多反 hook / 反调试 SDK 会绕开 libc 直接走 svc #0,这个 banner 把这层伪装直接戳穿。

但是这种其实很多so会被直接编译进入app的so,有的丢失了符号信息,后续有空会考虑对常见函数字节码进行匹配,实现 strip 函数名的函数依旧能展示上层语义(比如 strip 过的 BoringSSL 也能识别 SHA256_Update)。

不止 trace 内部,连最外层 hook 入口的参数也自动按类型解析:

支持 env / obj / jstr / jobj / int / cstr 等,trace banner 直接在logcat就能看到 Java 层传入的参数,不然你在trace中找输入还怪麻烦的,支持解析java对象,比如jstring就直接展示,map也可以展示内容。这样当你多次trace的时候,能够区分输入输出,不然你都不知道输入输出那trace还有什么意义;哦对了,这个trace文件的首尾也放了,方便使用呢。

整个工具对外只暴露 6 个函数,所有配置项通过 xfqtrace_configure(json) 一次性下发,用户不再需要 dlsym 一堆导出函数进行深度配置,写法固定,trace崩溃的时候方便定位错误点:

完整 schema 见下方 JSON 配置 schema

虽然 AI 时代我们可以拿到 trace 日志之后直接 ripgrep 暴力搜索 / 污点分析,但我一直认为古法逆向的功夫是必须练的,否则走不远。xfQTrace 在设计每一个 feature 的时候都优先考虑"人怎么读",AI 友好只是顺带的副产物。

当然你也可以认为我是在坚守道义。其实我早在 2025 年 8 月就开始用各种反编译 mcp 工具,11 月起也开源了不少逆向相关的 AI 工具,比如抓包工具 LunFengChen/proxypin-mcp-server

下面这部分是给 星球 内部的朋友看的,如果你感兴趣也可以看看,我都封装好了的(你想用frida的js或者frida的py写法都是可以的)

自动完成:push SO → frida spawn → 等待 trace 完成 → pull → lz4 解压

常用参数:

懒得起 Python,或者想自己控 spawn / attach 时机的,直接 frida CLI:

适用场景:

注入到目标进程的 libxfqtrace.so 把所有运行时信息(启动 banner / hook 安装结果 / hook 命中参数 / 文件路径 / 错误原因等)都打到 logcat,统一 tag 为 xfQTrace

trace 跑起来之后强烈建议另开一个终端实时盯着 logcat —— 大部分配置错误(比如 hook 装不上、arg_filter 不命中、命名后缀没生效)都能从这里第一时间看到。

带颜色、自动按包名跟随的话,推荐 JakeWharton/pidcat

按 trace 生命周期顺序,logcat 大致会出现这几类信息:

1) 注入成功(构造函数自动打的启动 banner)

看到这一段说明 SO 已经被成功 dlopen,注入流程通了。

2) configure / start 阶段

如果 configure 报错(比如 JSON 格式错 / target.base 没设),会看到:

3) hook 命中 / 跳过

hook 命中时(即 Java/native 层调用了目标函数):

如果配了 arg_filter 但本次调用没命中,trace 不会启动,logcat 只打一行 skip:

arg_filter.value 没命中、调试时找不到 trace 文件、就来这里看是不是 cmd 不对。

4) trace 文件路径

logger 启动时直接打路径:

如果配了 naming_source(参数/返回值塞文件名),trace 结束后还会再打一行重命名结果:

5) 安全停止

logcat 只是元信息和实时状态,真正的指令 trace 在落盘文件。默认输出目录:

demo_trace.py 在 trace done 回调里会自动 pull + 解压;手动模式见上一节末尾的 adb pull 命令。

编辑 example_trace/demo_trace.js 顶部:

整个 qtrace 对象会 JSON.stringify 后通过 xfqtrace_configure 一次性下发给 SO,
之后只需调用 xfqtrace_start() 即可装载 hook,无需逐个 dlsym 大量 setter。

复用 HookArgFormatter,逗号分隔,支持以下标签:

很多大厂 native 都把入口收敛成一个分发函数(如阿里系的 doCommandNative、字节系的 command、各种风控 SDK 的 doExecute),签名类似:

签名相关、设备指纹、加解密等几十种功能全挤在同一个函数里,根据 cmd 走不同分支。整体 trace 会:

arg_filter 让你只在 cmd == 你关心的那个值 时才进入 trace,其他调用直接走原函数,零开销。

要点:

如下图就是跳过的部分,这对于抓初始化函数感觉也会有用呢!

ae98bbde1cda10faabd12b590c777c08

同样是 doCommandNative 这种分发函数,如果你想跑一次 trace 多个 cmd 又要分辨清楚每条 trace 是哪个 cmd 触发的,用 naming_source: 0 + naming_index 把某个参数值塞进文件名:

输出文件类似 xfqtrace_libsgmainso_<base>_5b198_10401.log(基址 + 偏移 + cmd 后缀),方便后续按 cmd 归档。

naming_source: 1 则取返回值,常用于 MessageDigest.digest() 这种把结果哈希塞进文件名的场景,方便后续按返回值归档区分。

EVP_DigestUpdate(ctx, data, len) 这种 (void* buf, size_t len) 形式:

输出会把 x1 当成指向 x2 字节的 buffer 显示前 32 字节 hex。

env 标签是用来跳过 JNIEnv* 的;纯 C 函数直接按真实参数排:

不想在 JS 里 Process.findModuleByName().base 取基址再传,可以直接给 SO 名:

适合目标 SO 已经加载、且名字稳定的场景。如果是动态 dlopen / 名字带版本号变动,仍建议在 JS 层等 android_dlopen_ext 触发后再传 target.base(见 demo_trace.js 默认逻辑)。

memory_trace 默认开。如果只关心控制流和寄存器变化、不关心每条 load/store 的内存读写值,关掉能显著提速并大幅减小 trace 体积:

默认开启,可在 JSON 里调整:

解压:lz4 -d trace.log.lz4 trace.log

好处:

trace 输出(含子线程的)默认都是 .log.lz4,需要 lz4 CLI 解压才能阅读。各平台安装方式:

常用命令:

demo_trace.py 自动 pull + 解压时按平台从 example_trace/ 同目录拿固定二进制:Windows=lz4.exe、macOS=lz4-mac、Linux=lz4。仓库自带 Windows 版;其他平台请把上面命令装出来的 lz4 拷过去并按对应文件名命名。

精简到 6 个核心函数,所有配置项通过 xfqtrace_configure(json) 下发:

标准调用顺序:configure(json)set_done_callback(cb)start() → (hook 命中触发 trace) → 回调里收尾 → stop()

把 JSON 配置一次性灌进 SO,0=成功,非 0=失败(具体原因调 xfqtrace_get_last_error)。

按当前配置安装 hook,立即返回,等目标函数被调用时才真正进 trace。0=成功。

安全停止:置 abort flag → 移除 hook → flush logger → 关 LZ4 frame。无返回值,幂等。

幂等:同一操作执行 N 次,效果跟执行 1 次一样。第一次 stop 真干活,后续再调内部检查"已经停过"直接返回,不会 double free / 重复 close / 崩。
好处:Frida 端多个清理路径(rpc.stop / 退出 hook / catch 块)不用各自维护"停过没"的状态;done_callback 触发后已经自动停过一次,Python 收到信号再保险调一次也安全。

绕过内部 hook,把寄存器值直接喂给 trace 引擎,立刻开始 trace。

注册 trace 完成回调,无返回值。传 NULL 取消注册。

返回最近一次 API 调用失败的错误描述,C 字符串。成功调用会清空。

项目 zgy0x01/QTrace看雪文章,id:乐子人) lidongyooo/GumTrace看雪文章,id:lidongyooo) jiqiu2022/vm-trace-release看雪文章,id:棕熊) GitKittys/AndroidPrybar(无看雪文章) LunFengChen/xfQTrace(本文,id:x1a0f3n9)关 memory hexdump 开 memory hexdump
开源 开源 开源 仅成品 仅成品(疑似后续开源) 仅成品,星球 成员免费使用,不额外单独收费
引擎 QBDI VM(动态 DBI) frida-stalker(动态重编译) QBDI VM(动态 DBI) Unicorn VM(仿真执行) QBDI VM(动态 DBI) QBDI VM(动态 DBI)
wall_ms 294 s 32.0 s 83.1 s 崩溃 @ ~110 s(未完成) 19.3 s 86.9 s
处理速度 12.3 万 insn/s 113 万 insn/s 43.5 万 insn/s 失败 187 万 insn/s 41.6 万 insn/s
raw 字节 2.55 GB 3.71 GB 2.29 GB 2980 MB 4527 MB
落盘字节 2.55 GB(无压缩) 3.71 GB(无压缩) 2.29 GB(无压缩) 0(崩溃,缓冲未刷盘) 272 MB(LZ4,91% 压缩比) 351 MB(LZ4,92% 压缩比)
raw 吞吐 8.9 MB/s ≈ 0.52 GB/min 119 MB/s ≈ 6.95 GB/min 28.2 MB/s ≈ 1.65 GB/min 154 MB/s ≈ 9.05 GB/min 52.1 MB/s ≈ 3.05 GB/min
落盘吞吐 8.9 MB/s ≈ 0.52 GB/min 119 MB/s ≈ 6.95 GB/min 28.2 MB/s ≈ 1.65 GB/min 14.1 MB/s ≈ 0.83 GB/min 4.0 MB/s ≈ 0.24 GB/min
稳定性 完成 完成 完成 中段 SIGSEGV,VM 内部空指针 完成 完成
Provider 覆盖范围
JNI 全套 JNIEnv->* 函数(GetXxxField / CallXxxMethod / NewStringUTF / GetStringUTFChars 等)
Libc malloc / memcpy / strncpy / pthread_* / dlopen / dlsym ...
Crypto OpenSSL / BoringSSL:SHA256_Update / AES_* / EVP_* / HMAC_*
Protobuf WireFormatLite / 序列化反序列化入口
STL std::string / std::vector / std::map 等容器操作
Network socket / send / recv / SSL_read / SSL_write
Compress gzip / zlib / zstd / lz4 (函数级)
Syscall NR 0~291(直接走 SVC 的)
参数 说明
-p 目标包名(必填)
--attach 附加到已运行进程(默认 spawn 冷启动)
--no-push 跳过 push SO,复用设备上已有的 libxfqtrace.so
--pull-only 只拉文件 + 解压,不跑 frida(trace 已经跑完)
--reinstall 卸载重装 APK 重置设备指纹(传 APK 路径)
--serial 指定 ADB 设备(多设备时)
字段 类型 说明
hook_backend int 0=ShadowHook, 1=frida-gum (默认)
target.lib string 目标 SO 名(与 target.base 二选一)
target.base string 目标 SO 基址,支持 "0x..." 或十进制(与 target.lib 二选一)
target.offset int / "0x..." 目标函数偏移
output_format int 0=text(.log)
1=binary(.xfqtrttd, 开发中,⏳ 暂不可用,后续需要搭配俺开发的时间旅行调试器)
memory_trace bool 内存读写追踪开关(用不上可以关掉)
compression.enable bool LZ4 压缩开关,默认开
这个很有用,如果你认为trace文件大一点不影响的话可以关掉
compression.level int 0=fast(默认), 1-12=HC 高压缩
max_traces int 最大 trace 次数, -1=无限
-> 看你想要采集多少次trace
arg_filter.idx int 参数过滤索引(命中才 trace,见后续使用示例)
arg_filter.value int / "0x..." 参数过滤值
hook_format.args string 参数类型, 逗号分隔, 见HookArgFormatter
hook_format.ret string 返回值类型
hook_format.naming_source int 命名来源 (-1=禁用, 0=参数, 1=返回值)
hook_format.naming_index int naming_source=0 时取第几个参数当文件名后缀
标签 含义
env / _ 占位(不输出,用于跳过 JNIEnv* / jclass
jstr JNI String,自动 GetStringUTFChars,失败 fallback 到 toString()
obj JNI 对象,输出 getClass().getName()
jobj / jmap JNI 对象,输出 toString()(适合 Map/List/自定义对象)
jbarr byte[],输出 byte[N]{XX XX XX...} 前 16 字节 hex
cstr C 字符串 (char*),自动读 null-terminated
int 32 位整数(带符号)
long / hex / ptr 64 位整数(无符号 hex)
bool bool(0=false, 非 0=true)
buf.N 内存缓冲区,长度由第 N 个参数(x0~x7)给出,输出前 32 字节
平台 安装命令 / 下载
Windows 仓库自带 example_trace/lz4.exe,直接用即可;或从 lz4/lz4 releases 下载 lz4_winXX.zip 解压加 PATH
macOS brew install lz4
Ubuntu / Debian sudo apt install lz4
Arch sudo pacman -S lz4
Fedora / RHEL sudo dnf install lz4
源码编译 git clone 729K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6D9P5U0c8Q4x3V1k6D9P5U0b7`. && cd lz4 && make && sudo make install
函数 签名 说明
xfqtrace_configure int(const char* json) 解析 JSON 配置,0=成功
xfqtrace_start int(void) 安装 hook 等待触发,0=成功
xfqtrace_stop void(void) 安全停止 trace(abort flag + 移除 hook)
xfqtrace_run void(x0..x7) 直接 trace 入口(外部 hook 模式)
xfqtrace_set_done_callback void(void(*)()) trace 完成回调(函数指针,无法 JSON 化)
xfqtrace_get_last_error const char*(void) 取最近错误信息
[libxftest.so 0x220d4] 0x774524b0d4: "sub sp, sp, #0x1e0" ; sp=0xb400007751928f80  => sp=0xb400007751928da0
[libxftest.so 0x220d8] 0x774524b0d8: "stp x29, x30, [sp, #0x1c0]" ; x29=0xb400007751929000 x30=0x2a sp=0xb400007751928da0
(w 8) 0xb400007751928f60  00 90 92 51 77 00 00 b4                           |...Qw...        | [0xb400007751929000]
(r 8) 0x7705331028        c5 27 e0 25 f3 c5 cd ba                           |.'.%....        | [0xbacdc5f325e027c5]
(r 1) 0xb400007707b36e9c  73                                                |s               | [0x0000000000000073]
=============== -> call: libxftest.so::sub_0x3fb30(x0=0xb400007707b36e88, x1=0xffffffffffffffff, x2=0x0, ...) ===============
...
=============== <- libxftest.so::sub_0x3fb30() ret: 0x16 ===============
=============== -> call: libc.so::open(path="/data/local/tmp/qtrace_t28514.tmp", flags=0x242, mode=0x1a4) ===============
=============== <- libc.so::open(...) ret: -1 ===============
=============== -> call: libc.so::clock_gettime(clk_id=1, tp=0xb400007751928ed0) ===============
=============== <- libc.so::clock_gettime(...) ret: 0x0 (0) ===============
=============== -> call: libc.so::getuid() ===============
=============== <- libc.so::getuid() ret: 10265 ===============
=============== -> svc::openat(dfd=AT_FDCWD, filename="/proc/self/maps", flags=0x0, mode=0x0) ===============
=============== <- svc::openat => 0x5 ===============
[THREAD_START #1] tid=28514 entry=0x220d4 fork_insn_index=36232369
parent_tid=28447
child_tid=28514
start_routine=0x774524b0d4
fork_insn_index=36232379
completed=1
[THREAD_START #1] tid=28514 entry=0x220d4 fork_insn_index=36232369
[libxftest.so 0x220d4] 0x774524b0d4: "sub sp, sp, #0x1e0" ; sp=0xb400007751928f80  => sp=0xb400007751928da0
[libxftest.so 0x220d8] 0x774524b0d8: "stp x29, x30, [sp, #0x1c0]" ; x29=0xb400007751929000 x30=0x2a sp=0xb400007751928da0
(w 8) 0xb400007751928f60  00 90 92 51 77 00 00 b4                           |...Qw...        | [0xb400007751929000]
(w 8) 0xb400007751928f68  2a 00 00 00 00 00 00 00                           |*.......        | [0x000000000000002a]
...
=============== -> call: libxftest.so::sub_0x40230(x0=0xb400007751928f18, x1=0x242, x2=0x1a4, ...) ===============
[libxftest.so 0x222e8] 0x774524b2e8: "bl #0x1df48" ; sp=0xb400007751928da0  => lr=0x774524b2ec
[libxftest.so 0x40230] 0x7745269230: "adrp x16, #28672" => x16=0x7745270000

=============== -> call: libc.so::open(path="/data/local/tmp/qtrace_t28514.tmp", flags=0x242, mode=0x1a4) ===============
[libxftest.so 0x4023c] 0x774526923c: "br x17" ; x17=0x7a672dae7c
=============== <- libc.so::open(path="/data/local/tmp/qtrace_t28514.tmp", flags=0x242, mode=0x1a4) ret: -1 ===============

=============== <- libxftest.so::sub_0x40230() ret: 0xffffffff ===============
Trace 线程 → buf[active] → swap → IO 线程 write(fd, buf[standby])
=============== -> svc::openat(dfd=AT_FDCWD, filename="/proc/self/maps", flags=0x0, mode=0x0) ===============
=============== <- svc::openat => 0x5 ===============
"hook_format": {
    "args": "env,obj,jstr,jstr,jstr,jstr,jstr",
    "ret": "jstr",
    "naming_source": 1, "naming_index": 0
}
var qcfg = {
    hook_backend: 1,
    target: { base: mod.base.toString(), offset: 0x20294 },
    output_format: 0,
    compression: { enable: true, level: 0 },
    max_traces: 1,
    hook_format: { args: "env,obj,jstr", ret: "jstr" },
    arg_filter: { idx: 2, value: 10401 }
};
xfqtrace_configure(JSON.stringify(qcfg));
xfqtrace_start();
python example_trace/demo_trace.py -p com.mfw.roadbook
# 1) push SO 到目标 app 私有目录(需 root)
adb push example_trace/libxfqtrace.so /data/local/tmp/
adb shell su -c "cp /data/local/tmp/libxfqtrace.so /data/data/com.mfw.roadbook/files/ && \
                 chown $(adb shell stat -c %u:%g /data/data/com.mfw.roadbook) /data/data/com.mfw.roadbook/files/libxfqtrace.so && \
                 chcon u:object_r:app_data_file:s0 /data/data/com.mfw.roadbook/files/libxfqtrace.so"

# 2) spawn 注入(冷启动场景)
frida -U -f com.mfw.roadbook -l example_trace/demo_trace.js --no-pause

# 或者 attach 已运行进程
frida -U -n com.mfw.roadbook -l example_trace/demo_trace.js
frida -UF -l example_trace/demo_trace.js
frida -U -p $pid -l example_trace/demo_trace.js

# 3) trace 完成后(看到 "trace done" / max_traces 命中),按 q 退出 frida

# 4) 手动 pull + 解压
adb shell su -c "cp /data/data/com.mfw.roadbook/files/trace_logs/*.lz4 /data/local/tmp/"
adb pull /data/local/tmp/ ./trace_logs/
example_trace/lz4.exe -d ./trace_logs/xfqtrace_*.log.lz4
# 只看 xfQTrace tag(最常用)
adb logcat -s xfQTrace:*

# 启动前清掉缓冲区,只看本次 trace 产生的日志
adb logcat -c && adb logcat -s xfQTrace:*

# 按目标进程 PID 过滤(多进程 app / 子进程注入时尤其有用)
adb logcat -s xfQTrace:* --pid=$(adb shell pidof com.foo)

# 写到文件,trace 完了离线翻
adb logcat -s xfQTrace:* > xfqtrace.log
pidcat -t xfQTrace com.foo
I xfQTrace: libxfqtrace injected! waiting for xfqtrace_configure/xfqtrace_start
I xfQTrace: hook backend set to: frida-gum
I xfQTrace: frida-gum backend initialized
I xfQTrace: qtrace target: lib=libxftest.so base=0x7745229000 offset=0x20294
I xfQTrace: qtrace armed: lib=libxftest.so raw=... offset=0x20294 address=0x77452492 ...
E xfQTrace: xfqtrace_configure: parse failed near ...
E xfQTrace: target base not set
E xfQTrace: hook backend 'frida-gum' failed to install hook at 0x...
E xfQTrace: hook args: env=..., this=Lcom/foo/Bar;, arg0="hello", arg1=0x1
E xfQTrace: hook return: "encrypted_xxx"
D xfQTrace: hook skip: x2=10086 (want 10401)
I xfQTrace: log path: /data/data/com.foo/files/trace_logs/xfqtrace_libxftest_7745229000_20294.log.lz4 (double-buffer 128MB x2, lz4=1 level=0)
I xfQTrace: trace file renamed: /data/data/com.foo/files/trace_logs/xfqtrace_libxftest_7745229000_20294_10401.log.lz4
I xfQTrace: qtrace_stop: hook removed, abort signaled
/data/data/<目标包名>/files/trace_logs/
├── xfqtrace_<so>_<base>_<offset>[_<naming>].log.lz4   ← 主线程 trace
├── xfqtrace_<so>_<base>_<offset>_t<tid>.log.lz4       ← 子线程 trace(如有)
└── xfqtrace_thread_<tid>.meta                          ← 父子线程关系元信息
const CONFIG = {
    target_so: "libmfw.so",
    engine_path: "/data/user/0/com.mfw.roadbook/files/libxfqtrace.so",
    qtrace: {
        hook_backend: 1,                      // 0=ShadowHook, 1=frida-gum
        target: { offset: 0x642bc },          // base 由脚本注入运行时模块基址
        output_format: 0,                     // 0=text, 1=binary
        compression: { enable: true, level: 0 },  // 0=fast, 1-12=HC
        max_traces: 1,
        hook_format: { args: "env,obj,jstr", ret: "jstr",
                       naming_source: -1, naming_index: 0 },
        // arg_filter: { idx: 2, value: 10401 },  // 可选
        // memory_trace: false,                    // 可选
    },
};
public native Object doCommandNative(int cmd, Object[] args);
// 大麦 doCommandNative,只 trace cmd=10401(订单签名相关)
const CONFIG = {
    target_so: "libsgmainso-6.7.250903.so",
    engine_path: "/data/user/0/cn.damai/files/libxfqtrace.so",
    qtrace: {
        hook_backend: 1,
        target: { offset: 0x5b198 },              // doCommandNative
        hook_format: {
            args: "env,obj,int,jobj",             // JNIEnv*, jclass, int cmd, jobjectArray args
            ret:  "jobj",
            naming_source: -1, naming_index: 0,
        },
        arg_filter: { idx: 2, value: 10401 },     // 仅当 x2 == 10401 才 trace
        max_traces: 1,
    },
};
qtrace: {
    target: { offset: 0x5b198 },
    hook_format: {
        args: "env,obj,int,jobj",
        ret:  "jobj",
        naming_source: 0,       // 0=从参数取
        naming_index: 2,        // 取 x2 (cmd) 作为后缀
    },
    max_traces: -1,             // 不限次数
    // 不加 arg_filter,所有 cmd 都收
}
hook_format: {
    args: "ptr,buf.2,long",   // x1 是缓冲区指针,长度由 x2 给出
    ret:  "int",
}
// 比如 SSL_write(SSL* ssl, const void* buf, int num)
hook_format: {
    args: "ptr,buf.2,int",
    ret:  "int",
}
qtrace: {
    target: { lib: "libsgmainso-6.7.250903.so", offset: 0x5b198 },
    // 不需要 target.base,SO 内部 dlopen + dlsym 解析
}
qtrace: { memory_trace: false }
qtrace: {
    compression: { enable: true, level: 9 },  // 9=HC 推荐
}
# 解压
lz4 -d trace.log.lz4 trace.log
lz4 -d trace.log.lz4              # 同名输出 trace.log
lz4 -dc trace.log.lz4 | less      # 不落盘直接看,适合电脑存储吃紧、只想快速检查 trace 内容时使用
lz4 -dc trace.log.lz4 | more      # Windows 上没有 less,用 more

# 解压整个 trace_logs 目录
for f in example_trace/trace_logs/*.lz4; do lz4 -d "$f" "${f%.lz4}"; done

# Windows (PowerShell)
Get-ChildItem .\example_trace\trace_logs\*.lz4 | ForEach-Object {
    .\example_trace\lz4.exe -d $_.FullName ($_.FullName -replace '\.lz4$','')
}
var rc = configure(Memory.allocUtf8String(json));
if (rc !== 0) {
    console.log("[-] " + get_error().readCString());  // 看具体原因
    return;
}
g_done_callback = new NativeCallback(function() {
    send({type: "trace_done"});
}, "void", []);
set_done_cb(g_done_callback);

// 收尾时:先解绑再置空
set_done_cb(ptr(0));
g_done_callback = null;

2万字长文,写了给我累死了,本来可以直接随便写写就发的,但是还是写好一些吧,唉,我这算是完美主义者么?

当然有人会杠说我测试不够多啥的,对此我只能说事实胜于雄辩,不信就自己测试。

哦因为介绍了我自己的trace工具,可能还会有人说软广啥的,随便说,好的工具值得拿出来宣传,而且我自己也把优化点分享出来了(我觉得做到这一步已经很好了),有动手能力的都能自己写一个trace工具。

注:AndroidPrybar 崩溃位置在 libtrace.so 偏移 0x510a2c–0x5666c8 之间 8 帧(SIGSEGV / SEGV_MAPERR / fault addr 0x0),作者 README 也直接写了"代码让ai改崩溃了,一些功能不太稳定"。

vmtrace 接口和其他几家不同:它要求"Interceptor.replace 目标函数 → callback 里调 qbdi_vm_call(targetAddr, argsPtr, argNum, logPath, dumpMem) → 返回结果",等于把"切入 trace VM"和"调用目标"绑成了一次调用。某种程度上更直观(一行 trace 一个函数),但只能从函数边界进入,且接入方需要自己组装参数指针数组(参数个数填错会崩)。

当然这就是 demo 上的单点数据,实战中不同样本(深递归、大量子线程、自定义 linker、JNI 调用密集型等)相对差距会变。bench 脚本和 demo 已经放在仓库 trace_compare/bench/ 下,欢迎自己换样本复现。

最近一两个月,先是休息了半个月,打打游戏啥的,然后实在是压力太大了,一天不逆向浑身难受啊,跟蚂蚁在爬一样,然后就开始逆向了。最近一个月在学习密码学经典算法的设计原理相关的内容,感觉学起来还是有点难度的,很有意思。后续会用remotion来创作高质量视频,然后发布到b站给大家补点相关基础,放心讲的会非常非常详细,设计原理/手搓实现/汇编特征/魔改怎么逆等等;

然后平时偶尔逆向一下实战案例,一些安全SDK那种,过程中发现很多案例用unidbg来补会有一些问题,虽然中等难度的app大多都可以解决,但是稍微再难的就不方便处理了,与其耗时在深度魔改unidbg让其更像一个android,不如直接写一个优秀的真机trace工具;反正一个信息很全的trace日志,ai只需要使用ripgrep就可以暴力分析了,然后我们拿到分析结果之后就可以开始复现,这样可以利用ai当老师然后不断追问来进行学习,哪怕是vmp的案例也是照样逆完学习;

所以就会有明显痛点了,开源的真机trace好像有很多问题,而且直接用别人开源的成品好像原理啥的我也掌握不到位呀,俺是来学习的又不是来直接用成品的。所以一周前我开始学习qbdi,然后顺便开始开发我自己用的真机trace工具,连续3天睡眠加起来不超过14小时吧,也是非常抽象了,也是压根睡不着*几发还是一样哈哈哈哈;好在最近几天已经完工了,可以美美休息几天了。

然后感觉自己做的比开源的工具对比下来会有很多明显优点吧,自己也倾注了很多心血,而且后续还会支持俺的ttd;然后我认为单独收费卖感觉不太好,因为其实技术含金量不是很高,重要的是怎么思考到这些优化点,优化的点都是我一步步思考出来的; 那为啥我不打算免费发出来呢?

  • 一是白嫖的确实太多了,虽然我不讨厌白嫖,但是人要懂得知恩图报;很多人用了我开源的很多魔改工具或者明显参考了我的实战案例分享,但在自己分享的文章中压根连我名字都不愿意提,这让我有一点寒心呐。
  • 二是我的文章前段时间还 被盗了,那我的成品发出来不也会被盗么?大部分时候做的越好功劳越被抢走,做的但凡有一点不好使用反而还有傻逼跑过来骂我(身边认识的人已经有很多这种案例了)
  • 三是我最近发现有很多机构还有个别人会把别人的成品做一层包装,然后拿去授课用以及卖钱,这是我所绝对不能接受的,比如某灵机构/哆啦安全公众号等等。

基本上就是这三点,寒了多少愿意开源的朋友的心。所以仔细考虑下来,就打算把工具免费丢到 星球 了。当然你也可以认为我是为了引流吧,我承认有这一部分原因,但更多的是心确实已经寒了。(另外其实我在b站已经送出去很多朋友 星球 了,都是愿意分析文章或者工具或者都愿意点拨我的)

那发到 星球 还有什么好处?

  • 方便维护:保证用的人都会认真看我的文章,不会再出现开源之后 傻逼不阅读readme就来加我qq甚至打听我微信一顿问的情况。
  • 一周年福利:这样之前进了 星球 的朋友也不需要花钱照样就能使用了,也就当是发视频/创作 一周年一来各位朋友的支持吧;

因为诚实来说俺也没啥稳定收入,平时也不接点单,也没工作,现在就靠 星球 赚点生活费,能靠这样能引流几个人进去也算不错了;我个人认为就这样处理最妥当,如果还是被喷的话那就随他吧,反正这圈子遇到的神人确实还挺多的哈哈哈哈;

但是!!有很多朋友仍然非常支持我,我觉得我也要做点什么!所以我会把这个工具的实现以及优化思考全都放出来吧,这样即使各位不愿意进 星球 的朋友依旧可以拿本文喂给ai让他对开源的真机trace进行改造,具体见后文介绍(点击跳转);

但也不是越底层越好的,要综合评估,如性能开销还有实际效果,最底层的trace并不是适用于任何场景。

正所谓: 器本自通流,制器有来由,他铸合他求,探源得真枢,握得本根在,随心任去留。

但是俺用jnitrace经常崩溃,我又懒得去改源码,所以就直接在后面的trace中实现这个功能就好了;

当然你也可以认为我是在坚守道义。其实我早在 2025 年 8 月就开始用各种反编译 mcp 工具,11 月起也开源了不少逆向相关的 AI 工具,比如抓包工具 LunFengChen/proxypin-mcp-server

幂等:同一操作执行 N 次,效果跟执行 1 次一样。第一次 stop 真干活,后续再调内部检查"已经停过"直接返回,不会 double free / 重复 close / 崩。
好处:Frida 端多个清理路径(rpc.stop / 退出 hook / catch 块)不用各自维护"停过没"的状态;done_callback 触发后已经自动停过一次,Python 收到信号再保险调一次也安全。

项目 zgy0x01/QTrace看雪文章,id:乐子人) lidongyooo/GumTrace看雪文章,id:lidongyooo) jiqiu2022/vm-trace-release看雪文章,id:棕熊) GitKittys/AndroidPrybar(无看雪文章) LunFengChen/xfQTrace(本文,id:x1a0f3n9)关 memory hexdump 开 memory hexdump
开源 开源 开源 仅成品 仅成品(疑似后续开源) 仅成品,星球 成员免费使用,不额外单独收费
引擎 QBDI VM(动态 DBI) frida-stalker(动态重编译) QBDI VM(动态 DBI) Unicorn VM(仿真执行) QBDI VM(动态 DBI) QBDI VM(动态 DBI)
wall_ms 294 s 32.0 s 83.1 s 崩溃 @ ~110 s(未完成) 19.3 s 86.9 s
处理速度 12.3 万 insn/s 113 万 insn/s 43.5 万 insn/s 失败 187 万 insn/s 41.6 万 insn/s
raw 字节 2.55 GB 3.71 GB 2.29 GB 2980 MB 4527 MB
落盘字节 2.55 GB(无压缩) 3.71 GB(无压缩) 2.29 GB(无压缩) 0(崩溃,缓冲未刷盘) 272 MB(LZ4,91% 压缩比) 351 MB(LZ4,92% 压缩比)
raw 吞吐 8.9 MB/s ≈ 0.52 GB/min 119 MB/s ≈ 6.95 GB/min 28.2 MB/s ≈ 1.65 GB/min 154 MB/s ≈ 9.05 GB/min 52.1 MB/s ≈ 3.05 GB/min
落盘吞吐 8.9 MB/s ≈ 0.52 GB/min 119 MB/s ≈ 6.95 GB/min 28.2 MB/s ≈ 1.65 GB/min 14.1 MB/s ≈ 0.83 GB/min 4.0 MB/s ≈ 0.24 GB/min
稳定性 完成 完成 完成 中段 SIGSEGV,VM 内部空指针 完成 完成
Provider 覆盖范围
JNI 全套 JNIEnv->* 函数(GetXxxField / CallXxxMethod / NewStringUTF / GetStringUTFChars 等)
Libc malloc / memcpy / strncpy / pthread_* / dlopen / dlsym ...
Crypto OpenSSL / BoringSSL:SHA256_Update / AES_* / EVP_* / HMAC_*
Protobuf WireFormatLite / 序列化反序列化入口
STL std::string / std::vector / std::map 等容器操作
Network socket / send / recv / SSL_read / SSL_write
Compress gzip / zlib / zstd / lz4 (函数级)
Syscall NR 0~291(直接走 SVC 的)
参数 说明
-p 目标包名(必填)
--attach 附加到已运行进程(默认 spawn 冷启动)
--no-push 跳过 push SO,复用设备上已有的 libxfqtrace.so
--pull-only 只拉文件 + 解压,不跑 frida(trace 已经跑完)
--reinstall 卸载重装 APK 重置设备指纹(传 APK 路径)
--serial 指定 ADB 设备(多设备时)
字段 类型 说明
hook_backend int 0=ShadowHook, 1=frida-gum (默认)
target.lib string 目标 SO 名(与 target.base 二选一)
target.base string 目标 SO 基址,支持 "0x..." 或十进制(与 target.lib 二选一)
target.offset int / "0x..." 目标函数偏移
output_format int 0=text(.log)
1=binary(.xfqtrttd, 开发中,⏳ 暂不可用,后续需要搭配俺开发的时间旅行调试器)
memory_trace bool 内存读写追踪开关(用不上可以关掉)
compression.enable bool LZ4 压缩开关,默认开
这个很有用,如果你认为trace文件大一点不影响的话可以关掉
compression.level int 0=fast(默认), 1-12=HC 高压缩
max_traces int 最大 trace 次数, -1=无限
-> 看你想要采集多少次trace
arg_filter.idx int 参数过滤索引(命中才 trace,见后续使用示例)
arg_filter.value int / "0x..." 参数过滤值
hook_format.args string 参数类型, 逗号分隔, 见HookArgFormatter
hook_format.ret string 返回值类型
hook_format.naming_source int 命名来源 (-1=禁用, 0=参数, 1=返回值)
hook_format.naming_index int naming_source=0 时取第几个参数当文件名后缀
标签 含义
env / _ 占位(不输出,用于跳过 JNIEnv* / jclass
jstr JNI String,自动 GetStringUTFChars,失败 fallback 到 toString()
obj JNI 对象,输出 getClass().getName()
jobj / jmap JNI 对象,输出 toString()(适合 Map/List/自定义对象)
jbarr byte[],输出 byte[N]{XX XX XX...} 前 16 字节 hex
cstr C 字符串 (char*),自动读 null-terminated
int 32 位整数(带符号)
long / hex / ptr 64 位整数(无符号 hex)
bool bool(0=false, 非 0=true)
buf.N 内存缓冲区,长度由第 N 个参数(x0~x7)给出,输出前 32 字节
平台 安装命令 / 下载
Windows 仓库自带 example_trace/lz4.exe,直接用即可;或从 lz4/lz4 releases 下载 lz4_winXX.zip 解压加 PATH
macOS brew install lz4
Ubuntu / Debian sudo apt install lz4
Arch sudo pacman -S lz4
Fedora / RHEL sudo dnf install lz4
源码编译 git clone a59K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6D9P5U0c8Q4x3V1k6D9P5U0b7`. && cd lz4 && make && sudo make install

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 38
支持
分享
最新回复 (16)
雪    币: 11359
活跃值: (9615)
能力值: ( LV12,RANK:250 )
在线值:
发帖
回帖
粉丝
2
严肃学习
15小时前
0
雪    币: 1202
活跃值: (909)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
3
来支持了
15小时前
0
雪    币: 124
活跃值: (1550)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
资瓷一个
15小时前
0
雪    币: 1892
活跃值: (3106)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
5
看看
14小时前
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
tql
14小时前
0
雪    币: 158
活跃值: (4771)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
支持
13小时前
0
雪    币: 5290
活跃值: (5534)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
6666
13小时前
0
雪    币: 104
活跃值: (8607)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
tql
12小时前
0
雪    币: 666
活跃值: (1010)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
1
12小时前
0
雪    币: 568
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
666
11小时前
0
雪    币: 17
活跃值: (1486)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
666
11小时前
0
雪    币: 862
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
13
狠狠地严肃学习
8小时前
0
雪    币: 6149
活跃值: (7695)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
QBDI VM 会被检测到吗?比如目标函数会检查返回地址来检查PC地址是否在自身的模块里。
6小时前
0
雪    币: 8830
活跃值: (5568)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
辛苦了
5小时前
0
雪    币: 8830
活跃值: (5568)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
辛苦了
5小时前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
17
666
4小时前
0
游客
登录 | 注册 方可回帖
返回