FART 组件设计和源码分析

FART 架构设计概述

FART(First ART Unpack Tool)是第一个基于 ART 虚拟机源码修改的自动化脱壳工具,由作者非虫开发并开源。它通过修改 AOSP 源码,在 Android 系统层面对 DEX 加载过程进行拦截,实现了对抽取壳的全自动脱壳。

FART 的整体架构设计围绕三个核心模块展开:主动调用机制DEX Dump 机制DEX 修复机制。这三个模块协同工作,构成了 FART 的完整脱壳流程。

FART 整体架构

FART 的架构可以概括为以下几个层次:

┌─────────────────────────────────────────┐
│            应用层 (APP 运行)              │
├─────────────────────────────────────────┤
│         ART 虚拟机层 (被修改的源码)        │
│  ┌───────────┐ ┌──────────┐ ┌────────┐ │
│  │ 主动调用  │ │ DEX Dump │ │ Dex修复│ │
│  │ 机制     │ │  机制    │ │  机制  │ │
│  └───────────┘ └──────────┘ └────────┘ │
├─────────────────────────────────────────┤
│            fartserver (通信服务)          │
├─────────────────────────────────────────┤
│            Android 系统层                │
└─────────────────────────────────────────┘

三大核心模块

1. 主动调用机制:在 DEX 加载完成后,主动调用 DEX 中所有类的方法,迫使壳程序将被抽取的 code_item 回填到内存中。这是 FART 区别于 DexHunter 的关键创新。

2. DEX Dump 机制:在主动调用完成后,将内存中已经完整的 DEX 数据 dump 到文件系统中,同时收集每个方法的指令数据(dex_method_insns)。

3. DEX 修复机制:由于 dump 出的 DEX 可能存在结构不完整的问题(如 code_item 偏移量异常),需要通过修复工具将收集到的 dex_method_insns 回填到 DEX 中,生成可用的 DEX 文件。

fart.cpp 核心源码分析

fart.cpp 是 FART 的核心实现文件,位于修改后的 AOSP 源码的 art/runtime/ 目录下。它包含了 DEX dump 和方法指令收集的所有关键函数。

dumphex 函数

dumphex 是一个基础的十六进制 dump 工具函数,用于将内存中的数据以十六进制形式写入文件:

// art/runtime/fart.cpp
void dumphex(char* file_path, uint8_t* begin, int len) {
    // 以追加模式打开文件
    FILE* fp = fopen(file_path, "ab+");
    if (fp == NULL) return;
    
    // 写入内存数据
    int written = fwrite(begin, 1, len, fp);
    fclose(fp);
    
    // 记录 dump 信息
    ALOGD("FART: dump %d bytes to %s", written, file_path);
}

这个函数虽然简单,但它是整个 FART dump 机制的基础。所有 DEX 数据和指令数据的持久化都通过它来完成。

dumpdexfilebyclass 函数

dumpdexfilebyclass 是 FART 中最重要的函数之一。它负责遍历一个类中的所有方法,收集每个方法的指令数据并写入文件:

// art/runtime/fart.cpp(简化展示)
void dumpdexfilebyclass(DexFile* dex_file, char* dexfile_path, Mirror::Class* klass) {
    // 获取类的所有方法
    for (int i = 0; i < klass->NumDirectMethods(); i++) {
        ArtMethod* method = klass->GetDirectMethodUnchecked(i);
        dumpdexinsns(dex_file, dexfile_path, method);
    }
    for (int i = 0; i < klass->NumVirtualMethods(); i++) {
        ArtMethod* method = klass->GetVirtualMethodUnchecked(i);
        dumpdexinsns(dex_file, dexfile_path, method);
    }
}

该函数将类的方法分为两类处理:

  • Direct Methods:包括 static 方法、private 方法、构造函数 <init>
  • Virtual Methods:包括普通的 virtual 方法和 override 方法

dumpdexinsns 函数

dumpdexinsns 是收集单个方法指令数据的核心函数:

