首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Android DEX 加固拆包实战:从壳原理到 FART 脱壳全链路

Android DEX 加固拆包实战:从壳原理到 FART 脱壳全链路

作者头像
陆业聪
发布2026-06-01 19:19:22
发布2026-06-01 19:19:22
850
举报

📰 每日要闻

YouTube 推出 AI 定制视频流功能:用户可用自然语言描述想看的内容,AI 自动生成个性化推荐 Feed,进一步压缩传统算法推荐空间。(The Verge)

奥本海默首次覆盖 Cardinal Infrastructure,给予跑赢大市评级:数据中心基础设施赛道持续获机构增持,AI 算力需求成为主要逻辑。(Investing.com)

美联储官员:能源通胀比预期更具粘性:Goolsbee 表示油价虽因美伊和谈预期有所回落,但整体仍高于战前水平,加息路径预期分歧加剧。(CNBC)

Steam Deck 涨价,手持游戏机黄金时代或已终结:399 美元入门 PC 游戏的窗口正在关闭,掌机市场分化加速,大厂溢价化倾向明显。(The Verge)

上周有个同事找我,说他们的 App 被人二次打包重签了,里面塞了广告 SDK,渠道投诉一堆。他问我:「我们已经用了某加固平台,怎么还是被脱了?」

我当时没直接回答,而是问他:「你知道你用的是几代壳吗?」他一脸懵。

这就是问题所在——很多开发者接入了加固服务,但对壳的原理、强度、以及对应的脱壳手段完全不了解。加固不是万能的,理解它才能用好它。今天我们就来拆开这个话题,从壳原理到 FART 脱壳,走一遍全链路。

一、DEX 加固的演进:一代到三代壳

先搞清楚「壳」是什么。简单说,就是在你的原始 DEX 之外包一层或多层保护机制,让逆向工程师无法直接用 jadx、apktool 之类的工具还原出可读代码。

一代壳:DEX 整体加密

最早期的方案,思路非常直接:把原始 .dex 文件加密(AES/XOR),运行时由壳的 Application 解密,写入 /data/data/<pkg>/目录,再用 DexClassLoader 动态加载。

APK 安装

壳 Application.attachBaseContext()

解密 encrypted.dex → 写到 /data/data/.../files/

DexClassLoader 加载解密后的 DEX

原始代码正常运行

一代壳的脱壳非常简单:在文件系统里直接找。解密后的 DEX 会落盘到沙箱目录,用 root 权限 adb pull 就完事了。现在任何安全意识稍微强一点的加固平台都不会用这种方案。

二代壳:不落盘 + 函数粒度保护

二代壳的核心改进是两点:DEX 不再落盘(直接在内存中解密加载),以及引入方法体抽取(Function Extraction)——把每个方法的字节码从 DEX 中挖空,运行时动态填回去。

方法体抽取让静态分析完全失效。你用 jadx 打开加固后的 APK,所有方法要么是空的,要么只有一条 throw new RuntimeException

但二代壳有个固有弱点:方法体最终还是要填回去才能执行。只要在 ART 执行字节码之前 hook 住关键节点,就能把完整的方法体 dump 出来。这就是 dexdump 方案的原理——hook ArtMethod::Invoke 或 interpreter 入口,在方法被调用瞬间记录完整字节码。

三代壳:VMP + 代码虚拟化

三代壳(VMP,Virtual Machine Protection)是目前最强的方案。它不只是加密字节码,而是把字节码编译成自定义的虚拟机指令集,运行时由壳内嵌的解释器执行。

脱壳难度极高:即使你 dump 出了内存,拿到的也是 VMP 字节码,而不是 Dalvik/ART 字节码。你需要逆向那个自定义解释器,才能还原出原始语义——工作量往往是数周甚至数月级别的。

主流加固平台(梆梆、爱加密、360加固、腾讯乐固)的高级版都会用三代壳对核心代码进行保护。但有一点值得注意:VMP 有较明显的性能开销,所以通常只对关键逻辑(如支付、鉴权、核心算法)使用 VMP,非核心代码仍用二代方案。这就给了我们可乘之机。

二、脱壳方法横评:各种姿势的优劣

方法一:文件系统 dump(一代壳专用)

最简单,不需要任何工具。对于落盘的 DEX:

代码语言:javascript
复制
# adb shell 进入 root
adb root
adb shell# 找 data 目录下的 dex/odex 文件
find /data/data/<pkg> \
-name "*.dex" \
-o -name "*.odex"# 拷出来
adb pull /data/data/<pkg>/files/

对一代壳 100% 有效。但现在几乎所有商业加固平台都已经不落盘了,所以这个方法主要用来验证目标用的是哪代壳。

方法二:内存 dump(二代壳通用)

对于不落盘的壳,解密后的 DEX 只存在于内存中,核心思路是:在 DEX 加载完成后、方法执行前,扫描 /proc/pid/maps 找到内存中的 DEX 区域并 dump

工具推荐:DumpDex(Frida 脚本) 和 objection 的 memory dump 功能。

