Android APK 加壳是一种通过动态加载、代码加密和替换关键组件来保护应用核心代码的技术。其核心思想是在原始 APK 外部包裹一层“壳”程序,壳程序优先运行,负责解密、加载原始代码,并替换系统关键组件(如 Application),从而增加逆向分析的难度。以下将详细阐述加壳的技术方案、关键步骤,并提供部分核心代码实现。
一、加壳技术原理与整体方案
加壳过程涉及三个核心角色:加壳程序(加密工具)、解壳程序(壳 APK)和源程序(被保护的原始 APK)。整体流程可分为两种主要方案:
解壳数据位于解壳程序文件尾部:这是较简单的方案。加壳程序将加密后的源 APK(解壳数据)附加到壳程序 DEX 文件末尾,并修改 DEX 头信息(checksum、signature、file_size)。运行时,壳程序读取并解密这部分数据,再动态加载。
解壳数据位于解壳程序文件头:此方案更复杂,将解壳数据插入 DEX 文件头(如 0x70 偏移处),并需要修改 DEX 文件中大量的偏移量(如 string_ids_off、class_defs_off 等)。
无论哪种方案,其运行时流程均为:系统启动壳 APK → 壳程序(ProxyApplication)运行 → 解密原始 APK 数据 → 通过 DexClassLoader 动态加载解密后的 DEX → 替换系统 Application 为原始 Application → 执行原始业务代码。
二、关键实现步骤与代码
一个完整的加壳方案需要在壳程序(代理 Application)、加壳工具(Java 程序) 和源程序配置三部分进行实现。以下以方案一为例,提供关键环节的代码。
1. 壳程序(ProxyApplication)的实现
壳程序是一个独立的 Android 工程,其Application类(通常命名为ProxyApplication)是整个加壳机制的核心。它需要在attachBaseContext或onCreate中最早执行解壳和加载操作。
关键任务:
读取并解密原始 APK:从自身 APK 的特定位置(如 assets 或 DEX 文件尾部)读取加密数据,进行解密。
动态加载 DEX:使用
DexClassLoader加载解密后的 DEX 文件。替换 Application:通过反射,将系统当前加载的
Application对象替换为源程序中配置的原始Application,以确保源程序的正常生命周期和逻辑。修复类加载器:将系统
PathClassLoader的dexElements与壳程序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 头部的
checksum、signature和file_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>三、进阶加固方案与注意事项
Native 层加壳:为了进一步提高安全性,可将核心的解密和加载逻辑用 C/C++ 实现,编译为 SO 库。壳程序的
ProxyApplication调用 JNI 方法,在 Native 层完成解密和内存加载,极大增加静态分析的难度。代码混淆:加壳通常需与代码混淆(ProGuard/R8)结合使用。混淆通过重命名类、方法、变量名,并移除无用代码,降低反编译后代码的可读性,这是加固的基础手段。
反调试与完整性校验:在壳代码中可集成反调试检测(检查调试器连接)、签名校验和代码完整性校验,防止动态调试和篡改。
兼容性与性能:加壳涉及大量反射和底层操作,可能引发兼容性问题(尤其是不同 Android 版本)。同时,解密和动态加载过程会带来轻微的性能开销和启动延迟,需进行充分测试。
总结
APK 加壳是一种有效的主动防御技术,通过动态加载、反射替换和代码加密为核心,构建了一个先于原始代码执行的保护层。其实现关键在于代理 Application、DexClassLoader 动态加载和DEX 文件结构的精准修改。然而,没有绝对安全的方案,加固技术旨在提高逆向成本和门槛。在实际应用中,建议采用加壳(尤其是 Native 层加固)+ 强混淆 + 运行时安全检测的多层防御体系,并根据应用特点在安全、性能和兼容性之间取得平衡。
以上代码和方案为原理性示例,实际实现需处理更多边界情况、错误处理和不同 Android 版本的适配。完整的工程通常包含加壳工具、壳程序、反射工具类及加解密模块等多个部分。