一、加固壳的原理回顾
在深入脱壳之前,先理解壳是怎么保护 DEX 的:
public class ShellApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); byte[] encryptedDex = readEncryptedDex();
byte[] realDex = decrypt(encryptedDex);
ClassLoader loader = new InMemoryDexClassLoader( new ByteBuffer[]{ByteBuffer.wrap(realDex)}, getClassLoader() ); } }
|
加固的本质是 DEX 加密存放 + 运行时解密加载。因此脱壳的关键是:在 DEX 解密后、加载前,将其从内存中 dump 出来。
二、ZjDroid 脱壳原理
ZjDroid(https://github.com/halfkiss/ZjDroid)是早期著名的 Xposed 脱壳模块,核心思路是 Hook ClassLoader 和 DexFile 相关方法。
ZjDroid 的关键 Hook 点:
1. dalvik.system.DexFile.<init>(String) → DEX 文件构造函数 2. dalvik.system.DexFile.loadClass(String, ...) → 类加载 3. dalvik.system.BaseDexClassLoader.findClass() → BaseDexClassLoader 类查找 4. java.lang.Runtime.exec() → 监控命令执行(反调试检测)
|
脱壳工作流:
adb shell am broadcast -a com.zjdroid.invoke --ei target pid
adb pull /data/data/com.target.app/files/dex_dump/ ./
|
三、DumpDex 方法
DumpDex(https://github.com/WrBug/dumpDex)是另一种基于 Hook 的脱壳工具,原理是 Hook ClassLoader 的构造过程。
frida -U -l dump_dex.js -f com.target.app --no-pause
|
Java.perform(function() { var ClassLoader = Java.use("java.lang.ClassLoader"); var DexFile = Java.use("dalvik.system.DexFile");
var dex_classLoader = Java.use("dalvik.system.DexClassLoader"); dex_classLoader.$init.overload('java.lang.String', 'java.lang.String', 'java.lang.String', 'java.lang.ClassLoader').implementation = function( dexPath, optDir, libPath, parent) { console.log("[*] DexClassLoader init: " + dexPath); return this.$init(dexPath, optDir, libPath, parent); }; });
|
四、Frida 内存 Dump 技术
现代脱壳更多使用 Frida,因为更灵活、更新及时。
方法一:通过 ClassLoader 遍历 DEX
Java.perform(function() { Java.enumerateLoadedClasses({ onMatch: function(className) { var clazz = Java.use(className); }, onComplete: function() { console.log("[*] Enumeration complete"); } }); });
|
方法二:通过 Memory.scan 搜索 DEX 魔数
var pattern = "64 65 78 0a 30 33 35";
Process.enumerateRanges('r--').forEach(function(range) { var results = Memory.scanSync(range.base, range.size, pattern); results.forEach(function(match) { console.log("[*] DEX header found at: " + match.address); }); });
|
五、手动脱壳完整流程
针对一个未知壳的通用脱壳流程:
apktool d target.apk -o unpacked ls unpacked/lib/armeabi-v7a/
adb install target.apk adb shell am start -n com.target.app/.MainActivity
frida -U com.target.app
%load dump_dex.js
adb pull /data/data/com.target.app/dumped_dex/ ./
jadx-gui dumped_classes*.dex
|
六、对抗抽取型加固
抽取型加固(如 360 的 VMP)不一次性解密整个 DEX,而是按需解密每个方法的字节码。
var classNames = []; var loadClass = Java.use("java.lang.ClassLoader").loadClass; loadClass.overload('java.lang.String').implementation = function(name) { classNames.push(name); return this.loadClass(name); };
|
面试常考问题
Q1:为什么不能直接从 APK 中提取 DEX,必须要从内存 dump?
A:因为加固后的 APK 中,classes.dex 是加密的壳代码,真正的业务 DEX 被加密后放在 assets/、lib/ 或加密隐藏在 APK 文件尾部。运行时壳代码解密 DEX 并在内存中加载,只有此时才能获取到真实的业务 DEX。因此脱壳必须在运行时进行操作。
Q2:抽取型加固和 VMP 加固脱壳的主要难点是什么?
A:抽取型加固不会在内存中一次性出现完整的 DEX,需要 Hook ClassLoader.defineClass 或在每个方法第一次被执行时收集其字节码(Function Code Extraction)。VMP 加固将原有的 DEX 字节码替换为自定义虚拟机的 opcode,即使 dump 出 DEX,其方法体也是 VMP 指令而非常规 Dalvik 字节码。分析 VMP 需要逆向壳自带的解释器引擎,理解其指令集映射关系。
Q3:如何检测一个应用使用了哪种加固方案?
A:(1)静态检测:apktool 解包后检查 lib/ 下的 so 文件:libtup.so→腾讯乐固,libjiagu.so→360,libSecShell.so→梆梆,libnqshield.so→网易易盾,libnaga.so→娜迦;(2)检查 Manifest 中是否有壳的 StubApplication;(3)使用 APKiD(https://github.com/rednaga/APKiD)工具自动检测加固类型;(4)检查 classes.dex 的内容——壳的 DEX 通常很薄,只有几个 Application/ClassLoader 相关的类。