FART 框架介绍,选择脱壳点

FART 框架概述

FART(First Android Unpack Tool,Android 第一款 ART 环境自动化脱壳机)是由安全研究员星痕(hanbinglengyue)开发的开源 Android 脱壳框架。FART 的出现填补了 Android 5.0(ART 运行时)环境下自动化脱壳工具的空白,在逆向工程社区中具有里程碑意义。

在 FART 之前,大多数脱壳工作依赖于手动操作——逆向分析人员需要通过调试器找到壳程序的解密逻辑,手动在内存中搜索 DEX 数据并 dump。这种方式对分析者的技术水平要求高,且效率低下。FART 的核心理念是自动化:通过修改 Android 系统源码中的关键函数,在 DEX 加载的关键节点自动 dump 内存中的 DEX 数据,实现"一键脱壳"。

FART 的源码托管在 GitHub(https://github.com/hanbinglengyue/FART),基于 Android 6.0 和 7.0 的 AOSP 源码进行了修改。FART 脱壳后的数据输出到 /sdcard/fart 目录,包括完整的 DEX 文件和每个类的方法字节码 dump 数据。

FART 的设计目标和技术路线

设计目标

FART 的设计目标可以概括为以下三点:

  1. 全自动化脱壳:不需要手动分析壳程序的解密逻辑,在系统层面自动完成 DEX 的 dump
  2. 广泛兼容性:支持 DEX 整体加密壳和 DEX 抽取壳两种主流壳类型
  3. 完整数据输出:不仅 dump 完整的 DEX 文件,还 dump 每个类的方法字节码,用于修复抽取壳的空方法体

技术路线

FART 采用了**修改 Android 系统源码(AOSP)**的技术路线,而不是基于 Frida 等 hook 框架。这个选择有其深刻的原因:

  • 时机精确:直接在系统源码中修改,可以在 DEX 加载的最关键节点执行 dump 逻辑,时序上无延迟
  • 权限完整:系统级代码拥有最高权限,可以直接访问所有内存区域,不受进程权限限制
  • 稳定性高:不依赖外部 hook 框架的稳定性,避免了 Frida 检测和反调试问题
  • 兼容性强:在类链接阶段 dump DEX,无论壳程序使用何种解密方式,只要壳完成了 DEX 的加载,FART 就能捕获到

FART 的整体技术架构:

修改 AOSP 源码
    ↓
编译自定义 ROM(刷入手机)
    ↓
修改 ClassLinker::LinkClass(添加 dump 逻辑)
    ↓
修改 DexFile::Open(添加完整 DEX dump 逻辑)
    ↓
应用运行时自动 dump
    ↓
输出到 /sdcard/fart/
    ├── *.dex(完整 DEX 文件)
    └── *.txt(每个类的方法字节码 dump)

脱壳点选择的三大原则

FART 选择脱壳点时遵循三个核心原则,这些原则也适用于其他脱壳方案的脱壳点选择:

完整性原则

脱壳点处的 DEX 数据必须包含尽可能完整的类信息和方法体。完整性是脱壳工作的首要目标——dump 出来的 DEX 如果缺少类定义或方法字节码,对于逆向分析来说价值就大打折扣。

对于 DEX 整体加密壳,壳程序在 Application.attachBaseContext() 中完成 DEX 解密后,后续的 ClassLoader 加载过程中 DEX 数据就是完整的。因此,任何在解密完成之后的加载阶段都可以获取完整 DEX。

对于 DEX 抽取壳,情况更复杂。抽取壳将方法字节码从 DEX 中移除,在运行时动态回填。因此,脱壳点必须位于方法字节码回填完成之后,否则 dump 的 DEX 中方法体将是空的。

时效性原则

脱壳点必须在壳程序完成解密操作之后。如果脱壳点过早,DEX 数据仍然是加密状态,dump 出来的数据无法使用。

判断时机的关键指标:

  • 壳程序的 attachBaseContext() 方法执行完毕(DEX 解密通常在此阶段完成)
  • DexClassLoader 已经被创建并开始加载类
  • 类的 defineClass / LoadClass 已经被调用

FART 选择在 ClassLinker::LinkClass 阶段进行 dump,确保此时壳程序已经完成了所有解密和加载准备工作。

通用性原则

脱壳点应尽可能适用于不同类型的壳和不同版本的 Android 系统。过于依赖特定壳程序的内部实现细节的脱壳点,其通用性较差。

例如,如果选择 hook 壳程序的特定解密函数来 dump DEX,这种方法只对这一种壳有效。而选择在通用的类加载路径上 hook,则可以应对多种不同壳类型。

FART 选择 ClassLinker::LinkClass 作为脱壳点,正是因为这个函数是 ART 虚拟机类加载的必经之路,任何壳程序都无法绕过。

FART 在 ART 中的脱壳点实现

ClassLinker::LinkClass Hook

FART 的核心脱壳点位于 ClassLinker::LinkClass 函数。这个函数在 ART 虚拟机的类加载流程中负责将一个已经定义(LoadClass 完成)的类进行链接操作。

FART 的修改逻辑如下:

// art/runtime/class_linker.cc(FART 修改后的代码)
bool ClassLinker::LinkClass(Thread* self,
                            Handle<mirror::Class> klass,
                            Handle<mirror::ObjectArray<mirror::Class>> interfaces,
                            Handle<mirror::ClassLoader> class_loader) {
    // ... 原始 LinkClass 逻辑 ...
    
    // ========== FART 注入的 dump 逻辑 ==========
    // 获取该类所属的 DexFile
    const DexFile& dex_file = klass->GetDexFile();
    
    // dump 该类的所有方法字节码
    for (uint32_t i = 0; i < klass->NumDirectMethods(); i++) {
        ArtMethod* method = klass->GetDirectMethodUnchecked(i, kPointerSize);
        dumpDexMethod(method, dex_file);
    }
    for (uint32_t i = 0; i < klass->NumVirtualMethods(); i++) {
        ArtMethod* method = klass->GetVirtualMethodUnchecked(i, kPointerSize);
        dumpDexMethod(method, dex_file);
    }
    // ========================================
}

DexFile::Open Hook

除了 LinkClass 中的方法级 dump,FART 还在 DexFile::Open 函数中添加了完整 DEX 文件的 dump 逻辑:

// art/runtime/dex_file.cc(FART 修改后的代码)
bool DexFile::Open(const uint8_t* base, size_t size,
                   const std::string& location, uint32_t location_checksum,
                   const OatDexFile* oat_dex_file, std::string* error_msg) {
    // ... 原始 Open 逻辑 ...
    
    // ========== FART 注入的 dump 逻辑 ==========
    // 将完整的 DEX 数据 dump 到文件
    dumpDexFile(base, size, location);
    // ========================================
}

dumpDexMethod 的实现细节

dumpDexMethod 是 FART 的核心函数之一,负责 dump 单个方法的字节码信息:

void dumpDexMethod(ArtMethod* method, const DexFile& dex_file) {
    // 获取方法的 CodeItem 结构(包含字节码)
    const DexFile::CodeItem* code_item = method->GetCodeItem();
    if (code_item == nullptr) {
        return; // 抽象方法或 native 方法,无字节码
    }
    
    // 获取方法的 DEX 文件偏移
    uint32_t method_dex_idx = method->GetDexMethodIndex();
    
    // 获取方法名
    const std::string method_name = method->GetName();
    
    // 获取 CodeItem 的字节码大小
    uint32_t insns_size = code_item->insns_size_in_code_units_;
    
    // 将字节码数据写入输出文件
    char output_path[256];
    snprintf(output_path, sizeof(output_path),
             "/sdcard/fart/%s_%s.txt",
             dex_file.GetLocation().c_str(),
             method_name.c_str());
    
    FILE* fp = fopen(output_path, "ab");
    if (fp != nullptr) {
        // 写入方法索引、CodeItem 偏移、字节码大小
        fprintf(fp, "method_idx: %d, code_off: 0x%x, insns_size: %d\n",
                method_dex_idx,
                method->GetCodeItemOffset(),
                insns_size);
        
        // 写入实际的字节码
        const uint16_t* insns = code_item->insns_;
        for (uint32_t i = 0; i < insns_size; i++) {
            fprintf(fp, "%04x ", insns[i]);
        }
        fprintf(fp, "\n");
        fclose(fp);
    }
}

Frida 版 FART 的脱壳点实现差异

虽然 FART 原版是通过修改 AOSP 源码实现的,但社区也开发出了基于 Frida 的 FART 实现(通常称为 Frida-FART 或 frida-fart)。两者在脱壳点选择上存在一些差异:

AOSP 版 FART

  • 实现方式:直接修改 libart.so 的 C++ 源码,重新编译系统
  • 脱壳点ClassLinker::LinkClass 内部直接插入 dump 逻辑
  • 时机:在类链接过程中同步执行 dump
  • 优势:稳定、无检测风险、时机精确
  • 劣势:需要刷机、版本固定、部署成本高

Frida 版 FART

  • 实现方式:通过 Frida 的 Interceptor.attach hook ClassLinker::LinkClass 的符号地址
  • 脱壳点:通过符号名查找 LinkClass 函数地址,在函数入口/出口处执行 dump
  • 时机:在 hook 回调中异步执行 dump
  • 优势:无需刷机、灵活、可跨版本使用
  • 劣势:可能被壳程序检测到 Frida、需要 root 权限

Frida 版的核心实现差异:

// Frida 版 FART 的 LinkClass hook 示例
var artModule = Process.findModuleByName("libart.so");

// 查找 LinkClass 函数
var linkClassSym = artModule.findExportByName(
    "_ZN3art11ClassLinker9LinkClassEPNS_6ThreadEPNS_6HandleINS_6mirror5ClassEEEPNS3_INS_6ObjectArrayIS5_EEEPNS3_INS_6mirror11ClassLoaderEEE"
);

if (linkClassSym) {
    Interceptor.attach(linkClassSym, {
        onEnter: function(args) {
            // arg1: mirror::Class 指针
            this.klass = args[1];
        },
        onLeave: function(retval) {
            if (retval.toInt32() === 1) { // LinkClass 返回 true
                dumpClassMethods(this.klass);
            }
        }
    });
}

脱壳点的 Android 版本兼容性分析

FART 原版基于 Android 6.0 和 7.0 开发,随着 Android 版本的不断更新,ClassLinker::LinkClass 的函数签名和内部结构也在发生变化。

Android 6.0 - 7.0(FART 原版支持)

这是 FART 原版的直接支持范围。ClassLinker::LinkClass 的函数签名和参数布局完全匹配 FART 的 hook 代码。所有 dump 功能正常工作。

Android 8.0 - 9.0

从 Android 8.0 开始,ART 进行了较大的架构调整:

  • ClassLinker::LinkClass 的函数签名发生变化,参数数量和类型有调整
  • ArtMethod 的内部布局重新设计,GetCodeItem() 的实现方式改变
  • DexFile 结构体成员位置发生变化

需要针对这些版本适配 FART 的 dump 逻辑。社区已有适配 Android 8.1 和 9.0 的 FART 修改版本。

Android 10+

Android 10 引入了更多安全机制:

  • 内存访问权限更加严格
  • ClassLinker 的内部实现大幅重构
  • 部分 ART 内部符号不再导出
  • hiddenapi 限制影响了反射访问

这些变化使得 FART 的兼容性适配变得更加困难。对于 Android 10+ 的脱壳,通常建议使用 Frida 版 FART 或其他基于动态分析的脱壳方案。

各版本脱壳点选择建议

Android 版本 推荐脱壳方案 主要挑战
5.0 - 7.1 FART AOSP 版 部分壳有 Frida 检测
8.0 - 9.0 FART AOSP 适配版 / Frida-FART 函数签名变化
10 - 11 Frida-FART hiddenapi 限制、符号不导出
12+ Frida-FART + 定制化适配 安全机制更强

总结

FART 框架通过修改 ART 虚拟机的 ClassLinker::LinkClass 函数,在类链接阶段自动 dump DEX 数据,实现了 Android ART 环境下的自动化脱壳。脱壳点选择的三大原则——完整性、时效性、通用性——指导了 FART 的设计决策。虽然 FART 原版受限于 Android 版本,但其设计理念被广泛借鉴到 Frida 版和其他脱壳工具中。理解 FART 的脱壳点选择原理,有助于在实际逆向工作中灵活选择和定制脱壳方案。