FART & FRIDA,FART修复组件和辅助 VMP 分析
FART 与 Frida 的互补关系
FART 和 Frida 是 Android 逆向工程中两个极其重要的工具,它们在脱壳领域具有天然的互补关系。理解它们各自的优势和局限,以及如何将它们结合起来使用,能够大幅提升脱壳的效率和成功率。
FART 的优势与局限
FART 的优势:
- 系统级权限:FART 通过修改 AOSP 源码实现,运行在系统层面,拥有最高权限,可以访问虚拟机的所有内部数据结构
- 主动调用机制:能够在 DEX 加载后主动触发所有方法的执行,迫使壳程序完成
code_item的回填 - 自动化程度高:一次配置完成后,对所有使用抽取壳的 APP 都可以自动脱壳,无需针对每个壳单独编写脚本
- 对抽取壳效果显著:FART 的设计目标就是对抗抽取壳,在这方面的能力非常强
FART 的局限:
- 需要刷入自定义 ROM:必须使用 FART 编译的系统镜像,无法在普通手机上使用
- 支持的 Android 版本有限:FART 主要支持 Android 5.0-7.1,对于 Android 8.0+ 的新版本需要自行移植
- 部署成本高:需要有额外的测试设备,且刷机操作有一定门槛
- 对 VMP 壳效果有限:FART 只能 dump Dalvik 字节码,对于 VMP(虚拟机保护)将字节码翻译为自定义指令的情况无法处理
Frida 的优势与局限
Frida 的优势:
- 动态注入能力强:可以在运行时注入 JavaScript 代码到目标进程中,灵活地 hook 任意函数
- 无需刷机:只需要一台 root 手机即可使用,部署成本低
- 版本兼容性好:支持几乎所有 Android 版本
- 生态丰富:有大量的现成脚本和工具可供使用
Frida 的局限:
- 用户态权限受限:无法直接访问 ART 虚拟机的内部数据结构,需要通过已知的 exported API 间接操作
- 检测风险高:Frida 的注入行为容易被壳程序检测到,很多壳会主动检测并对抗 Frida
- 对抽取壳的直接脱壳能力有限:单纯的 Frida hook 难以在正确的时机 dump 完整的 DEX
互补关系
FART 和 Frida 的互补关系体现在:
FART 的优势 = Frida 的局限
Frida 的优势 = FART 的局限
- FART 解决 Frida 难以解决的问题:系统级的 DEX dump 和主动调用
- Frida 弥补 FART 的不足:灵活的运行时 hook、无需刷机的便捷性、版本兼容性
使用 Frida 增强 FART 脱壳能力
在实际脱壳实战中,将 Frida 与 FART 结合使用可以形成更强大的脱壳方案。
场景一:绕过 Frida 检测
许多抽取壳会检测 Frida 的存在。如果目标 APP 壳程序检测到 Frida,可能会拒绝运行或触发反制措施。FART 由于是系统级的修改,不受 Frida 检测的影响:
- 使用 FART 脱壳环境运行 APP,避免壳程序检测到 Frida
- APP 正常运行并完成 DEX 加载
- FART 自动 dump DEX 和收集方法指令
场景二:Frida 辅助 FART 的 dump 分析
FART dump 出 DEX 后,可以使用 Frida 进一步分析:
// Frida 脚本:分析 dump 的 DEX 中的方法信息
Java.perform(function() {
var DexFile = Java.use("dalvik.system.DexFile");
// 遍历已加载的类,检查方法是否完整
Java.enumerateLoadedClasses({
onMatch: function(className) {
try {
var clazz = Java.use(className);
var methods = clazz.class.getDeclaredMethods();
methods.forEach(function(method) {
var codeSize = method.getCodeSize();
if (codeSize === 0) {
console.log("[!] 方法可能被抽取: " + method.toString());
}
});
} catch(e) {}
},
onComplete: function() {}
});
});
场景三:Frida + 内存 Dump 作为 FART 的替代方案
在没有 FART 环境的情况下,可以使用 Frida 实现类似的内存 dump 功能:
// Frida 脚本:搜索内存中的 DEX 并 dump
function dumpDexFromMemory() {
var ranges = Process.enumerateRanges('r--');
ranges.forEach(function(range) {
try {
// 搜索 DEX magic: "dex\n035" 或 "dex\n036" 等
var results = Memory.scanSync(range.base, range.size, "64 65 78 0a 30 33");
results.forEach(function(match) {
// 读取 DEX file_size 字段(偏移 0x20 处,4 字节 little-endian)
var fileSize = match.address.add(0x20).readU32();
if (fileSize > 0 && fileSize < range.size) {
console.log("[+] Found DEX at " + match.address + ", size: " + fileSize);
// dump DEX
var dexData = match.address.readByteArray(fileSize);
var filename = "/data/local/tmp/dex_" + match.address + ".dex";
var file = new File(filename, "wb");
file.write(dexData);
file.close();
}
});
} catch(e) {}
});
}
// 在合适的时机调用
setTimeout(dumpDexFromMemory, 5000);
场景四:Frida Hook ART 函数辅助 FART
在 FART 环境中结合 Frida,可以更精细地控制脱壳过程:
// Frida hook LinkClass,在 FART dump 之前记录日志
Interceptor.attach(Module.findExportByName("libart.so",
"_ZN3art11ClassLinker9LinkClassEPNS_6ThreadEPNS_6mirror5ClassEPNS_6HandleINS_11ObjectArrayINS_6mirror5ClassEEEE"), {
onEnter: function(args) {
this.klass = args[2];
},
onLeave: function(retval) {
// LinkClass 完成后,检查类的状态
var klass = this.klass;
console.log("[FART] Class linked: " + klass.readPointer());
}
});
FART 修复组件的原理
FART 脱壳后输出的文件主要包括两部分:
- dump 出的 DEX 文件:可能存在
code_item不完整的问题 dex_method_insns文件:每个方法的指令数据
由于抽取壳的运作机制,FART dump 出的 DEX 文件中,某些方法的 code_item 可能存在异常。FART 修复组件(dex_fix 工具)的作用就是将收集到的 dex_method_insns 数据回填到 DEX 文件中,生成一个完整可用的 DEX。
为什么需要修复
FART dump 的 DEX 存在的问题主要有:
1. code_item 偏移量异常
FART dump DEX 是在内存中进行的,内存中的 DEX 数据可能与原始文件存在布局差异。特别是 code_item 在 DEX 中的偏移量可能已经被修改。
2. 指令数据不完整
虽然在主动调用后壳程序会回填 code_item,但 dump 的时机和方法可能导致部分方法的指令数据没有被完整捕获。
3. DEX 结构校验问题
直接从内存 dump 的 DEX 可能无法通过 DEX 文件格式校验(如 checksum 不匹配等),需要重新计算。
dex_fix 工具的工作流程
dex_fix 是 FART 提供的 Python 修复工具,其工作流程如下:
# dex_fix 的核心逻辑(伪代码)
def fix_dex(dex_path, insns_dir, output_path):
# 1. 读取 dump 的 DEX 文件
dex_data = open(dex_path, 'rb').read()
# 2. 解析 DEX 头部和 class_defs
dex_header = parse_dex_header(dex_data)
class_defs = parse_class_defs(dex_data, dex_header)
# 3. 遍历每个类的方法
for class_def in class_defs:
for method in class_def.methods:
# 构造方法指令文件的路径
insns_file = os.path.join(insns_dir,
f"{method.class_name}.{method.name}")
if os.path.exists(insns_file):
# 4. 读取收集到的指令数据
insns_data = read_method_insns(insns_file)
insns_size, insns_bytes = parse_insns_file(insns_data)
# 5. 找到 DEX 中对应方法的 code_item 位置
code_item_offset = find_code_item_offset(dex_data, method)
# 6. 回填指令数据
patch_code_item(dex_data, code_item_offset,
insns_size, insns_bytes)
# 7. 修复 DEX 头部校验信息
fix_dex_header(dex_data)
# 8. 写出修复后的 DEX
open(output_path, 'wb').write(dex_data)
修复抽取壳时 code_item 的回填
对于抽取壳保护的应用,修复过程的关键在于 code_item 的回填。以下是具体的修复步骤:
步骤一:定位 code_item
在 DEX 文件中,每个方法的 method_item 包含一个 code_off 字段,指向 code_item 在 DEX 中的偏移量。修复工具通过这个偏移量找到需要回填的位置:
def find_code_item_offset(dex_data, method_idx, dex_header):
# 通过 method_ids 表找到 method_idx 对应的方法定义
method_id_offset = dex_header.method_ids_off + method_idx * 8
class_idx, proto_idx, name_idx = struct.unpack_from('<HHI', dex_data, method_id_offset)
# 在 class_defs 中找到包含此方法的类
# 然后遍历该类的 method_list,找到目标方法的 code_off
code_off = locate_method_code_off(dex_data, method_idx, dex_header)
return code_off
步骤二:回填指令数据
找到 code_item 的偏移量后,将 dex_method_insns 中的指令数据写入对应位置:
def patch_code_item(dex_data, code_item_offset, insns_size, insns_bytes):
# code_item 结构:
# uint16_t registers_size (offset +0)
# uint16_t ins_size (offset +2)
# uint16_t outs_size (offset +4)
# uint16_t tries_size (offset +6)
# uint32_t debug_info_off (offset +8)
# uint32_t insns_size (offset +12) ← 需要回填
# uint16_t insns[] (offset +16) ← 需要回填
# 回填 insns_size
struct.pack_into('<I', dex_data, code_item_offset + 12, insns_size)
# 回填 insns 数据
insns_offset = code_item_offset + 16
dex_data[insns_offset:insns_offset + len(insns_bytes)] = insns_bytes
步骤三:修复 DEX 头部
DEX 文件头部有几个校验字段需要在修改后重新计算:
def fix_dex_header(dex_data):
# 修复 SHA-1 签名(偏移 12-31)
sha1_data = bytearray(dex_data[32:]) # SHA-1 基于去除前 32 字节后的数据
sha1_hash = hashlib.sha1(bytes(sha1_data)).digest()
dex_data[12:32] = sha1_hash
# 修复 Adler-32 校验和(偏移 8-11)
checksum_data = bytearray(dex_data[12:]) # checksum 基于去除前 12 字节后的数据
checksum = zlib.adler32(bytes(checksum_data)) & 0xFFFFFFFF
struct.pack_into('<I', dex_data, 8, checksum)
FART 辅助 VMP 分析的应用场景
VMP(Virtual Machine Protection)是比抽取壳更高级的保护方案。VMP 将 Dalvik 字节码翻译为自定义的虚拟机指令,使得逆向分析需要还原自定义虚拟机的指令集才能理解原始逻辑。虽然 FART 本身不是为 VMP 脱壳设计的,但 FART 的输出数据在辅助 VMP 分析方面有着重要价值。
VMP 的基本原理
VMP 的处理流程:
原始 Dalvik 字节码
→ VMP 编译器翻译
→ 自定义虚拟机指令
→ 替换原始 code_item
→ 插入 VMP 解释器
分析者在反编译时看到的不再是标准的 Dalvik 指令,而是一系列自定义的虚拟机操作码和操作数。
FART 在 VMP 分析中的作用
1. 定位 VMP handler
FART dump 的 dex_method_insns 数据包含了方法被执行时的实际指令。如果一个方法被 VMP 保护,dex_method_insns 中记录的就是翻译后的自定义虚拟机指令,而非原始的 Dalvik 指令。
通过对比多个 VMP 保护方法的 dex_method_insns,可以发现 VMP 解释器的指令模式和 handler 地址,为还原 VMP 指令集提供线索。
2. 分析 VMP 解释器的结构
# 分析 FART 输出的方法指令,识别 VMP 特征
def analyze_vmp_method(insns_file):
data = open(insns_file, 'rb').read()
insns_size = struct.unpack('<I', data[:4])[0]
insns = data[4:]
# 统计指令操作码的分布
opcode_freq = {}
for i in range(0, len(insns), 2):
opcode = struct.unpack('<H', insns[i:i+2])[0]
# 提取高字节作为操作码(假设 VMP 使用第一个字节作为 opcode)
vm_opcode = (opcode >> 8) & 0xFF
opcode_freq[vm_opcode] = opcode_freq.get(vm_opcode, 0) + 1
# 如果操作码种类很少且分布均匀,可能是 VMP 保护
if len(opcode_freq) < 20 and all(v > 10 for v in opcode_freq.values()):
print("[VMP] 检测到可能的 VMP 保护方法")
print(" 指令种类: %d, 指令总数: %d" % (len(opcode_freq), insns_size))
return opcode_freq
3. 区分抽取壳与 VMP 壳
通过 FART 的输出可以快速判断 APP 使用的是抽取壳还是 VMP 壳:
- 抽取壳:FART 主动调用后,
dex_method_insns中的数据是完整的 Dalvik 字节码,可以直接用于修复 DEX - VMP 壳:
dex_method_insns中的数据是自定义虚拟机指令,不是标准的 Dalvik 指令,无法直接修复
这种区分对于选择正确的脱壳策略至关重要。
FART 输出文件的格式和使用方法
输出文件结构
FART 的脱壳结果存储在 /data/data/<包名>/fartdex/ 目录下:
/data/data/com.target.app/fartdex/
├── base.apk # dump 的 DEX 文件
├── base.apk.classes.dex # 多 DEX 时的后续 DEX
├── com.target.app.MainActivity # 方法指令文件
├── com.target.app.MainActivity.onCreate # 方法指令文件
├── com.target.app.Utils # 方法指令文件
│ # (可能是类级别的指令集合)
└── ...
DEX 文件格式
dump 出的 DEX 文件是标准 DEX 格式,但可能存在以下问题:
code_item的insns_size可能为 0(如果 FART dump 时机早于壳程序回填)- DEX 头部的
checksum和signature可能不正确 - 某些方法的
code_item偏移量可能指向无效位置
dex_method_insns 文件格式
每个方法指令文件的二进制格式:
字节偏移 数据类型 说明
0-3 uint32_t insns_size (指令条数)
4-... uint16_t[] insns 数据 (每条指令 2 字节)
使用方法
步骤一:提取 dump 结果
# 从设备提取 fartdex 目录
adb pull /data/data/com.target.app/fartdex/ ./fartdex_output/
步骤二:运行修复工具
# 使用 dex_fix 修复 DEX
python dex_fix.py ./fartdex_output/base.apk ./fartdex_output/ ./output/
修复工具会将收集到的 dex_method_insns 回填到 DEX 中,生成完整的 DEX 文件。
步骤三:验证修复结果
# 使用 jadx 验证修复后的 DEX
jadx -d output_jadx output/base.apk.fixed.dex
# 检查方法是否完整
# 如果方法体不再是空的,说明修复成功
步骤四:反编译分析
修复成功后,可以使用标准的逆向工具进行分析:
# 使用 jadx 反编译
jadx -d jadx_output output/base.apk.fixed.dex
# 使用 apktool 查看资源
apktool d output/base.apk.fixed.dex -o apktool_output
# 使用 smali 查看指令
# 在 apktool_output/smali/ 目录下查看反编译后的 smali 代码
修复失败的常见原因
- dex_method_insns 文件缺失:FART 没有成功收集到某些方法的指令数据,可能是方法未被触发或壳程序的反制
- DEX 结构被严重破坏:壳程序对 DEX 结构做了额外的修改,简单的回填无法修复
- 方法签名不匹配:收集到的指令数据与方法在 DEX 中的对应关系出现问题
- 多 DEX 场景:壳程序使用了多 DEX 加载,FART 可能没有 dump 到所有 DEX
总结
FART 与 Frida 是脱壳领域的两个利器,它们的互补关系使得结合使用能够应对更广泛的加壳场景。FART 的修复组件通过将 dex_method_insns 数据回填到 dump 的 DEX 中,解决了抽取壳的核心难题。同时,FART 的输出数据在辅助 VMP 分析方面也具有重要价值——通过分析 dump 的方法指令,可以识别 VMP 保护、定位 VMP handler、为还原自定义虚拟机指令集提供线索。掌握 FART 输出文件的格式和修复工具的使用方法,是将 FART 应用于实际脱壳实战的关键技能。