GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

应用安全 --- 安卓安全 之 DEX壳

一个app启动时,启动一个没有任何业务代码、无实质内容的dex壳文件,这个文件一般是加固程序sdk提供的,比如爱加密

这个壳会开始解密 存放在assest文件中的加密了的dex文件 ,之后 加载、执行 这个解密后的DEX文件。这段解密、加载、执行的这段代码一般用c语言编写放置在lib文件夹的so文件中,增加逆向难度。

一、整体架构

┌─────────────────── 加固后的APK结构 ───────────────────┐
│                                                        │
│  classes.dex          ← 壳DEX(空壳,无业务代码)        │
│  assets/              ← 加密后的原始DEX                  │
│  │  └── packed.dat       (或其他名称,加密存储)         │
│  lib/                 ← Native层解密引擎                 │
│  │  └── armeabi-v7a/                                    │
│  │       └── libjiagu.so  ← C/C++编写的解密+加载逻辑     │
│  AndroidManifest.xml  ← Application被替换为壳的入口类     │
│  META-INF/            ← 签名信息                        │
│  res/                 ← 资源文件(通常不加密)             │
│                                                        │
└────────────────────────────────────────────────────────┘

二、启动流程详解

┌──────────────────────────────────────────────────────────────┐
│                      APP 启动流程                              │
│                                                                │
│  ① Android系统启动APP                                          │
│     │                                                          │
│     ▼                                                          │
│  ② 加载 classes.dex(壳DEX)                                   │
│     │  壳DEX中只有一个 StubApplication 类                       │
│     │  AndroidManifest.xml:                                    │
│     │  android:name="com.shell.StubApplication"                │
│     │                                                          │
│     ▼                                                          │
│  ③ StubApplication.attachBaseContext() 执行                    │
│     │                                                          │
│     ▼                                                          │
│  ④ System.loadLibrary("jiagu")                                │
│     │  加载 lib/libjiagu.so                                    │
│     │                                                          │
│     ▼                                                          │
│  ⑤ SO中的 JNI_OnLoad() 执行                                   │
│     │  注册Native方法                                           │
│     │  反调试检测                                               │
│     │  环境检测(root/模拟器/frida/xposed)                      │
│     │                                                          │
│     ▼                                                          │
│  ⑥ 调用SO中的解密函数                                           │
│     │  读取 assets/packed.dat                                  │
│     │  在Native层解密(AES/DES/自定义算法)                      │
│     │  解密结果 → 原始DEX字节流(在内存中)                       │
│     │                                                          │
│     ▼                                                          │
│  ⑦ 加载解密后的DEX                                              │
│     │  方式A: 写入私有目录 → DexClassLoader加载                  │
│     │  方式B: InMemoryDexClassLoader(Android 8.0+)            │
│     │  方式C: 直接操作 DexFile Native接口                       │
│     │                                                          │
│     ▼                                                          │
│  ⑧ 替换 ClassLoader                                            │
│     │  通过反射将APP的ClassLoader替换为新的                       │
│     │  使得后续类加载都从解密后的DEX中查找                        │
│     │                                                          │
│     ▼                                                          │
│  ⑨ 替换 Application                                            │
│     │  反射创建真正的 Application 实例                           │
│     │  调用真正 Application 的 onCreate()                       │
│     │                                                          │
│     ▼                                                          │
│  ⑩ APP正常运行(业务代码开始执行)                                │
│                                                                │
└──────────────────────────────────────────────────────────────┘

三、壳DEX代码示例

/**
 * 壳DEX中唯一的关键类
 * 这就是"没有任何业务代码"的壳
 */
public class StubApplication extends Application {

    static {
        // ④ 加载Native库
        System.loadLibrary("jiagu");
    }

    // Native方法声明(实现在SO中)
    private static native byte[] decryptDex(Context context, String assetName);
    private static native void antiDebug();

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        
        // ⑤ 反调试
        antiDebug();
        
        // ⑥ 调用SO解密DEX
        byte[] dexBytes = decryptDex(base, "packed.dat");
        