// art/runtime/fart.cpp(核心逻辑简化)
void dumpdexinsns(DexFile* dex_file, char* dexfile_path, ArtMethod* method) {
    // 获取方法的 CodeItem
    const DexFile::CodeItem* code_item = method->GetCodeItem();
    if (code_item == NULL) {
        // code_item 为空,说明方法被抽取,无法收集指令
        ALOGD("FART: code_item is NULL for method: %s", method->PrettyMethod().c_str());
        return;
    }
    
    // 获取指令数据的起始地址和大小
    uint32_t insns_size = code_item->insns_size_in_code_units_;
    uint16_t* insns = code_item->insns_;
    
    if (insns == NULL || insns_size == 0) return;
    
    // 构造方法指令的输出文件路径
    char method_name[512];
    snprintf(method_name, sizeof(method_name),
             "%s/%s", dexfile_path, method->PrettyMethod().c_str());
    
    // dump 指令数据
    // 先写入指令大小
    dumphex(method_name, (uint8_t*)&insns_size, 4);
    // 再写入指令数据
    dumphex(method_name, (uint8_t*)insns, insns_size * 2);
    
    // 记录方法的元数据
    ALOGD("FART: dump insns for %s, insns_size=%d",
          method->PrettyMethod().c_str(), insns_size);
}

dumpdexfile 函数

dumpdexfile 负责将整个 DEX 文件从内存中 dump 到磁盘:

// art/runtime/fart.cpp(核心逻辑简化)
void dumpdexfile(DexFile* dex_file, const char* dumpdir) {
    // 获取 DEX 在内存中的起始地址和大小
    const uint8_t* begin = dex_file->Begin();
    size_t size = dex_file->Size();
    
    // 构造输出文件路径
    char dexfilepath[256];
    snprintf(dexfilepath, sizeof(dexfilepath),
             "%s/%s", dumpdir, dex_file->GetLocation().c_str());
    
    // 使用 "/" 作为目录分隔符替换
    // 确保路径中不包含非法字符
    
    // dump 整个 DEX
    dumphex(dexfilepath, (uint8_t*)begin, size);
    
    ALOGD("FART: dump dex file %s, size=%zu", dexfilepath, size);
}

脱壳函数的调用链

理解 FART 的脱壳过程,需要梳理清楚关键函数的调用链。FART 的完整调用链如下:

LinkClass()                          // ART 虚拟机的类链接入口
  └── FART 主动调用所有方法             // 修改后的逻辑
        ├── 获取 DEX 中的所有类
        ├── 遍历每个类
        │     ├── 获取类的所有 direct methods
        │     │     └── 主动调用每个方法
        │     └── 获取类的所有 virtual methods
        │           └── 主动调用每个方法
        └── 所有方法调用完成后
              ├── dumpdexfile()       // dump 整个 DEX
              │     └── dumphex()
              └── dumpdexfilebyclass() // 遍历类收集方法指令
                    └── dumpdexinsns() // 收集单个方法指令
                          └── dumphex()

主动调用的实现细节

FART 的主动调用机制是在 art/runtime/class_linker.ccLinkClass 方法中实现的。当 ART 完成一个类的链接后,FART 不会立即返回,而是遍历 DEX 文件中所有已定义的类,主动调用它们的每个方法:

// art/runtime/class_linker.cc 中修改的逻辑(简化)
void ClassLinker::LinkClass(Thread* self, ...) {
    // 原始的 LinkClass 逻辑
    ...
    
    // FART 新增:主动调用机制
    if (thread_local_dex_is_fart) {
        // 遍历 DEX 中的所有类
        for (size_t i = 0; i < dex_file->NumClassDefs(); i++) {
            const DexFile::ClassDef& class_def = dex_file->GetClassDef(i);
            Mirror::Class* klass = ...; // 获取类对象
            
            // 主动调用该类的所有方法
            dumpdexfilebyclass(dex_file, dexfilepath, klass);
        }
    }
}

dump 目录结构说明

FART 将脱壳结果输出到固定的目录中。默认的 dump 目录为 /data/data/<包名>/fartdex/,结构如下:

/data/data/com.example.target/fartdex/
├── base.apk                          # dump 出的完整 DEX 文件
├── base.apk.classes.dex              # 如果存在多个 DEX,依次命名
├── com.example.target.MainActivity   # 方法指令数据(以类名+方法名命名)
│                                      # 文件内容:前4字节为指令大小 + 指令数据
├── com.example.target.Utils.encrypt
├── com.example.target.Utils.decrypt
└── ...

文件命名规则

  • DEX 文件:以 DEX 的原始路径命名,如 base.apkclasses2.dex
  • 方法指令文件:以 DexLocation/ClassName.MethodName 格式命名,每个文件对应一个方法的 code_item 指令数据

文件格式

方法指令文件的二进制格式为:

[4 bytes: insns_size] [insns_size * 2 bytes: insns_data]
  • 前 4 字节(uint32_t):指令条数 insns_size_in_code_units_
  • 后续字节:实际的 Dalvik 指令数据,每条指令占 2 字节(uint16_t)

