一、加壳原理概述:为什么我们需要脱壳?
做逆向这么多年,我见过太多人一上来就问「怎么脱壳」。其实,你得先搞清楚壳是什么。
说白了,加壳就是对原始 DEX 文件做一层「包装」。这层包装会加密、压缩甚至虚拟化你的代码。应用启动时,壳先运行,解密出真正的 DEX 再加载到内存里。嗯,这就像你寄快递——箱子是壳,里面的东西才是真正的代码。
我个人习惯把加壳技术分成三个层次:
- DEX 加固:最基础的方案。对整个 DEX 文件加密,运行时解密。我早期做项目时遇到过不少这种壳,说白了就是「套一层皮」。
- VMP(虚拟机保护):这个就狠了。它不是加密 DEX,而是把 Dalvik 字节码翻译成自定义指令集,然后用一个内置的虚拟机解释执行。你反编译看到的代码全是假的,真正的逻辑在虚拟机里跑。
- DexProtector 等商业方案:这类工具通常混合使用多种技术,比如字符串加密、资源保护、反调试、反 Hook。我在项目中遇到过一款商业壳,光是反调试就做了三层,搞得我差点想放弃。
核心观点:无论壳怎么变,最终真正的 DEX 一定会出现在内存中。因为 CPU 要执行它,就必须解密。这就是我们脱壳的根本依据。
二、内存 Dump 技术:抓住内存里的「真身」
既然壳最终会把真正的 DEX 加载到内存,那我们就在它加载完成后,把内存里的 DEX 数据「捞」出来。这就是内存 Dump 的核心思路。
2.1 Frida Dump:我最常用的方式
Frida 是个好东西。它可以在运行时注入 JavaScript 代码,Hook 关键函数,然后读取内存数据。我个人习惯用 Frida 来 Hook DexFile::open 或者 ArtMethod::Invoke 这类函数。
为什么会选这些函数?因为它们会在 DEX 被加载时被调用。你想想看,壳解密完 DEX 后,总要调用系统函数来加载吧?这就是我们的机会。
小技巧:我建议你在 Hook 时加上延迟。有些壳会在加载后立即释放内存,你 dump 慢了就什么都没了。我曾经因为这个原因折腾了一整天,后来加了个 500ms 的延迟就搞定了。
2.2 DDMS Dump:老派但有效
DDMS(Dalvik Debug Monitor Server)是 Android SDK 自带的工具。它可以通过调试接口获取进程的内存信息。说实话,现在用 DDMS 的人不多了,但有些场景下它反而更稳定。
我记得有一次,目标应用的反 Frida 检测做得特别严,Frida 一注入就闪退。最后我换回 DDMS,配合 dumpsys meminfo 和 procrank,硬是把 DEX 给 dump 出来了。嗯,工具不在新旧,管用就行。
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Frida Dump | 灵活、可编程、实时 | 容易被检测、需要 root | 大多数情况 |
| DDMS Dump | 稳定、无需注入代码 | 速度慢、功能有限 | 反 Frida 场景 |
三、Frida Unpacker 脚本编写:从零开始写一个脱壳脚本
写脱壳脚本其实不难,核心就三步:找到 DEX 在内存中的地址、读取数据、保存到文件。我给你看一个我常用的模板:
// frida_unpacker.js
function dumpDex() {
// 1. 枚举所有加载的 DEX 文件
Java.enumerateLoadedClasses({
onMatch: function(className) {
// 这里可以过滤目标类
},
onComplete: function() {}
});
// 2. 通过 ArtMethod 获取 DEX 地址
var ArtMethod = Java.use('java.lang.reflect.ArtMethod');
ArtMethod.getDexMethodIndex.implementation = function() {
var result = this.getDexMethodIndex();
// 在这里 dump 内存
return result;
};
// 3. 读取内存并保存
var dexAddr = Module.findBaseAddress('libart.so');
// 具体偏移量需要根据 Android 版本调整
Memory.readByteArray(dexAddr.add(0x1234), 0x100000);
}
setTimeout(dumpDex, 500); // 延迟 500ms 执行
注意:不同 Android 版本的 ArtMethod 结构体偏移量不同。Android 8.0 和 Android 10 的偏移量可能差好几个字节。我建议你写脚本时加上版本判断,或者用 Frida 的 Process.findModuleByName 动态获取。
写脚本时有个坑,我踩过好几次——就是 DEX 文件头校验。有些壳会篡改 DEX 的魔数(magic number),让你 dump 出来的文件打不开。解决办法是手动修复文件头,把 64 65 78 0A 30 33 35 00(dex\n035\0)写回去。
四、常见脱壳工具:让专业工具帮你干活
说实话,很多时候我们不需要自己写脚本。社区里已经有很成熟的工具了。我常用的有两个:
4.1 FRIDA-DEXDump
这个工具是 Frida 生态里最流行的脱壳工具之一。它自动扫描内存中的 DEX 文件,然后批量 dump 出来。用法很简单:
# 安装
pip install frida-dexdump
# 使用
frida-dexdump -U -f com.example.app
我个人觉得它最大的优点是「无脑」。你不需要关心 DEX 在哪个地址,它全自动搞定。但要注意,它对付 VMP 类壳效果一般,因为 VMP 的代码根本不在 DEX 里。
4.2 BlackDex
BlackDex 是另一个好用的工具,它基于 Xposed 框架实现。我更喜欢用它来对付一些老版本的应用(Android 7 以下),因为 Xposed 在那个年代兼容性最好。
BlackDex 的原理是 Hook 了 Zygote 进程,在应用启动时拦截 DEX 加载。它比 FRIDA-DEXDump 更底层,所以能抓到一些 Frida 抓不到的壳。
| 工具 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| FRIDA-DEXDump | Frida Hook + 内存扫描 | 跨平台、易用 | 对 VMP 无效 |
| BlackDex | Xposed Hook Zygote | 底层、兼容性好 | 需要 Xposed 环境 |
我的建议:先试 FRIDA-DEXDump,搞不定再上 BlackDex。如果还不行,那就得自己写脚本了。我曾经遇到一个壳,两个工具都 dump 不出来,最后手动分析内存才找到真正的 DEX 在 native 层的一个匿名映射里。
好了,关于脱壳的基础知识就聊到这里。记住一句话:壳是死的,人是活的。只要它跑在设备上,就一定有办法拿到真正的代码。下一节我们会深入实战,手把手带你脱一个真实的壳。