论坛首页 逆向工程技术区 阅读主题

[原创]ARM64 动态指令 trace 离线向后切片分析器 —— trace-slice

287 浏览 6 回复
#1 楼主 2026-06-01 21:09:11
ARM64 动态指令 trace 离线向后切片分析器项目地址:trace-slice 逆向算法还原中,目标二进制(安卓 SO 库、IoT 固件、桌面应用等)常常被 VMP(虚拟机保护)、CFF(控制流平坦化)、OLLVM 等混淆方案层层加固,静态分析几乎无法看清算法逻辑。逆向工程师通常借助 unidbg 等模拟执行框架,对目标函数进行动态指令级 trace,记录每一条 ARM64 指令的执行过程以绕过混淆。然而,这样产生的 trace 文件动辄几千万行、数 GB 大小。在这些海量指令中,大量是混淆引入的膨胀代码——虚拟机调度循环中的寄存器搬运、控制流平坦化的跳转分发、与目标值无直接关联的中间计算等。要从中还原出算法的真实计算逻辑,人工逐行分析虽然并非不可能,但异常困难且极度耗时,自动化工具能从 trace 中直接提取出与目标值相关的指令子集,将分析效率提升几个数量级。trace-slice 实现了向后数据流切片(backward slicing):给定一个"你关心的值"——某个寄存器的最终状态或某个内存地址的内容——工具会自动从该值出发,沿数据依赖链反向追踪,找出所有直接或间接参与该值计算的指令,输出仅包含这些指令的最小子集。所有与目标值无关的指令(混淆代码、无关寄存器操作等)都被自动过滤掉。向后数据流切片(backward slicing),自动去除与目标值无关的全部指令字节粒度内存依赖追踪,精确处理不同宽度的 load/store(1/2/4/8/16 字节及 SIMD 128 位)覆盖全部 150+ ARM64 助记符(40 个语义类别),包括 SIMD/NEON、原子操作等可选控制依赖追踪(条件分支影响分析),通过 --with-control-dep 启用值相等性剪枝(默认启用):当 LOAD 读取的值与对应 STORE 写入的值完全相等(纯值搬运)时,自动剪除 LOAD 的地址计算依赖链,大幅减少 VMP 地址噪音。可通过 --no-prune 禁用高性能,实测 unidbg 的 2400 万行 2.8GB trace 日志文件,默认模式(含值剪枝)切片耗时约 8s,切片结果从 258 万行进一步精简至 128 万行(-50.3%)向后切片(backward slicing)是一种经典的程序分析技术,由 Mark Weiser 于 1981 年首次提出(ICSE'81),并在 1984 年的 IEEE TSE 论文 "Program Slicing" 中系统阐述。它的目标很简单:给定程序中某个变量的某次取值,找出所有影响了这个值的语句。可以用刑侦溯源来类比:你知道了案件的结果(某个寄存器最终存放的值),现在需要追查这个值是怎么一步步计算出来的——哪些指令提供了原始数据,哪些指令做了运算,哪些指令把中间结果搬到了最终位置。只有真正参与了这条"因果链"的指令才会被保留,其余全部排除。与正向分析形成对比:正向分析是"跟踪所有变量,看每条指令会产生什么影响",需要关注全局状态;而向后切片是"从结果反推,只保留真正参与计算的指令",天然具有过滤能力。关键洞察在于:一条两千万行的 trace 中,与某个特定输出值相关的指令通常只占原始行数的一成左右,向后切片能精准地把这部分指令提取出来。以下是 5 行 unidbg 格式的 ARM64 trace(为说明原理而简化):逐行说明:目标:追踪 x0 的值从何而来逐步反向追踪过程:结论:5 行全部参与了 x0 的计算,全部保留在切片结果中。依赖关系图(箭头表示"依赖于"):在上面的例子中,假设第 2 行和第 3 行之间插入了一行无关指令:这行指令给 x15 赋值为 999,但在 x0 的整条计算链中,x15 从未被使用过。从第 5 行出发反向追踪时,永远不会触及这条指令,因此它被自动排除在切片结果之外。这就是向后切片的核心价值所在。在真实的 VMP 混淆 trace 中,大量指令与你所关心的目标值没有数据依赖关系。向后切片能自动识别并过滤掉这些无关指令——实测 2400 万行的 trace 切片后保留约 258 万行(10.8%)。在此基础上,值相等性剪枝进一步将结果精简至约 128 万行(5.4%),剪除了大量 VMP 地址搬运噪音(详见第 8 节更新日志)。需要注意的是,切片保留的是所有与目标值存在数据依赖的指令,其中仍会包含 VMP 虚拟机调度循环中参与数据传递的指令(如寄存器搬运、虚拟栈操作等),这些并非核心算法逻辑但无法被数据流切片去除。数据依赖(默认启用)数据依赖描述的是值的直接传递关系:一条指令读取了某个寄存器或内存地址,而这个寄存器或内存地址的当前值是由之前的某条指令写入的。这是最核心、最基础的依赖关系,涵盖了寄存器赋值和内存读写两大类。控制依赖(通过 --with-control-dep 启用)控制依赖描述的是条件分支对指令执行的影响:某条指令是否执行,取决于之前某个条件分支的判断结果。用 3 行示例说明:大多数场景下,仅使用数据依赖就足以提取核心算法逻辑。控制依赖适用于需要理解"为什么走了这条执行路径"的分析场景。trace-slice 的分析过程分为三步:Pass 1(正向扫描):从头到尾逐行扫描整个 trace 文件。对每一行,解析出指令的助记符和操作数,判断哪些寄存器/内存被读取(USE)、哪些被写入(DEF),然后记录行与行之间的依赖关系,构建完整的依赖图。BFS 反向切片:从用户指定的目标(某个寄存器或内存地址在某一行的值)出发,沿依赖边进行反向广度优先遍历(BFS),标记所有传递可达的行。被标记的行就是与目标值相关的最小指令集合。Pass 2(输出):再次遍历 trace 文件,只将被标记的行写入输出文件,未被标记的行全部跳过。本工具默认解析 unidbg 模拟器输出的 ARM64 指令 trace 格式。每行记录一条指令的执行信息,包含时间戳、模块偏移、机器码、PC 地址、反汇编文本、寄存器输入/输出值以及内存操作注解等字段。一行完整的 trace 示例(无内存操作):各字段说明:带内存写入操作(store)的完整行示例:带内存读取操作(load)的完整行示例:本工具默认为 unidbg trace 设计,但满足以下条件的其他 trace 格式也能兼容。必要条件:修改前缀跳过长度:如果你的 trace 格式前缀长度不是 40 字节,需要修改源码中的跳过长度。具体位置在 src/parser.rs 文件的 find_disasm_with_pos 函数中:寄存器命名要求:必须使用 ARM64 标准寄存器名:最小合规示例:(X 代表任意非 " 字符的填充,总共 40 个字符)不合规示例(缺少内存注解):缺少 ; mem[WRITE] abs=0x...,工具无法识别内存写入,依赖链在此处断裂。RegId(u8 newtype):用一个字节紧凑编码 66 个 ARM64 寄存器 -- x0-x30(0-30)、sp(31)、xzr(32)、v0-v31(33-64)、nzcv(65)。作为 FxHashMap 键时仅占 1 字节,最大限度减少哈希开销。ParsedLine:单行 trace 的解析结果,包含助记符、操

...(已截断)

---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-290217.htm
#2 2026-06-01 21:09:11
感谢分享
#3 2026-06-01 21:09:11
感谢分享
#4 2026-06-01 21:09:11
我去感谢分享
#5 2026-06-01 21:09:11
tql
#6 2026-06-01 21:09:11
tql
#7 2026-06-01 21:09:11
感谢分享

请登录后参与讨论

立即登录 注册账号