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.cc 的 LinkClass 方法中实现的。当 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.apk、classes2.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 选择在主动调用方法之后、方法执行之前进行收集。具体来说:
- 主动调用某个方法
- 壳程序检测到方法即将执行,将抽取的
code_item回填到内存 - 方法实际开始执行前,FART 读取回填后的
code_item数据 - 将指令数据写入文件
这样收集到的指令数据就是壳程序回填后的完整指令,可以用于后续的 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 的核心功能
- 接收目标 APP 的包名:告诉 fartserver 需要对哪个应用进行脱壳
- 设置脱壳标志:在内存中设置
thread_local_dex_is_fart标志,这个标志决定了 LinkClass 中是否会触发主动调用和 dump 逻辑 - 启动目标 APP:通过
am start命令启动目标应用 - 等待 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 的完整使用流程如下:
- 刷入 FART 系统:将编译好的 FART ROM 刷入测试设备(FART 基于 Android 5.0 - 7.1)
- 安装目标 APP:将需要脱壳的 APK 安装到设备上
- 连接 fartserver:通过 ADB 或网络连接到设备上的 fartserver
- 发送脱壳命令:指定目标 APP 的包名,启动脱壳流程
- 等待 APP 运行:让目标 APP 正常启动和运行一段时间
- 提取 dump 结果:从
/data/data/<包名>/fartdex/目录提取脱壳文件 - 修复 DEX:使用 dex_fix 工具将收集到的
dex_method_insns回填到 dump 的 DEX 中
总结
FART 的架构设计简洁而高效,通过修改 ART 源码在 LinkClass 阶段实现了主动调用机制,巧妙地解决了抽取壳的难题。fart.cpp 中的 dumphex、dumpdexfilebyclass、dumpdexinsns 等函数构成了完整的 DEX dump 和指令收集链路,fartserver 则提供了便捷的远程控制能力。理解这些核心组件的设计和源码实现,是有效使用 FART 进行脱壳的基础。在后续文章中,我们将深入分析 FART 选择的脱壳点以及与 Frida 的协同使用方法。