不同代的壳和加壳技术
前言
在 Android 逆向工程中,“加壳"是最常见的应用保护手段。所谓"壳”,就是在原始 APK 外层包裹一层加密或混淆逻辑,使得攻击者无法直接通过 jadx、apktool 等工具反编译出真实的业务代码。随着攻防对抗的升级,加壳技术经历了从简单到复杂、从用户态到内核态的多次迭代。
本文将系统梳理五代壳的核心原理、代表产品及对应的脱壳思路,帮助你建立对加壳技术的全局认知。
第一代壳:DEX 整体加密壳
核心原理
第一代壳的思路最为直接——将 classes.dex 整体加密后存储,运行时由壳程序先解密再加载。具体流程如下:
打包阶段:
原始 classes.dex → 加密算法(AES/DES) → 加密后的 classes.dex → 放入 APK
壳的 classes.dex(解密逻辑)→ 替代原始 DEX 作为入口
运行阶段:
系统加载壳 DEX → 壕Application.attachBaseContext() 触发
→ 读取加密的 DEX → 解密还原 → DexClassLoader 加载原始 DEX
→ 反射替换 PathClassLoader 中的 DEX Elements
代表产品
- 早期 360 加固:使用 AES 加密整个 DEX,密钥硬编码在 native 库中
- 爱加密:早期版本采用类似的整包加密方案
- 百度加固(早期):通过自定义 ClassLoader 实现动态加载
代码示例:手动实现简化版第一代壳
// 壳 Application - 解密并加载原始 DEX
public class ShellApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// 1. 从 APK 中读取加密的 DEX 文件
File encryptedDex = new File(getApplicationInfo().dataDir, "encrypted.dex");
byte[] encryptedData = readFile(encryptedDex);
// 2. 使用密钥解密
byte[] decryptedData = decrypt(encryptedData, "my_secret_key");
// 3. 写入解密后的 DEX
File decryptedDex = new File(getCacheDir(), "real.dex");
writeFile(decryptedDex, decryptedData);
// 4. 使用 DexClassLoader 加载
DexClassLoader dexLoader = new DexClassLoader(
decryptedDex.getAbsolutePath(),
getCacheDir().getAbsolutePath(),
null,
getClassLoader()
);
// 5. 反射合并 DEX Elements(关键步骤)
mergeDexElements(getClassLoader(), dexLoader);
}
}
脱壳思路
第一代壳的脱壳相对简单:
- 内存 dump 法:使用 Frida hook
DexClassLoader或PathClassLoader,在 DEX 加载后从内存中 dump 出解密后的classes.dex - 主动调用法:Hook
openDexFile等 native 函数,拦截 DEX 文件打开的路径
// Frida 脚本 - dump 内存中的 DEX
Java.perform(function() {
var DexFile = Java.use("dalvik.system.DexFile");
DexFile.openDexFile.implementation = function(filename, opt) {
console.log("[*] DexFile.openDexFile: " + filename);
return this.openDexFile(filename, opt);
};
});
第二代壳:DEX 抽取壳
核心原理
第二代壳不再加密整个 DEX 文件,而是将 DEX 中的方法体(Dalvik 字节码)抽取出来,存放到 SO 库或独立的加密文件中。DEX 文件本身保留,但所有方法体被替换为空操作(return void)或直接抛出异常。运行时,壳程序在方法被调用前,动态将原始字节码填充回去。
打包阶段:
原始 DEX → 解析方法体 → 抽取到 native_data.bin
→ DEX 中方法体替换为空指令 → 打包进 APK
运行阶段:
加载 DEX(方法体为空)→ 类加载时 / 方法调用前
→ 从 native_data.bin 读取原始字节码 → 写回 DEX 内存
→ 方法正常执行
代表产品
- 梆梆加固:将方法体抽取到 SO 库中,通过 JNI 在运行时回填
- 娜迦(Nagain):使用独立加密文件存储方法体,解密后动态修复
关键技术:DexMethod 结构
// DEX 文件中 method_id_item 和 code_item 的关系
// 每个方法对应一个 code_item,其中包含 Dalvik 字节码
// 第二代壳将 code_item 的 insns 字段清空或替换
typedef struct {
u2 registersSize; // 使用的寄存器数量
u2 insSize; // 参数个数
u2 outsSize; // 调用其他方法时需要的参数空间
u2 triesSize; // try/catch 块数量
u4 debugInfoOff; // 调试信息偏移
u4 insnsSize; // 指令数量
u2 insns[1]; // Dalvik 字节码 ← 被抽取的目标
} DexCode;
脱壳思路
- 内存修复法:在
ClassLinker::LinkMethod完成后 dump DEX,此时方法体已被回填 - 主动调用 + dump:遍历所有类和方法,主动调用触发回填,再 dump 内存
- F-art 脱壳机:修改 Android 系统源码,在 DEX 加载后自动 dump
// Frida - 枚举所有类并触发方法加载
Java.perform(function() {
Java.enumerateLoadedClasses({
onMatch: function(className) {
if (className.startsWith("com.target.")) {
try {
var clazz = Java.use(className);
// 触发类初始化,促使壳回填方法体
clazz.class.getConstructor().newInstance();
} catch(e) {}
}
},
onComplete: function() {
console.log("[*] Class enumeration complete, now dump DEX");
}
});
});
第三代壳:DEX 混淆 + VMP
核心原理
第三代壳引入了虚拟机保护(Virtual Machine Protection,VMP) 技术。它不再仅仅是加密或抽取字节码,而是将 Java/Smali 字节码翻译为自定义虚拟机的指令集。运行时由一个自定义解释器执行这些虚拟指令,从而使得原始逻辑完全无法通过静态分析还原。
编译保护阶段:
Java 字节码 → 转换为自定义 VM 指令集
→ 嵌入到 SO 库中(或独立的 .dat 文件)
→ 自定义 VM 解释器编译到壳的 SO 中
运行阶段:
壳加载 → 初始化自定义 VM → 载入虚拟指令
→ VM 解释器逐条执行 → 还原原始逻辑(仅在运行时存在)
代表产品
- 腾讯乐固(Legu):使用自研虚拟机引擎,将关键 Java 方法翻译为 VM 指令
- 网易易盾:结合代码混淆和 VMP,提供多层保护
- 360 加固 Pro:在第二代基础上增加了 VMP 保护层
自定义 VM 示例
// 简化的自定义虚拟机解释器
typedef struct {
uint8_t opcode; // 操作码
uint8_t op1; // 操作数1
uint8_t op2; // 操作数2
uint32_t result; // 结果
} VMInstruction;
// 操作码定义(完全自定义)
#define OP_ADD 0x01
#define OP_SUB 0x02
#define OP_LOAD 0x03
#define OP_STORE 0x04
#define OP_CALL 0x05
#define OP_RET 0xFF
void vm_execute(VMInstruction *code, int len) {
uint8_t regs[256]; // 虚拟寄存器
int pc = 0; // 程序计数器
while (pc < len) {
switch (code[pc].opcode) {
case OP_ADD:
regs[code[pc].op1] += regs[code[pc].op2];
break;
case OP_CALL:
// 调用 native 函数
call_native_handler(code[pc].op1, code[pc].op2);
break;
// ... 更多指令
}
pc++;
}
}
脱壳思路
- VM 指令逆向:分析 SO 中的 VM 解释器,还原自定义指令集的语义
- 动态插桩:使用 Unicorn Engine 或 QEMU 模拟执行 VM 指令,追踪执行流
- 内存 dump + 去混淆:结合动态执行,在关键逻辑执行后 dump 中间结果
第三代壳的脱壳难度显著上升,通常需要深入逆向 SO 层代码。
第四代壳:运行时编译(JIT/AOT)
核心原理
第四代壳彻底抛弃了 DEX 格式,将 Java 代码直接编译为机器码(Native Code),通过 JIT(Just-In-Time)或 AOT(Ahead-Of-Time)编译技术在运行时执行。这意味着连 Dalvik 字节码都不存在了,传统的 DEX dump 方法完全失效。
保护阶段:
Java 源码 / DEX → 编译器(基于 LLVM) → ARM/x86 机器码
→ 加密存储在 SO 或自定义格式中
→ 运行时 JIT/AOT 编译执行
运行阶段:
壳加载 → 解密机器码 → 映射到可执行内存
→ 直接在 CPU 上执行(不经过 ART/Dalvik 虚拟机)
代表产品
- 某信安全 SDK:将关键业务逻辑直接编译为 native 代码
- O-LLVM + VMP 组合:利用 O-LLVM(Obfuscator-LLVM)对 native 代码进行控制流平坦化和指令替换,再叠加 VMP
脱壳思路
- 反 JIT 编译:Hook JIT 编译入口,截获编译后的机器码并反汇编
- Native 层动态分析:使用 IDA Pro / Ghidra 静态分析 SO,结合 Frida 动态调试
- 符号执行:使用 Unicorn 模拟执行关键函数,恢复控制流
// Frida - Hook ART 的 JIT 编译入口(Android 7.0+)
Interceptor.attach(Module.findExportByName("libart.so",
"_ZN3art3jit4Jit11CompileMethodEPNS_6mirror5ArtMethodEPb"), {
onEnter: function(args) {
var method = args[1];
console.log("[*] JIT compiling method: " + method);
},
onLeave: function(retval) {
console.log("[*] JIT compilation done, code at: " + retval);
}
});
第五代壳:内核级保护
核心原理
第五代壳是最激进的保护方案——直接在 Android 内核层实现保护。通过修改内核或使用定制 ROM,在操作系统层面拦截调试器、root 检测和内存读取操作,从根本上阻断逆向分析。
保护层级:
用户空间(APP) → 反调试、反注入、完整性校验
Native 层(SO) → SECCOMP 过滤、ptrace 保护
内核层(Kernel) → 修改 /proc/self/mem 权限、拦截系统调用
ROM 层(定制系统) → 整个系统层面保护,信任链从 Bootloader 开始
代表产品
- 某大型互联网公司的金融类 APP:使用定制 ROM + 内核级反调试
- 企业级安全方案:通过 SELinux 策略限制调试器 attach
- 硬件级 TEE(可信执行环境):将关键逻辑放入 TrustZone 中执行
脱壳思路
- 内核逆向:使用 Linux 内核调试技术(KGDB、ftrace)分析保护逻辑
- 修改内核:编译自定义内核,移除保护模块
- 硬件漏洞利用:寻找 TrustZone 漏洞(如 Spectre/Meltdown 类攻击)
- 侧信道分析:通过功耗、电磁辐射等物理手段推断执行逻辑
第五代壳通常只出现在金融支付、军事通信等极端安全需求的场景中,普通 APP 极少使用。
各代壳对比
| 特性 | 第一代 | 第二代 | 第三代 | 第四代 | 第五代 |
|---|---|---|---|---|---|
| 核心原理 | DEX 整体加密 | 方法体抽取 | VMP 虚拟化 | JIT/AOT 编译 | 内核级保护 |
| 安全性 | ★☆☆☆☆ | ★★☆☆☆ | ★★★☆☆ | ★★★★☆ | ★★★★★ |
| 性能影响 | 低(仅启动时) | 中等 | 较高 | 高 | 极高 |
| 脱壳难度 | 简单 | 中等 | 困难 | 很难 | 极难 |
| 代表产品 | 早期 360、爱加密 | 梆梆、娜迦 | 乐固、易盾 | 某信安全 | 金融定制 ROM |
| 主要脱壳方式 | 内存 dump | 修复 + dump | VM 逆向 | 反编译 + 动态分析 | 内核逆向 |
如何判断 APP 使用了哪一代壳
实际逆向中,我们可以通过以下工具和方法快速判断目标 APP 的加壳类型:
步骤一:APK Analyzer 查看结构
# 使用 Android SDK 自带的 apk analyzer
aapt dump badging target.apk
# 查看文件列表
unzip -l target.apk | grep -E "\.dex|\.so|assets"
步骤二:jadx 查看代码结构
# 使用 jadx 反编译查看
jadx -d output target.apk
# 查看反编译结果
ls output/sources/
判断特征
| 观察结果 | 推测壳类型 |
|---|---|
classes.dex 体积异常小,且有 assets/libjiagu.so 等文件 |
第一代壳 |
DEX 中大量方法体为空或只有 return |
第二代壳(方法体抽取) |
| DEX 中有大量无意义的 switch-case(状态机) | 第三代壳(VMP) |
| 几乎没有 DEX 代码,逻辑全在 SO 中 | 第四代壳(运行时编译) |
| APP 无法在模拟器和 root 设备上运行 | 可能是第五代壳(内核保护) |
步骤三:使用检测工具
# 使用 Apktool 解包后检查
apktool d target.apk -o decoded/
# 检查 META-INF 中是否有加固商签名
ls decoded/META-INF/
# 使用 packer-detector 等工具自动识别
# https://github.com/nicokayang/apk-packer-detect
# Python 简易检测脚本
import zipfile, os
def detect_packer(apk_path):
features = {
"360": ["libjiagu.so", "libjiagu_art.so", "libjiagu_x86.so"],
"梆梆": ["libSecShell.so", "libDexHelper.so", "libexec.so"],
"爱加密": ["libexec.so", "libijiami.so"],
"通付盾": ["libtosprotection.so", "libtosversioncheck.so"],
"娜迦": ["libnagain.so", "libnagain2.so"],
"腾讯乐固": ["libshella-*.so", "libBugly.so"],
}
with zipfile.ZipFile(apk_path, 'r') as z:
names = z.namelist()
for packer, signatures in features.items():
for sig in signatures:
for name in names:
if sig.replace("*", "") in name:
return packer
return "未知加固(可能是自研方案或新一代壳)"
print(detect_packer("target.apk"))
总结
加壳技术的演进本质上是攻防对抗的升级史。从第一代简单的 DEX 加密,到第五代的内核级保护,每一代的进化都是为了对抗当时的逆向技术。
作为逆向工程师,理解每一代壳的原理不仅能帮助我们选择正确的脱壳方法,更能培养我们的安全思维——没有任何保护是不可攻破的,但成本可以高到不值得攻破。这正是加壳技术的价值所在。
在后续的文章中,我们将针对每一代壳进行更深入的实战分析,包括具体的脱壳工具使用和案例分析。