代码语言:javascript
复制
// Frida 脚本:扫描内存找 DEX magic
const DEX_MAGIC = [
0x64, 0x65, 0x78,
0x0a // "dex\n"
];Process.enumerateRanges("r--")
.forEach(range => {
try {
const header =
range.base.readByteArray(4);
const arr =
new Uint8Array(header);
if (
arr[0] === DEX_MAGIC[0] &&
arr[1] === DEX_MAGIC[1] &&
arr[2] === DEX_MAGIC[2] &&
arr[3] === DEX_MAGIC[3]
) {
// 读取 DEX file_size 字段
const size =
range.base.add(0x20)
.readU32();
const dexData =
range.base.readByteArray(
size
);
// 写到 /data/local/tmp/
const f = new File(
`/data/local/tmp/
dump_${range.base}.dex`,
"wb"
);
f.write(dexData);
f.close();
}
} catch (e) {}
});

内存 dump 对整体加密的壳有效,但对方法体抽取方案只能 dump 出「空壳 DEX」——方法体是空的。

方法三:FART——针对方法体抽取的主动调用脱壳

这是本文的重点。FART(Fart Android Runtime)是 2019 年由 hanbingle 开源的脱壳方案,专门针对二代壳的方法体抽取保护。

核心思想:与其被动等方法被调用,不如主动遍历所有 ClassLoader 中已加载的每个类、每个方法,强制触发一次执行,在 ArtMethod 层面把完整字节码捞出来。

三、FART 原理深挖

ArtMethod 结构与方法体位置

在 ART 虚拟机中,每个 Java 方法都对应一个 ArtMethod 结构体。二代壳进行方法体抽取时,会把字节码从 DEX 里挖走,但 ArtMethod 本身仍然存在——只是其 dex_code_item_offset_ 字段指向的 CodeItem 是空的或被篡改的。

壳在方法被调用前,会通过 JNI hook 或 inline hook 拦截执行流,把真实 CodeItem 填回 ArtMethod,再放行执行。这个「填回」的时机,就是 FART 的切入点。

FART 在 AOSP 源码层面修改了 ART 解释器,在 interpreter/interpreter.ccExecute() 函数入口处插入 dump 逻辑:

代码语言:javascript
复制
// interpreter.cc(FART 修改后)
void Execute(
Thread* self,
const DexFile::CodeItem* code,
ShadowFrame& shadow_frame,
JValue* result,
bool stay_in_interpreter
) {
// FART 注入点
if (code != nullptr) {
dumpArtMethod(
shadow_frame.GetMethod()
);
}
// 原始执行逻辑...
}

dumpArtMethod() 会把当前方法的 CodeItem 写入文件,记录该方法在 DEX 中的 offset 以及完整字节码。

主动调用:覆盖所有方法

仅靠「等方法被调用」是不够的——很多私有方法、初始化路径不常走的方法可能永远不会被触发。FART 的关键创新在于主动调用

代码语言:javascript
复制
// Java 层主动枚举(FART 注入的代码)
public static void fartthread() {
new Thread(new Runnable() {
@Override
public void run() {
try {
// 等壳解密完成
Thread.sleep(1000 * 2);
fart();
} catch (Exception e) {
e.printStackTrace();
}
}}).start();
}public static void fart() {
ClassLoader cl =
Thread.currentThread()
.getContextClassLoader();
// 枚举所有已加载的类
Object[] dexFiles =
getDexFilesFromClassLoader(cl);
for (Object dexFile : dexFiles) {
// 遍历每个 Class
for (String className :
getClassNameList(dexFile)) {
loadClassAndInvoke(
cl, className
);
}
}
}private static void
loadClassAndInvoke(
ClassLoader cl,
String className
) {
try {
Class cls =
cl.loadClass(className);
// 对每个方法主动 invoke
for (Method m :
cls.getDeclaredMethods()) {
invokeMirrMethod(m);
}
} catch (Exception e) {}
}

invokeMirrMethod() 是关键——它不走正常的 Java 反射调用栈,而是直接操作 native 层的 ArtMethod 结构,绕过访问权限检查,强制触发 interpreter 执行路径(哪怕方法体里是空的也不会崩)。

dump 文件结构与还原

FART 脱壳后会产出两类文件:

文件

内容

用途

xxx.dex

内存中 dump 的 DEX(方法体空洞)

提供类/方法结构骨架

xxx_ins.bin

每个方法真实 CodeItem 的二进制 dump

填充到 DEX 还原方法体

还原工具是 FART 配套的 dexfixer.py,它读取 .dex 和 _ins.bin,把每个方法对应的 CodeItem 按 offset 写回 DEX 文件:

代码语言:javascript
复制
# dexfixer.py 核心逻辑(简化版)
import structdef fix_dex(dex_path, ins_path):
with open(dex_path, "rb") as f:
dex = bytearray(f.read())with open(ins_path, "rb") as f:
while True:
header = f.read(16)
if not header:
break
# offset, size
offset, size = struct.unpack(
"<II", header[:8]
)
code = f.read(size)
# 写回 DEX 对应偏移
dex[offset:offset+size] = codewith open(
dex_path + "_fixed.dex", "wb"
) as f:
f.write(dex)