        // ⑦ 加载解密后的DEX
        loadDecryptedDex(base, dexBytes);
        
        // ⑧ 替换ClassLoader
        replaceClassLoader(base);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // ⑨ 启动真正的Application
        launchRealApplication();
    }
    
    // ... 省略具体实现
}

四、SO层解密核心逻辑(C/C++)

// libjiagu.so 中的核心逻辑(简化示意)

// ⑤ JNI_OnLoad - SO加载时自动执行
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env;
    vm->GetEnv((void**)&env, JNI_VERSION_1_6);
    
    // 注册Native方法
    registerNativeMethods(env);
    
    // 反调试:检测 /proc/self/status 中的 TracerPid
    anti_debug_check();
    
    // 反Hook检测
    detect_frida();
    detect_xposed();
    
    return JNI_VERSION_1_6;
}

// ⑥ 核心解密函数
JNIEXPORT jbyteArray JNICALL
native_decryptDex(JNIEnv *env, jclass clazz, 
                  jobject context, jstring assetName) {
    
    // 1. 读取 assets/packed.dat
    AAssetManager *mgr = AAssetManager_fromJava(env, assetManager);
    AAsset *asset = AAssetManager_open(mgr, "packed.dat", AASSET_MODE_BUFFER);
    size_t size = AAsset_getLength(asset);
    unsigned char *encrypted = (unsigned char*)AAsset_getBuffer(asset);
    
    // 2. 解密(可能是AES、DES、RC4、自定义算法)
    //    密钥可能来自:硬编码、签名计算、服务器下发
    unsigned char key[16];
    derive_key(key, env, context);  // 密钥派生(可能基于APK签名)
    
    unsigned char *decrypted = malloc(size);
    
    // 分散构建的AES密钥(anti-strings技术)
    // 而不是直接写 key = "MySecretKey12345"
    key[0]  = 0x4D;  // 'M'
    key[1]  = 0x79;  // 'y'
    // ... 逐字节构建
    
    aes_decrypt(encrypted, size, key, decrypted);
    
    // 3. 可能还有解压缩
    unsigned char *decompressed = NULL;
    size_t decompressed_size = 0;
    zlib_decompress(decrypted, size, &decompressed, &decompressed_size);
    
    // 4. 验证DEX魔数
    if (memcmp(decompressed, "dex\n035\0", 8) != 0) {
        // DEX头校验失败,可能被篡改
        abort();
    }
    
    // 5. 返回解密后的DEX字节数组
    jbyteArray result = env->NewByteArray(decompressed_size);
    env->SetByteArrayRegion(result, 0, decompressed_size, decompressed);
    
    // 6. 清除内存中的敏感数据
    memset(decrypted, 0, size);
    memset(key, 0, 16);
    free(decrypted);
    free(decompressed);
    
    return result;
}

五、完整对抗流程

