简单的Android APK加壳方案

简单的Android APK加壳方案

_

Android APK 加壳是一种通过动态加载、代码加密和替换关键组件来保护应用核心代码的技术。其核心思想是在原始 APK 外部包裹一层“壳”程序,壳程序优先运行,负责解密、加载原始代码,并替换系统关键组件(如 Application),从而增加逆向分析的难度。以下将详细阐述加壳的技术方案、关键步骤,并提供部分核心代码实现。

一、加壳技术原理与整体方案

加壳过程涉及三个核心角色:加壳程序(加密工具)、解壳程序(壳 APK)和源程序(被保护的原始 APK)。整体流程可分为两种主要方案:

  1. 解壳数据位于解壳程序文件尾部:这是较简单的方案。加壳程序将加密后的源 APK(解壳数据)附加到壳程序 DEX 文件末尾,并修改 DEX 头信息(checksum、signature、file_size)。运行时,壳程序读取并解密这部分数据,再动态加载。

  2. 解壳数据位于解壳程序文件头:此方案更复杂,将解壳数据插入 DEX 文件头(如 0x70 偏移处),并需要修改 DEX 文件中大量的偏移量(如 string_ids_off、class_defs_off 等)。

无论哪种方案,其运行时流程均为:系统启动壳 APK → 壳程序(ProxyApplication)运行 → 解密原始 APK 数据 → 通过 DexClassLoader 动态加载解密后的 DEX → 替换系统 Application 为原始 Application → 执行原始业务代码

二、关键实现步骤与代码

一个完整的加壳方案需要在壳程序(代理 Application)加壳工具(Java 程序)源程序配置三部分进行实现。以下以方案一为例,提供关键环节的代码。

1. 壳程序(ProxyApplication)的实现

壳程序是一个独立的 Android 工程,其Application类(通常命名为ProxyApplication)是整个加壳机制的核心。它需要在attachBaseContextonCreate中最早执行解壳和加载操作。

关键任务

  • 读取并解密原始 APK:从自身 APK 的特定位置(如 assets 或 DEX 文件尾部)读取加密数据,进行解密。

  • 动态加载 DEX:使用DexClassLoader加载解密后的 DEX 文件。

  • 替换 Application:通过反射,将系统当前加载的Application对象替换为源程序中配置的原始Application,以确保源程序的正常生命周期和逻辑。

  • 修复类加载器:将系统PathClassLoaderdexElements与壳程序DexClassLoader加载的dexElements合并,确保所有类都能被正确找到。

部分核心代码示例(ProxyApplication)

public class ProxyApplication extends Application {
    private String apkFileName; // 解密后APK的存储路径
    private String odexPath; // 优化后DEX的存放目录
    private String libPath; // so库目录

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        try {
            // 1. 初始化目录
            File odex = this.getDir("payload_odex", MODE_PRIVATE);
            File libs = this.getDir("payload_lib", MODE_PRIVATE);
            odexPath = odex.getAbsolutePath();
            libPath = libs.getAbsolutePath();
            apkFileName = odex.getAbsolutePath() + "/payload.apk";

            // 2. 读取并解密原始APK数据(此处需结合加壳工具的加密方式)
            byte[] dexdata = this.readDexFileFromApk(); // 从自身APK读取加密数据
            this.splitPayLoadFromDex(dexdata); // 解密并保存为payload.apk

            // 3. 配置动态加载环境
            this.configApplicationEnv();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void configApplicationEnv() throws Exception {
        // 获取ActivityThread实例
        Object currentActivityThread = RefInvoke.invokeStaticMethod(
                "android.app.ActivityThread", "currentActivityThread", new Class[]{}, new Object[]{});
        
        // 获取当前应用的LoadedApk对象
        HashMap mPackages = (HashMap) RefInvoke.getFieldOjbect(
                "android.app.ActivityThread", currentActivityThread, "mPackages");
        WeakReference wr = (WeakReference) mPackages.get(this.getPackageName());
        
        // 创建DexClassLoader加载解密后的APK
        DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath, libPath,
                (ClassLoader) RefInvoke.getFieldOjbect("android.app.LoadedApk", wr.get(), "mClassLoader"));
        
        // 关键:替换LoadedApk中的mClassLoader为我们的DexClassLoader
        RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader", wr.get(), dLoader);

        // 4. 替换Application(从meta-data读取原始Application类名)
        ApplicationInfo appInfo = this.getPackageManager().getApplicationInfo(
                this.getPackageName(), PackageManager.GET_META_DATA);
        Bundle bundle = appInfo.metaData;
        if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) {
            String appClassName = bundle.getString("APPLICATION_CLASS_NAME");
            Application app = (Application) dLoader.loadClass(appClassName).newInstance();
            
            // 通过反射替换ContextImpl、LoadedApk等内部成员中的Application引用
            RefInvoke.setFieldOjbect("android.app.ContextImpl", "mOuterContext",
                    this.getBaseContext(), app);
            RefInvoke.setFieldOjbect("android.content.ContextWrapper", "mBase", app, this.getBaseContext());
            
            Object mBoundApplication = RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread", currentActivityThread, "mBoundApplication");
            Object info = RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread$AppBindData", mBoundApplication, "info");
            RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication", info, app);
        }
    }
    
    // 反射工具类RefInvoke的方法示例(简化)
    public static Object invokeStaticMethod(String className, String methodName, 
            Class[] paramTypes, Object[] paramValues) throws Exception {
        Class clazz = Class.forName(className);
        Method method = clazz.getMethod(methodName, paramTypes);
        return method.invoke(null, paramValues);
    }
    // ... 其他反射工具方法
}