dex_method_insns 的收集逻辑

dex_method_insns 是 FART 输出中最重要的数据之一。它记录了每个方法被调用时的完整指令数据,即使原始 DEX 中的 code_item 被抽取壳清零了,在 FART 主动调用后,壳程序必须将指令回填才能让方法执行,此时 FART 就能捕获到完整的指令数据。

收集时机

指令收集的时机非常关键。FART 选择在主动调用方法之后、方法执行之前进行收集。具体来说:

  1. 主动调用某个方法
  2. 壳程序检测到方法即将执行,将抽取的 code_item 回填到内存
  3. 方法实际开始执行前,FART 读取回填后的 code_item 数据
  4. 将指令数据写入文件

这样收集到的指令数据就是壳程序回填后的完整指令,可以用于后续的 DEX 修复。

收集范围

FART 会收集 DEX 中所有类的方法指令,包括:

  • Direct Methods(<init><clinit>、static 方法、private 方法)
  • Virtual Methods(public/protected virtual 方法)

对于 native 方法(native 标志位为 true 的方法),没有 Dalvik 字节码指令,因此不参与收集。

fartserver 的通信机制

fartserver 是 FART 提供的辅助服务组件,用于在运行时控制脱壳行为。它本质上是一个 Socket 服务端,运行在 Android 系统中,等待外部客户端连接并发送控制命令。

通信流程

客户端 (PC 工具)          fartserver (Android 设备)
       │                         │
       ├── connect ──────────────>│
       │                         │
       ├── 发送包名 ─────────────>│
       │                         │
       │    设置脱壳标志 ──────────┤
       │    thread_local_dex_is_fart = true
       │                         │
       ├── 启动目标 APP ─────────>│
       │                         │
       │    APP 运行 ─────────────┤
       │    LinkClass 触发 ──────┤
       │    主动调用 + dump ─────┤
       │                         │
       │    dump 完成 ────────────┤
       │                         │
       ├── 接收结果 ─────────────>│
       │                         │
       └── disconnect ───────────>│

fartserver 的核心功能

  1. 接收目标 APP 的包名:告诉 fartserver 需要对哪个应用进行脱壳
  2. 设置脱壳标志:在内存中设置 thread_local_dex_is_fart 标志,这个标志决定了 LinkClass 中是否会触发主动调用和 dump 逻辑
  3. 启动目标 APP:通过 am start 命令启动目标应用
  4. 等待 dump 完成:监听 dump 过程的日志输出,确认脱壳完成

源码关键变量

FART 中最核心的全局变量是 thread_local_dex_is_fart

// art/runtime/fart.cpp
__thread bool thread_local_dex_is_fart = false;

这个线程局部变量控制着脱壳流程的开启和关闭:

  • false(默认):ART 正常运行,不触发任何脱壳逻辑
  • true:在 LinkClass 中触发主动调用和 DEX dump

fartserver 的主要职责就是将这个变量设置为 true,然后启动目标 APP,使得 APP 运行过程中的 DEX 加载会触发 FART 的脱壳流程。

FART 的使用流程

综合以上分析,FART 的完整使用流程如下:

  1. 刷入 FART 系统:将编译好的 FART ROM 刷入测试设备(FART 基于 Android 5.0 - 7.1)
  2. 安装目标 APP:将需要脱壳的 APK 安装到设备上
  3. 连接 fartserver:通过 ADB 或网络连接到设备上的 fartserver
  4. 发送脱壳命令:指定目标 APP 的包名,启动脱壳流程
  5. 等待 APP 运行:让目标 APP 正常启动和运行一段时间
  6. 提取 dump 结果:从 /data/data/<包名>/fartdex/ 目录提取脱壳文件
  7. 修复 DEX:使用 dex_fix 工具将收集到的 dex_method_insns 回填到 dump 的 DEX 中

总结

FART 的架构设计简洁而高效,通过修改 ART 源码在 LinkClass 阶段实现了主动调用机制,巧妙地解决了抽取壳的难题。fart.cpp 中的 dumphexdumpdexfilebyclassdumpdexinsns 等函数构成了完整的 DEX dump 和指令收集链路,fartserver 则提供了便捷的远程控制能力。理解这些核心组件的设计和源码实现,是有效使用 FART 进行脱壳的基础。在后续文章中,我们将深入分析 FART 选择的脱壳点以及与 Frida 的协同使用方法。