┌─────────────────────────────────────────────────────────────────┐
│                                                                   │
│                    逆向分析师 vs 加固方案                           │
│                                                                   │
│  ┌─────────────────┐         ┌─────────────────┐                 │
│  │    攻(逆向)     │   VS   │    防(加固)     │                 │
│  └────────┬────────┘         └────────┬────────┘                 │
│           │                           │                           │
│           ▼                           ▼                           │
│                                                                   │
│  ═══════════════ 第一层:静态分析对抗 ═══════════════               │
│                                                                   │
│  攻:反编译APK                    防:DEX加壳                      │
│  │   apktool d app.apk           │  classes.dex是空壳             │
│  │   jadx app.apk                │  看不到业务代码                  │
│  │                                │                               │
│  │   结果:只看到                  │  assets/packed.dat            │
│  │   StubApplication              │  加密的二进制blob              │
│  │   没有业务逻辑               │                               │
│  │                                                                │
│  ═══════════════ 第二层:脱壳 ═══════════════════                  │
│                                                                   │
│  攻方案A:内存Dump脱壳                                              │
│  │                                                                │
│  │  原理:不管怎么加密,DEX最终必须                                   │
│  │       以明文形态加载到内存中运行                                   │
│  │                                                                │
│  │  ┌────────────────────────────────────┐                        │
│  │  │  时机:解密完成后、加载到内存时       │                        │
│  │  │                                    │                        │
│  │  │  方法1: Frida Hook                 │                        │
│  │  │    Hook DexFile::Open()            │                        │
│  │  │    Hook InMemoryDexClassLoader      │                       │
│  │  │    在DEX加载瞬间dump内存            │                        │
│  │  │                                    │                        │
│  │  │  方法2: /proc/pid/maps + dd        │                        │
│  │  │    找到DEX在内存中的映射            │                         │
│  │  │    直接dump对应内存区域             │                         │
│  │  │                                    │                        │
│  │  │  方法3: FART (ART环境脱壳机)        │                        │
│  │  │    修改Android源码自动dump          │                        │
│  │  │                                    │                        │
│  │  │  方法4: Xposed/Frida 通用脱壳       │                        │
│  │  │    FDex2、DexDump等工具             │                        │
│  │  └────────────────────────────────────┘                        │
│  │                                                                │
│  │                          防方案:对抗脱壳                        │
│  │                          │                                     │
│  │                          ├─ 反Frida检测                        │
│  │                          │  检测frida-server端口(27042)         │
│  │                          │  检测/proc/maps中frida相关so         │
│  │                          │  检测D-Bus协议通信                    │
│  │                          │                                     │
│  │                          ├─ 反调试                              │
│  │                          │  ptrace(PTRACE_TRACEME)自占位        │
│  │                          │  检测TracerPid != 0                  │
│  │                          │  时间差检测                           │
│  │                          │                                     │
│  │                          ├─ 反Root检测                          │
│  │                          │  检测su二进制                        │
│  │                          │  检测Magisk                          │
│  │                          │                                     │
│  │                          ├─ 环境检测                            │
│  │                          │  检测模拟器                          │
│  │                          │  检测Xposed框架                      │
│  │                          │                                     │
│  │                          └─ DEX碎片化加载                       │
│  │                             不一次性加载完整DEX                  │
│  │                             按需解密单个类/方法                   │
│  │                                                                │
│  ═══════════════ 第三层:对抗反检测 ════════════════               │
│  │                                                                │
│  │  攻方案:绕过检测                                                │
│  │  │                                                             │
│  │  ├─ Frida绕过                                                  │
│  │  │  重命名frida-server                                         │
│  │  │  修改默认端口                                                │
│  │  │  使用frida-gadget注入                                       │
│  │  │  Hook检测函数使其返回安全值                                   │
│  │  │                                                             │
│  │  ├─ 反调试绕过                                                  │
│  │  │  Hook ptrace() 返回0                                       │
│  │  │  修改 /proc/self/status                                    │
│  │  │  内核层patch                                                │
│  │  │                                                             │
│  │  ├─ Root隐藏                                                   │
│  │  │  Magisk Hide / Shamiko                                     │
│  │  │  挂载命名空间隔离                                            │
│  │  │                                                             │
│  │  └─ 模拟器伪装                                                 │
│  │     修改设备指纹                                                │
│  │     Hook Build类返回值                                         │
│  │                                                                │
│  ═══════════════ 第四层:SO层对抗 ═══════════════                  │
│  │                                                                │
│  │  防:解密逻辑在SO中(C/C++)                                     │
│  │  │  SO本身也可能被保护:                                        │
│  │  │  ├─ SO加壳(UPX等)                                         │
│  │  │  ├─ 代码混淆(OLLVM)                                       │
│  │  │  ├─ 字符串加密(anti-strings)                               │
│  │  │  ├─ 控制流平坦化                                            │
│  │  │  └─ 函数名strip                                            │
│  │  │                                                             │
│  │  攻:分析SO                                                    │
│  │  │                                                             │
│  │  ├─ IDA Pro 静态分析                                           │
│  │  │  找到JNI_OnLoad                                             │
│  │  │  找到解密函数                                                │
│  │  │  识别加密算法(AES S-Box特征等)                              │
│  │  │                                                             │
│  │  ├─ 动态调试SO                                                 │
│  │  │  IDA远程调试                                                │
│  │  │  GDB attach                                                │
│  │  │  在解密函数处断点                                            │
│  │  │                                                             │
│  │  └─ Frida Hook SO函数                                         │
│  │     Interceptor.attach(解密函数地址)                            │
│  │     直接读取解密后的返回值                                       │
│  │                                                                │
│  ═══════════════ 第五层:高级对抗 ═══════════════                  │
│  │                                                                │
│  │  防:VMP(虚拟机保护)/ 指令抽取                                  │
│  │  │  将关键Java方法的字节码抽取                                   │
│  │  │  替换为Native调用                                           │
│  │  │  运行时通过自定义解释器执行                                   │
│  │  │  即使dump出DEX,关键方法体也是空的                            │
│  │  │                                                             │
│  │  攻:                                                          │
│  │  ├─ FART主动调用脱壳                                           │
│  │  │  主动触发每个方法执行                                        │
│  │  │  在ART解释器层面dump方法体                                   │
│  │  │                                                             │
│  │  └─ 分析VMP解释器                                              │
│  │     逆向自定义指令集                                            │
│  │     编写反汇编器                                                │
│  │                                                                │
└─────────────────────────────────────────────────────────────────┘

