FART 框架的修复组件与 VMP 还原
FART 脱壳后的 DEX 问题分析
FART 脱壳虽然能够自动 dump DEX 数据,但 dump 出来的结果并不总是完美的。根据目标应用使用的壳类型不同,脱壳后可能存在以下两类主要问题:
DEX 文件不完整
某些壳程序(特别是新一代的加固方案)不会一次性将完整的 DEX 加载到内存中,而是按需加载——只有当某个类被实际使用时,才将其对应的 DEX 数据解密到内存。FART 在 LinkClass 阶段只能 dump 已经加载过的类,对于那些尚未被使用的类,其 DEX 数据可能仍然处于加密状态或尚未被加载。
常见表现:
- dump 出的 DEX 文件大小小于原始 DEX
- 使用
dexdump分析时发现部分类定义缺失 - 反编译工具报错:“classes.dex is not a valid dex file”
方法字节码缺失
这是 DEX 抽取壳的典型问题。壳程序将方法体字节码从原始 DEX 中抽取出来,替换为空方法壳。FART 在 LinkClass 阶段 dump 方法字节码时,某些方法可能还没有被回填,导致 dump 到的字节码仍然是空壳(通常只是一条 return 指令)。
常见表现:
- 反编译后看到大量空方法体
jadx等工具显示// Method was not decompiled- FART 的方法 dump 文件中,部分方法的
insns_size为 1(只有一条 return 指令)
dex_fix 修复工具的使用方法与原理
FART 项目自带了一个修复工具 dex_fix,专门用于修复 FART dump 后存在问题的 DEX 文件。
dex_fix 的工作原理
dex_fix 的核心思路是利用 FART 在 LinkClass 阶段 dump 的方法级字节码数据,将其回填到 dump 的 DEX 文件中对应的方法体位置。具体步骤如下:
- 读取 FART dump 的 DEX 文件
- 解析 DEX 文件格式,找到每个方法的
code_item结构 - 读取 FART dump 的方法字节码文件(
.txt格式) - 将 dump 的字节码数据回填到 DEX 文件的
code_item中 - 重新计算 DEX 的
checksum和signature - 输出修复后的 DEX 文件
code_item 回填技术
DEX 文件中,方法的字节码存储在 code_item 结构中。理解 code_item 的格式是修复工作的关键:
code_item 结构(以 DEX35 为例):
+0x00: uint16_t registers_size // 使用的寄存器数量
+0x02: uint16_t ins_size // 输入参数占用的寄存器数
+0x04: uint16_t outs_size // 调用其他方法时需要的参数寄存器数
+0x06: uint16_t tries_size // try/catch 块数量
+0x08: uint32_t debug_info_off // 调试信息偏移
+0x0C: uint32_t insns_size // 指令数量(单位:16位)
+0x10: uint16_t insns[insns_size] // 方法字节码
+...: try_item[tries_size] // try 块信息
+...: encoded_catch_handler_list // catch handler 信息
回填过程需要修改的关键字段是 insns_size(指令数量)和 insns(指令数据本身)。由于回填后指令长度可能发生变化,code_item 的总大小也可能改变,这需要调整 DEX 文件中后续数据的偏移量。
dex_fix 使用示例
FART dump 后,在 /sdcard/fart/ 目录下会生成以下文件:
/sdcard/fart/
├── com.example.target_0.dex # dump 的完整 DEX
├── com.example.target_0_method.txt # 所有方法的字节码 dump
└── com.example.target_1_method.txt # 另一个 DEX 的方法 dump
方法 dump 文件的格式通常为:
class_name: Lcom/example/TargetClass;
method_idx: 123
code_off: 0x1a2b
insns_size: 30
bytecode: 0e00 1a00 6e20 3200 ...(16位指令序列)
---
class_name: Lcom/example/AnotherClass;
method_idx: 456
...
使用 dex_fix 进行修复的流程:
# 将 dump 文件拉取到 PC
adb pull /sdcard/fart/ ./fart_dump/
# 运行 dex_fix 修复
python dex_fix.py \
--dex ./fart_dump/com.example.target_0.dex \
--methods ./fart_dump/com.example.target_0_method.txt \
--output ./fart_dump/fixed.dex
# 验证修复结果
dexdump -d ./fart_dump/fixed.dex | head -50
# 使用 jadx 反编译验证
jadx -d ./output/ ./fart_dump/fixed.dex
手动修复 DEX 的方法
当 dex_fix 自动修复失败时,可以尝试手动修复。手动修复的核心步骤如下:
第一步:分析 DEX 结构
使用 dexdump 工具分析 dump 的 DEX 文件:
dexdump -d target.dex > dexdump_output.txt
重点关注以下信息:
- 类的数量是否完整(对比原始 APK 的类列表)
- 空方法体的位置和数量
- DEX 文件的 checksum 是否正确
第二步:定位需要修复的方法
在 FART 的方法 dump 文件中,找到需要修复的方法的字节码数据:
# 从 dump 文件中提取方法的 code_off 和 bytecode
class_name: Lcom/example/TargetClass;
method_idx: 42
code_off: 0x00001a40
insns_size: 128
bytecode: 12 00 1a 00 6e 20 32 00 0c 00 ...
第三步:修改 DEX 文件
使用十六进制编辑器(如 010 Editor 或 HxD)打开 DEX 文件,定位到 code_off 指示的偏移位置,将新的字节码数据写入:
DEX 文件偏移 0x1a40:
原始数据: 0e 00(return-void,空方法壳)
修复后: 12 00 1a 00 6e 20 32 00 ...(完整字节码)
第四步:修复 DEX 校验和
DEX 文件头中有两个字段需要更新:
- SHA-1 签名(偏移 12-31):对 DEX 文件从偏移 32 到文件末尾计算 SHA-1
- Adler-32 校验和(偏移 8-11):对 DEX 文件从偏移 12 到文件末尾计算 Adler-32
import hashlib
import zlib
def fix_dex_checksum(dex_path):
with open(dex_path, 'rb') as f:
data = bytearray(f.read())
# 清除旧的签名校验
for i in range(32, len(data)):
pass # SHA-1 计算范围:32 到末尾
# 计算新的 SHA-1 签名
sha1 = hashlib.sha1(bytes(data[32:])).digest()
data[12:32] = sha1
# 计算新的 Adler-32 校验和
checksum = zlib.adler32(bytes(data[12:])) & 0xFFFFFFFF
data[8:12] = checksum.to_bytes(4, 'little')
with open(dex_path, 'wb') as f:
f.write(data)
FART 在 VMP 分析中的应用
VMP 程序的特征识别
VMP(Virtual Machine Protection)保护的程序具有以下典型特征:
- 方法体巨大:被 VMP 保护的方法字节码远大于正常方法,因为包含了自定义 VM 的解释器
- 大量 switch-case:反编译后可以看到大型的 switch 分支结构(VM 分发器)
- 寄存器使用异常:VMP 保护的方法使用大量寄存器进行 VM 状态管理
- 字符串加密:代码中的字符串被加密,运行时由 VM 解密
- native 方法增多:部分 VMP 实现将 VM 解释器放在 native 层
通过 FART 的 dump 数据,可以快速识别 VMP 保护的类和方法——凡是字节码异常庞大、结构不符合常规 Dalvik 指令模式的方法,大概率是 VMP 保护的方法。
通过 dump 定位 VMP Handler
FART dump 的方法级数据对于分析 VMP 特别有价值。分析流程如下:
- 筛选可疑方法:从 FART dump 的方法列表中,筛选
insns_size异常大的方法 - 提取字节码:将可疑方法的字节码提取到独立文件
- 模式匹配:分析字节码中的指令模式,识别 VMP 的分发循环(dispatch loop)
- 定位 handler 表:在分发循环中找到 VM 操作码与处理函数的映射关系
# 从 FART dump 文件中筛选大方法
def find_large_methods(dump_file, threshold=500):
"""筛选指令数超过阈值的方法"""
with open(dump_file, 'r') as f:
content = f.read()
methods = content.split('---')
large_methods = []
for method in methods:
lines = method.strip().split('\n')
for line in lines:
if line.startswith('insns_size:'):
size = int(line.split(':')[1].strip())
if size > threshold:
large_methods.append((method.split('\n')[0], size))
return large_methods
从 FART dump 数据还原 VMP 保护逻辑
VMP 还原是一个系统性的逆向分析过程,FART 的 dump 数据提供了关键的原始材料:
第一步:理解 VM 架构
通过分析 VMP 分发器的字节码,确定以下信息:
- VM 使用多少个虚拟寄存器
- VM 的操作码格式(定长还是变长)
- VM 的 PC(程序计数器)如何管理
- VM 有多少个操作码(handler 数量)
第二步:提取操作码定义
从 dump 的字节码中提取每个操作码对应的处理逻辑:
# 分析 VMP 操作码分布
def analyze_vm_opcodes(bytecode):
"""分析字节码中的操作码使用频率"""
opcodes = {}
for insn in bytecode:
opcode = insn & 0xFF # 假设操作码在低 8 位
opcodes[opcode] = opcodes.get(opcode, 0) + 1
return opcodes
第三步:语义还原
根据操作码的定义和参数,还原每个操作码的语义含义。将自定义 VM 的指令逐条翻译回等效的 Dalvik 字节码或高级语言伪代码。
第四步:重建原始逻辑
将还原后的 Dalvik 字节码重新组装成可分析的方法,替换原来的 VMP 保护代码。这一步通常需要手动完成,因为自动化还原的准确性有限。
常见修复问题与解决方案
修复后 DEX 仍然报错
如果修复后的 DEX 文件使用 dexdump 或 jadx 仍然报错,可能的原因和解决方案:
- code_item 大小不匹配:回填的字节码长度与原始
code_item空间不一致,导致后续数据错位。需要重新分配code_item空间。 - try/catch 信息丢失:部分壳在抽取方法体时也修改了异常处理表。需要手动恢复或移除无效的 try/catch 信息。
- 寄存器数量不正确:回填字节码后,
registers_size、ins_size、outs_size可能需要调整以匹配新的字节码。
FART dump 数据不完整
如果 FART 只 dump 了部分类的数据,可能的原因:
- 壳的多阶段加载:壳程序分多个阶段加载 DEX,FART 只捕获了第一阶段的 dump。解决方案:让应用运行更长时间,触发更多类加载。
- 延迟加载:某些类只在特定用户操作后才加载。解决方案:遍历应用的各个功能页面,触发更多的类加载。
- 反 dump 检测:壳程序检测到了 dump 操作并终止了加载。解决方案:分析壳的反 dump 逻辑并绕过。
总结
FART 脱壳后的 DEX 修复工作是整个脱壳流程中不可或缺的一环。dex_fix 工具和 code_item 回填技术能够有效解决 DEX 抽取壳导致的方法体缺失问题。对于 VMP 保护的程序,FART 的方法级 dump 数据为 VM 指令集分析和逻辑还原始提供了宝贵的原始材料。掌握 DEX 文件格式、code_item 结构和校验和修复技术,是成功完成脱壳后修复工作的关键。