2. 加壳工具(Java程序)的实现

加壳工具是一个独立的 Java 应用程序,负责对原始 APK 进行加密,并将其与壳程序的 DEX 合并。

关键任务

  • 加密源 APK:使用 AES 等加密算法对原始 APK 文件进行加密。

  • 合并 DEX:将加密后的数据写入壳程序 DEX 文件的末尾(或头部),并添加数据长度信息。

  • 修正 DEX 头:由于 DEX 文件结构被改变,必须重新计算并更新 DEX 头部的checksumsignaturefile_size等字段。

部分核心代码示例(修正 DEX 头)

public class DexShellTool {
    // 修正DEX文件的Adler32校验和
    private static void fixAdler32Header(byte[] dexBytes) {
        Adler32 adler = new Adler32();
        // 从DEX头后(偏移12字节)开始计算整个剩余数据的校验和
        adler.update(dexBytes, 12, dexBytes.length - 12);
        long value = adler.getValue();
        int va = (int) value;
        byte[] newcs = intToByte(va); // 将int转为4字节数组
        byte[] recs = new byte[4];
        // 转换为小端序(Little-Endian)
        for (int i = 0; i < 4; i++) {
            recs[i] = newcs[newcs.length - 1 - i];
        }
        // 将校验和写回DEX头偏移8字节处
        System.arraycopy(recs, 0, dexBytes, 8, 4);
    }

    // 修正DEX文件的SHA-1签名
    private static void fixSHA1Header(byte[] dexBytes) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        // 从DEX头后(偏移32字节)开始计算签名
        md.update(dexBytes, 32, dexBytes.length - 32);
        byte[] newdt = md.digest();
        // 将签名写回DEX头偏移12字节处
        System.arraycopy(newdt, 0, dexBytes, 12, 20);
    }

    // 修正DEX文件大小字段
    private static void fixFileSizeHeader(byte[] dexBytes) {
        byte[] newfs = intToByte(dexBytes.length);
        byte[] refs = new byte[4];
        for (int i = 0; i < 4; i++) {
            refs[i] = newfs[newfs.length - 1 - i];
        }
        // 将文件大小写回DEX头偏移32字节处
        System.arraycopy(refs, 0, dexBytes, 32, 4);
    }
    
    private static byte[] intToByte(int number) {
        byte[] b = new byte[4];
        for (int i = 3; i >= 0; i--) {
            b[i] = (byte) (number % 256);
            number >>= 8;
        }
        return b;
    }
}

3. 源程序与壳程序的配置

  • 源程序:其AndroidManifest.xml中需配置自己的Application类(如MyApplication),但打包时此文件会被修改。

  • 壳程序配置:壳程序的AndroidManifest.xml中,<application>标签的android:name属性指向ProxyApplication。同时,需要通过<meta-data>标签将源程序的Application类名传递给壳程序。

<!-- 壳程序的AndroidManifest.xml -->
<application
    android:name="com.shell.ProxyApplication"
    ... >
    <!-- 传递原始Application类名 -->
    <meta-data
        android:name="APPLICATION_CLASS_NAME"
        android:value="com.original.MyApplication" />
    <!-- 其他配置,如壳程序自身的Activity(可选) -->
</application>

三、进阶加固方案与注意事项

  1. Native 层加壳:为了进一步提高安全性,可将核心的解密和加载逻辑用 C/C++ 实现,编译为 SO 库。壳程序的ProxyApplication调用 JNI 方法,在 Native 层完成解密和内存加载,极大增加静态分析的难度。

  2. 代码混淆:加壳通常需与代码混淆(ProGuard/R8)结合使用。混淆通过重命名类、方法、变量名,并移除无用代码,降低反编译后代码的可读性,这是加固的基础手段。

  3. 反调试与完整性校验:在壳代码中可集成反调试检测(检查调试器连接)、签名校验和代码完整性校验,防止动态调试和篡改。

  4. 兼容性与性能:加壳涉及大量反射和底层操作,可能引发兼容性问题(尤其是不同 Android 版本)。同时,解密和动态加载过程会带来轻微的性能开销和启动延迟,需进行充分测试。

总结

APK 加壳是一种有效的主动防御技术,通过动态加载、反射替换和代码加密为核心,构建了一个先于原始代码执行的保护层。其实现关键在于代理 ApplicationDexClassLoader 动态加载DEX 文件结构的精准修改。然而,没有绝对安全的方案,加固技术旨在提高逆向成本和门槛。在实际应用中,建议采用加壳(尤其是 Native 层加固)+ 强混淆 + 运行时安全检测的多层防御体系,并根据应用特点在安全、性能和兼容性之间取得平衡。

以上代码和方案为原理性示例,实际实现需处理更多边界情况、错误处理和不同 Android 版本的适配。完整的工程通常包含加壳工具、壳程序、反射工具类及加解密模块等多个部分。

APK程序中TCP与HTTPS连接的数据加密安全实践 2026-01-13
图片Base64编码存储与前端展示全流程技术 2026-01-13

© 2026 网络攻防研究