六、具体脱壳实战

方法1:Frida 内存 Dump

// frida -U -f com.target.app -l dump_dex.js

Java.perform(function() {
    
    // Hook 1: DexClassLoader构造函数
    var DexClassLoader = Java.use("dalvik.system.DexClassLoader");
    DexClassLoader.$init.implementation = function(dexPath, optDir, libPath, parent) {
        console.log("[+] DexClassLoader 加载: " + dexPath);
        
        // 读取解密后的DEX文件
        var file = new Java.use("java.io.File").$new(dexPath);
        var fis = new Java.use("java.io.FileInputStream").$new(file);
        var size = file.length();
        var buffer = Java.array('byte', new Array(size).fill(0));
        fis.read(buffer);
        
        // 保存到可访问目录
        var output = new Java.use("java.io.FileOutputStream")
            .$new("/data/local/tmp/dumped_" + Date.now() + ".dex");
        output.write(buffer);
        output.close();
        console.log("[+] DEX已dump!");
        
        return this.$init(dexPath, optDir, libPath, parent);
    };
    
    // Hook 2: InMemoryDexClassLoader (Android 8.0+)
    if (Java.available) {
        try {
            var InMemoryDex = Java.use("dalvik.system.InMemoryDexClassLoader");
            InMemoryDex.$init.overload('java.nio.ByteBuffer', 'java.lang.ClassLoader')
                .implementation = function(buf, parent) {
                    console.log("[+] InMemoryDexClassLoader detected!");
                    
                    var remaining = buf.remaining();
                    var bytes = Java.array('byte', new Array(remaining).fill(0));
                    buf.get(bytes);
                    buf.position(0);  // 重置position
                    
                    // 写入文件
                    var fos = new Java.use("java.io.FileOutputStream")
                        .$new("/data/local/tmp/inmem_" + Date.now() + ".dex");
                    fos.write(bytes);
                    fos.close();
                    console.log("[+] 内存DEX已dump! Size: " + remaining);
                    
                    return this.$init(buf, parent);
            };
        } catch(e) {}
    }
});

方法2:Hook openDexFile(更底层)

// 直接Hook ART层的DEX打开函数
Interceptor.attach(Module.findExportByName("libart.so", 
    "_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_"), 
{
    onEnter: function(args) {
        // args[1] = DEX数据指针
        // args[2] = DEX大小
        var base = args[1];
        var size = args[2].toInt32();
        
        console.log("[+] DexFile::OpenMemory called, size: " + size);
        
        // 检查DEX魔数
        var magic = Memory.readUtf8String(base, 4);
        if (magic === "dex\n") {
            // dump DEX
            var dexData = Memory.readByteArray(base, size);
            var file = new File("/data/local/tmp/art_dump_" + size + ".dex", "wb");
            file.write(dexData);
            file.close();
            console.log("[+] ART层DEX dump成功!");
        }
    }
});

