一、加固原理
加固原理相對簡單,首先對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();
}