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 检测的影响:

  1. 使用 FART 脱壳环境运行 APP,避免壳程序检测到 Frida
  2. APP 正常运行并完成 DEX 加载
  3. 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 脱壳后输出的文件主要包括两部分:

  1. dump 出的 DEX 文件:可能存在 code_item 不完整的问题
  2. 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_iteminsns_size 可能为 0(如果 FART dump 时机早于壳程序回填)
  • DEX 头部的 checksumsignature 可能不正确
  • 某些方法的 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 代码

修复失败的常见原因

  1. dex_method_insns 文件缺失:FART 没有成功收集到某些方法的指令数据,可能是方法未被触发或壳程序的反制
  2. DEX 结构被严重破坏:壳程序对 DEX 结构做了额外的修改,简单的回填无法修复
  3. 方法签名不匹配:收集到的指令数据与方法在 DEX 中的对应关系出现问题
  4. 多 DEX 场景:壳程序使用了多 DEX 加载,FART 可能没有 dump 到所有 DEX

总结

FART 与 Frida 是脱壳领域的两个利器,它们的互补关系使得结合使用能够应对更广泛的加壳场景。FART 的修复组件通过将 dex_method_insns 数据回填到 dump 的 DEX 中,解决了抽取壳的核心难题。同时,FART 的输出数据在辅助 VMP 分析方面也具有重要价值——通过分析 dump 的方法指令,可以识别 VMP 保护、定位 VMP handler、为还原自定义虚拟机指令集提供线索。掌握 FART 输出文件的格式和修复工具的使用方法,是将 FART 应用于实际脱壳实战的关键技能。