一、加固原理
加固原理相对简单,首先对apk进行解压获取到原dex, 接着对原dex 进行加密,制作并生成壳dex(加载时用来解密原dex), 并从新打包成apk, 运行时利用壳dex对加密的dex进行解密并加载到内存中。 是不是很简单? 当然,这只是大概的原理,下面我们将详细叙述。
1.1 加密
加密的方式有很多种,如RSA,AES等,加固中常用的加密算法是AES,由于加密算法不是本文的重点,读者可自行去了解相关算法的区别。 这里我使用gradle插件的方式在编译的时候自动解压加密并重新打包,避免了手动加密的繁琐。 解压加密的核心代码处理如下:
// 解压 apk 文件 , 获取所有的 dex 文件
// 被解压的 apk 文件
var apkFile = File(apk)
// 解压的目标文件夹
var apkUnZipFile = File("app/build/outputs/apk/release/unZipFile")
// 解压文件
var rawPathList=unZip(apkFile, apkUnZipFile)
// println(Arrays.asList(rawPathList))
// 从被解压的 apk 文件中找到所有的 dex 文件, 小项目只有 1 个, 大项目可能有多个
// 使用文件过滤器获取后缀是 .dex 的文件
var dexFiles : Array<File> = apkUnZipFile.listFiles({ file: File, s: String ->
s.endsWith(".dex")
})
// 加密找到的 dex 文件
var aes = AES(AES.DEFAULT_PWD)
// 遍历 dex 文件
for(dexFile: File in dexFiles){
// 读取文件数据
var bytes = getBytes(dexFile)
// 加密文件数据
var encryptedBytes = aes.encrypt(bytes)
// 将加密后的数据写出到指定目录
var outputFile = File(apkUnZipFile, "secret-${dexFile.name}")
// 创建对应输出流
var fileOutputStream = FileOutputStream(outputFile)
// 将加密后的 dex 文件写出, 然后刷写 , 关闭该输出流
fileOutputStream.write(encryptedBytes)
fileOutputStream.flush()
fileOutputStream.close()
// 删除原来的文件
dexFile.delete()
}
1.2 制作壳dex
为了在点击桌面icon首先执行我们的壳Application, 首先要在打包过程中,将原Application 替换成 壳程序的Application, 同时使用Meta-Data 记录原Application的全路径名,最终的实现如下:
<!-- 写入权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:name="com.test.multipledex.ProxyApplication"
android:theme="@style/Theme.AppEncrypton">
<!-- app_name 值是该应用的 Application 的真实全类名
真实 Application : kim.hsl.dex.MyApplication
代理 Application : kim.hsl.multipledex.ProxyApplication -->
<meta-data android:name="app_name" android:value="com.test.appencrypton.MyApplication"/>
<!-- DEX 解密之后的目录名称版本号 , 完整目录名称为 :
kim.hsl.dex.MyApplication_1.0 -->
<meta-data android:name="app_version" android:value="1.0"/>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
1.3 解密
首先获取到安装APK 文件的加密dex,然后将加密后的dex 文件进行解密 并加载到内存中。
/*
I . 解密与加载多 DEX 文件
先进行解密, 然后再加载解密之后的 DEX 文件
1. 先获取当前的 APK 文件
2. 然后解压该 APK 文件
*/
// 获取当前的 APK 文件, 下面的 getApplicationInfo().sourceDir 就是本应用 APK 安装文件的全路径
File apkFile = new File(getApplicationInfo().sourceDir);
// 获取在 app Module 下的 AndroidManifest.xml 中配置的元数据,
// 应用真实的 Application 全类名
// 解密后的 dex 文件存放目录
ApplicationInfo applicationInfo = null;
packageName=getPackageName();
applicationInfo = getPackageManager().getApplicationInfo(
packageName,
PackageManager.GET_META_DATA
);
Bundle metaData = applicationInfo.metaData;
if (metaData != null) {
// 检查是否存在 app_name 元数据
if (metaData.containsKey("app_name")) {
app_name = metaData.getString("app_name").toString();
}
// 检查是否存在 app_version 元数据
if (metaData.containsKey("app_version")) {
app_version = metaData.getString("app_version").toString();
}
}
// 创建用户的私有目录 , 将 apk 文件解压到该目录中
File privateDir = getDir(app_name + "_" + app_version, MODE_PRIVATE);
Log.i(TAG, "attachBaseContext 创建用户的私有目录 : " + privateDir.getAbsolutePath());
// 在上述目录下创建 app 目录
// 创建该目录的目的是存放解压后的 apk 文件的
File appDir = new File(privateDir, "app");
// app 中存放的是解压后的所有的 apk 文件
// app 下创建 dexDir 目录 , 将所有的 dex 目录移动到该 dexDir 目录中
// dexDir 目录存放应用的所有 dex 文件
// 这些 dex 文件都需要进行解密
File dexDir = new File(appDir, "dexDir");
// 遍历解压后的 apk 文件 , 将需要加载的 dex 放入如下集合中
ArrayList<File> dexFiles = new ArrayList<File>();
// 如果该 dexDir 不存在 , 或者该目录为空 , 并进行 MD5 文件校验
if (!dexDir.exists() || dexDir.list().length == 0) {
// 将 apk 中的文件解压到了 appDir 目录
ZipUtils.unZipApk(apkFile, appDir);
if (!dexDir.exists()){
dexDir.mkdir();
}
// 获取 appDir 目录下的所有文件
File[] files = appDir.listFiles();
// Log.i(TAG, "attachBaseContext appDir 目录路径 : " + appDir.getAbsolutePath());
// Log.i(TAG, "attachBaseContext appDir 目录内容 : " + files);
// 遍历文件名称集合
for (int i = 0; i < files.length; i++) {
File file = files[i];
// Log.i(TAG, "attachBaseContext 遍历 " + i + " . " + file);
// 如果文件后缀是 .dex , 并且不是 主 dex 文件 classes.dex
// 符合上述两个条件的 dex 文件放入到 dexDir 中
if (file.getName().endsWith(".dex") &&
!TextUtils.equals(file.getName(), "classes.dex")) {
// 筛选出来的 dex 文件都是需要解密的
// 解密需要使用 OpenSSL 进行解密
// 获取该文件的二进制 Byte 数据
// 这些 Byte 数组就是加密后的 dex 数据
byte[] bytes = OpenSSL.getBytes(file);
// 解密该二进制数据, 并替换原来的加密 dex, 直接覆盖原来的文件即可
File temp=new File(dexDir,file.getName());
Log.i(TAG, "temp: " + temp.getAbsolutePath());
OpenSSL.decrypt(bytes, temp);
// 将解密完毕的 dex 文件放在需要加载的 dex 集合中
dexFiles.add(temp);
// 拷贝到 dexDir 中
Log.i(TAG, "attachBaseContext 解密完成 被解密文件是 : " + temp);
}// 判定是否是需要解密的 dex 文件
}// 遍历 apk 解压后的文件
} else {
Log.i(TAG, "再次启动");
// 已经解密完成, 此时不需要解密, 直接获取 dexDir 中的文件即可
for (File file : dexDir.listFiles()) {
if (file.getName().endsWith(".dex")){
dexFiles.add(file);
}
}
}
Log.i(TAG, "attachBaseContext 解密完成 dexFiles : " + dexFiles);
for (int i = 0; i < dexFiles.size(); i++) {
Log.i(TAG, i + " . " + dexFiles.get(i).getAbsolutePath());
}
// 截止到此处 , 已经拿到了解密完毕 , 需要加载的 dex 文件
// 加载自己解密的 dex 文件
loadDex(dexFiles, privateDir);
Log.i(TAG, "attachBaseContext 完成");
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}