不同代的壳和加壳技术

前言

在 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);
    }
}

脱壳思路

第一代壳的脱壳相对简单:

  1. 内存 dump 法:使用 Frida hook DexClassLoaderPathClassLoader,在 DEX 加载后从内存中 dump 出解密后的 classes.dex
  2. 主动调用法: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;

脱壳思路

  1. 内存修复法:在 ClassLinker::LinkMethod 完成后 dump DEX,此时方法体已被回填
  2. 主动调用 + dump:遍历所有类和方法,主动调用触发回填,再 dump 内存
  3. 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++;
    }
}

脱壳思路

  1. VM 指令逆向:分析 SO 中的 VM 解释器,还原自定义指令集的语义
  2. 动态插桩:使用 Unicorn Engine 或 QEMU 模拟执行 VM 指令,追踪执行流
  3. 内存 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

脱壳思路

  1. 反 JIT 编译:Hook JIT 编译入口,截获编译后的机器码并反汇编
  2. Native 层动态分析:使用 IDA Pro / Ghidra 静态分析 SO,结合 Frida 动态调试
  3. 符号执行:使用 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 中执行

脱壳思路

  1. 内核逆向:使用 Linux 内核调试技术(KGDB、ftrace)分析保护逻辑
  2. 修改内核:编译自定义内核,移除保护模块
  3. 硬件漏洞利用:寻找 TrustZone 漏洞(如 Spectre/Meltdown 类攻击)
  4. 侧信道分析:通过功耗、电磁辐射等物理手段推断执行逻辑

第五代壳通常只出现在金融支付、军事通信等极端安全需求的场景中,普通 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 加密,到第五代的内核级保护,每一代的进化都是为了对抗当时的逆向技术。

作为逆向工程师,理解每一代壳的原理不仅能帮助我们选择正确的脱壳方法,更能培养我们的安全思维——没有任何保护是不可攻破的,但成本可以高到不值得攻破。这正是加壳技术的价值所在。

在后续的文章中,我们将针对每一代壳进行更深入的实战分析,包括具体的脱壳工具使用和案例分析。