四、实战:用 FART 脱壳某商业加固 APK

环境准备

FART 需要刷入修改过的 AOSP ROM,不能直接跑在正常 Android 设备上。目前官方支持的版本是 Android 8.1(FART 原版)和 Android 10(FART10,社区维护版本)。

推荐用 Pixel 2 + Android 8.1 的 FART ROM(原版兼容性最好)。如果没有 Pixel 2,可以用 Android Studio 模拟器 + 自编译 FART AOSP,但速度会很慢。

代码语言:javascript
复制
# 刷入 FART ROM 后验证
adb shell getprop ro.build.version.release
# 应输出 8.1.0# 检查 FART 标记
adb shell getprop ro.product.model
# FART ROM 通常改了 model 标识

脱壳流程

安装目标 APK

adb install target.apk

启动 App,等待壳初始化完成

FART 后台线程自动枚举并 dump(约30-120秒)

pull dump 文件

adb pull /data/local/tmp/dex/

dexfixer.py 还原 → jadx 反编译

代码语言:javascript
复制
# 拉取 dump 文件
adb pull /data/local/tmp/dex/ ./dump/# 遍历所有 .dex 找对应 _ins.bin
for dex in dump/*.dex; do
ins="${dex%.dex}_ins.bin"
if [ -f "$ins" ]; then
python3 dexfixer.py \
"$dex" "$ins"
fi
done# 用 jadx 反编译还原后的 dex
jadx -d output/ \
dump/*_fixed.dex

五、加固方的反脱壳手段

知己知彼。作为防守方,了解加固平台用什么手段对抗 FART 这类工具,才能判断自己的保护强度。

1. 环境检测:识别 FART ROM

检测 ro.product.model、ro.build.fingerprint 等系统属性是否为已知 FART ROM 特征值。检测到则拒绝运行或行为变异。

代码语言:javascript
复制
// 壳的环境检测示例
val isFartEnv: Boolean get() {
val model = Build.MODEL
val fp = Build.FINGERPRINT
return model.contains(
"walleye_fart",
ignoreCase = true
) || fp.contains("fart")
}

2. 主动调用检测:识别「非正常」的 invoke

FART 的主动调用有一个明显特征:在极短时间内批量调用大量方法,且调用栈深度异常浅(没有正常业务调用链)。加固平台可以 hook DeclaredMethod.invoke,检测是否来自 FART 的枚举线程。

3. 方法体按需解密(懒加载)

这是对 FART 最有效的对抗:不在应用启动时统一解密方法体,而是在方法第一次被真实业务调用时才解密填充。FART 主动 invoke 时传入的参数是随机的/null,可能触发 NPE 等异常,壳检测到异常调用就不解密,返回空体。

这也是为什么 FART 的后续改进版本(FART_bypass、强FART)要针对性地处理 NPE 异常,以及对方法执行路径加更细粒度的控制。

六、我的判断:选什么加固方案

说几个我实际用下来的结论,不代表任何平台:

1. 加固是必要的,但不是银弹。只要代码最终要在设备上跑,理论上就能被还原。加固的本质是提高逆向成本,不是让逆向变得不可能。

2. 选加固方案要看你的核心资产在哪里。如果核心算法在 Native 层(so),DEX 加固意义有限,你更应该关注 so 的混淆和反调试。如果核心逻辑都在 Java/Kotlin 层,才值得上三代壳。

3. VMP 有性能代价,别无脑全量用。实测一段业务代码用 VMP 保护后,执行速度可能慢 10-50 倍(取决于逻辑复杂度)。只保护真正需要保护的核心函数。

4. 加固 + 代码混淆 + 反调试是三件套,缺一不可。FART 能还原字节码,但还原出来的代码如果变量名都是 a/b/c、方法名都是 a0/b0/c0(R8 full mode),逆向效率还是很低。混淆和加固要组合用。

5. 你的 APK 真的需要防护吗?说真的,大多数 To B 的工具类 App,逆向价值很低。上完整加固套餐反而会增加 App 体积、影响启动速度、增加兼容性问题。想清楚你的威胁模型再决定。

安全是一场成本博弈:让攻击者的投入大于他的收益,就赢了。

尾声

FART 是 2019 年的工具,距今已经 7 年了,但它背后的原理——「主动调用触发 ART 解释器,在 native 层 dump CodeItem」——至今仍然有效,因为 ART 的基础执行模型没有根本性变化。

值得关注的是,随着 Android 14 在部分设备上引入更激进的 AOT 编译策略,以及 Profile-guided Compilation 的普及,未来 JIT 解释器的执行路径可能越来越少,纯粹依赖 interpreter hook 的脱壳方案会面临新挑战。

同样值得研究的还有 强FART(FART 改进版,针对按需解密场景)和 BlackDex(不需要特定 ROM,在标准 Android 上运行的脱壳工具)。这两个方向我后续有机会再展开聊。

本文首发于公众号,转载请注明出处。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 陆业聪 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档