方法3:通用脱壳(遍历内存搜索DEX)

// 扫描内存中所有DEX文件
function scanMemoryForDex() {
    Process.enumerateRanges('r--', {
        onMatch: function(range) {
            try {
                // 搜索DEX魔数: "dex\n035\0" 或 "dex\n037\0"
                var results = Memory.scanSync(range.base, range.size, 
                    "64 65 78 0a 30 33");  // "dex\n03"
                
                results.forEach(function(match) {
                    // 读取DEX头部获取文件大小
                    var fileSize = Memory.readU32(match.address.add(32));
                    
                    if (fileSize > 0x70 && fileSize < 0x10000000) {
                        console.log("[+] Found DEX at: " + match.address + 
                                   " Size: " + fileSize);
                        
                        var dexBytes = Memory.readByteArray(
                            match.address, fileSize);
                        var f = new File("/data/local/tmp/scan_" + 
                                        match.address + ".dex", "wb");
                        f.write(dexBytes);
                        f.close();
                    }
                });
            } catch(e) {}
        },
        onComplete: function() {
            console.log("[*] Memory scan complete");
        }
    });
}

// 延迟执行,等壳解密完成
setTimeout(scanMemoryForDex, 10000);

七、加固方案分级

┌────────────────────────────────────────────────────────────┐
│                     加固强度分级                              │
│                                                              │
│  Level 1: 整体加密(一代壳)                                  │
│  ├─ 整个DEX加密存储                                          │
│  ├─ 运行时整体解密加载                                       │
│  ├─ 对抗难度: ★☆☆☆☆                                        │
│  └─ 脱壳方法: 内存dump即可,DEX完整还原                       │
│                                                              │
│  Level 2: 类抽取(二代壳)                                    │
│  ├─ DEX中的类数据被抽取                                      │
│  ├─ 运行时动态回填                                           │
│  ├─ 对抗难度: ★★★☆☆                                        │
│  └─ 脱壳方法: Hook defineClass,主动加载所有类后dump           │
│                                                              │
│  Level 3: 方法体抽取/指令抽取(三代壳)                        │
│  ├─ 方法的字节码指令被抽空                                    │
│  ├─ CodeItem 被置空或替换为Native跳板                         │
│  ├─ 运行时才回填指令                                          │
│  ├─ 对抗难度: ★★★★☆                                        │
│  └─ 脱壳方法: FART主动调用 + CodeItem dump                    │
│                                                              │
│  Level 4: VMP虚拟化保护(四代壳)                              │
│  ├─ 关键方法被编译为自定义指令集                               │
│  ├─ 通过自定义虚拟机解释执行                                  │
│  ├─ 即使dump也无法还原原始逻辑                                │
│  ├─ 对抗难度: ★★★★★                                        │
│  └─ 脱壳方法: 逆向VMP解释器 / trace指令流                     │
│                                                              │
│  商业方案对应:                                                │
│  ├─ 爱加密    → Level 2~3                                    │
│  ├─ 梆梆安全  → Level 2~4                                    │
│  ├─ 腾讯乐固  → Level 2~3                                    │
│  ├─ 360加固   → Level 2~3                                    │
│  └─ 网易易盾  → Level 2~4                                    │
│                                                              │
└────────────────────────────────────────────────────────────┘

八、核心哲学

┌──────────────────────────────────────────────────┐
│                                                    │
│    不管加密多复杂、壳多厚、检测多严格,              │
│    有一个不可打破的事实:                            │
│                                                    │
│    ┌──────────────────────────────────┐            │
│    │  CPU 只能执行明文指令              │            │
│    │  DEX 最终必须以明文加载到内存       │            │
│    │  这个瞬间,就是脱壳的窗口           │            │
│    └──────────────────────────────────┘            │
│                                                    │
│    加固的目标不是"不可破解"                          │
│    而是"提高破解成本,使其超过收益"                   │
│                                                    │
└──────────────────────────────────────────────────┘

posted on 2026-02-11 05:45  GKLBB  阅读(52)  评论(0)    收藏  举报