效能優化系列
簡介
現在隨意在應用市場下載一個 APK 檔案然後反編譯,95% 以上基本上都是經過混淆,加密,或第三方加固(第三方加固也是這個原理),那麼今天我們就對 Dex 來進行加密解密。讓反編譯無法正常閱讀專案原始碼。
加密後的結構
APK 分析
通過 AS 工具分析加密後的 APK 檔案,檢視 dex 是報錯的,要的就是這個效果。反編譯效果
想要對 Dex 加密 ,先來了解什麼是 64 K 問題
想要詳細瞭解 64 k 的問題可以參考官網
隨著 Android 平臺的持續成長,Android 應用的大小也在增加。當您的應用及其引用的庫達到特定大小時,您會遇到構建錯誤,指明您的應用已達到 Android 應用構建架構的極限。早期版本的構建系統按如下方式報告這一錯誤:
Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536
複製程式碼
較新版本的 Android 構建系統雖然顯示的錯誤不同,但指示的是同一問題:
trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.
複製程式碼
這些錯誤狀況都會顯示下面這個數字:65,536。這個數字很重要,因為它代表的是單個 Dalvik Executable (DEX) 位元組碼檔案內的程式碼可呼叫的引用總數。本節介紹如何通過啟用被稱為 Dalvik 可執行檔案分包的應用配置來越過這一限制,使您的應用能夠構建並讀取 Dalvik 可執行檔案分包 DEX 檔案。
關於 64K 引用限制
Android 5.0 之前版本的 Dalvik 可執行檔案分包支援
Android 5.0(API 級別 21)之前的平臺版本使用 Dalvik 執行時來執行應用程式碼。預設情況下,Dalvik 限制應用的每個 APK 只能使用單個 classes.dex
位元組碼檔案。要想繞過這一限制,您可以使用 Dalvik 可執行檔案分包支援庫,它會成為您的應用主要 DEX 檔案的一部分,然後管理對其他 DEX 檔案及其所包含程式碼的訪問。
Android 5.0 及更高版本的 Dalvik 可執行檔案分包支援
Android 5.0(API 級別 21)及更高版本使用名為 ART 的執行時,後者原生支援從 APK 檔案載入多個 DEX 檔案。ART 在應用安裝時執行預編譯,掃描 classesN.dex
檔案,並將它們編譯成單個 .oat
檔案,供 Android 裝置執行。因此,如果您的 minSdkVersion
為 21 或更高值,則不需要 Dalvik 可執行檔案分包支援庫。
解決 64K 限制
-
如果您的
minSdkVersion
設定為 21 或更高值,您只需在模組級build.gradle
檔案中將multiDexEnabled
設定為true
,如此處所示:android { defaultConfig { ... minSdkVersion 21 targetSdkVersion 28 multiDexEnabled true } ... } 複製程式碼
但是,如果您的
minSdkVersion
設定為 20 或更低值,則您必須按如下方式使用 Dalvik 可執行檔案分包支援庫:-
修改模組級
build.gradle
檔案以啟用 Dalvik 可執行檔案分包,並將 Dalvik 可執行檔案分包庫新增為依賴項,如此處所示android { defaultConfig { ... minSdkVersion 15 targetSdkVersion 28 multiDexEnabled true } ... } dependencies { compile 'com.android.support:multidex:1.0.3' } 複製程式碼
-
當前 Application extends MultiDexApplication {...} 或者 MultiDex.install(this);
-
-
通過混淆 開啟 ProGuard 移除未使用的程式碼,構建程式碼壓縮。
-
減少第三方庫的直接依賴,儘可能下載原始碼,需要什麼就用什麼沒必要依賴整個專案。
Dex 加密與解密
流程:
- 拿到 APK 解壓得到所有的 dex 檔案。
- 通過 Tools 來進行加密,並把加密後的 dex 和代理應用 class.dex 合併,然後重新簽名,對齊,打包。
- 當使用者安裝 APK 開啟進入代理解密的 Application 時,反射得到 dexElements 並將解密後的 dex 替換 DexPathList 中的 dexElements .
Dex 檔案載入過程
既然要查 Dex 載入過程,那麼得先知道從哪個原始碼 class 入手,既然不知道那麼我們就先列印下 ClassLoader ;
下面就以一個流程圖來詳細瞭解下 Dex 載入過程吧
最後我們得知在 findClass(String name,List sup) 遍歷 dexElements 找到 Class 並交給 Android 載入。
Dex 解密
現在我們知道 dex 載入流程了 , 那麼我們怎麼進行來對 dex 解密勒,剛剛我們得知需要遍歷 dexElements 來找到 Class 那麼我們是不是可以在遍歷之前 ,初始化 dexElements 的時候。反射得到 dexElements 將我們解密後的 dex 交給 dexElements 。下面我們就通過程式碼來進行解密 dex 並替換 DexPathList 中的 dexElements;
-
得到當前加密了的 APK 檔案 並解壓
//得到當前加密了的APK檔案 File apkFile=new File(getApplicationInfo().sourceDir); //把apk解壓 app_name+"_"+app_version目錄中的內容需要boot許可權才能用 File versionDir = getDir(app_name+"_"+app_version,MODE_PRIVATE); File appDir=new File(versionDir,"app"); File dexDir=new File(appDir,"dexDir"); 複製程式碼
-
得到我們需要載入的 Dex 檔案
//把apk解壓到appDir Zip.unZip(apkFile,appDir); //獲取目錄下所有的檔案 File[] files=appDir.listFiles(); for (File file : files) { String name=file.getName(); if(name.endsWith(".dex") && !TextUtils.equals(name,"classes.dex")){ try{ AES.init(AES.DEFAULT_PWD); //讀取檔案內容 byte[] bytes=Utils.getBytes(file); //解密 byte[] decrypt=AES.decrypt(bytes); //寫到指定的目錄 FileOutputStream fos=new FileOutputStream(file); fos.write(decrypt); fos.flush(); fos.close(); dexFiles.add(file); }catch (Exception e){ e.printStackTrace(); } } } 複製程式碼
-
把解密後的 dex 載入到系統
private void loadDex(List<File> dexFiles, File versionDir) throws Exception{ //1.獲取pathlist Field pathListField = Utils.findField(getClassLoader(), "pathList"); Object pathList = pathListField.get(getClassLoader()); //2.獲取陣列dexElements Field dexElementsField=Utils.findField(pathList,"dexElements"); Object[] dexElements=(Object[])dexElementsField.get(pathList); //3.反射到初始化dexElements的方法 Method makeDexElements=Utils.findMethod(pathList,"makePathElements",List.class,File.class,List.class); ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); Object[] addElements=(Object[])makeDexElements.invoke(pathList,dexFiles,versionDir,suppressedExceptions); //合併陣列 Object[] newElements= (Object[])Array.newInstance(dexElements.getClass().getComponentType(),dexElements.length+addElements.length); System.arraycopy(dexElements,0,newElements,0,dexElements.length); System.arraycopy(addElements,0,newElements,dexElements.length,addElements.length); //替換 DexPathList 中的 element 陣列 dexElementsField.set(pathList,newElements); } 複製程式碼
解密已經完成了,下面來看看加密吧,這裡為什麼先說解密勒,因為 加密涉及到 簽名,打包,對齊。所以留到最後講。
Dex 加密
-
製作只包含解密程式碼的 dex
1. sdk\build-tools 中執行下面命令 會得到包含 dex 的 jar dx --dex --output out.dex in.jar 2. 通過 exec 執行 File aarFile=new File("proxy_core/build/outputs/aar/proxy_core-debug.aar"); File aarTemp=new File("proxy_tools/temp"); Zip.unZip(aarFile,aarTemp); File classesJar=new File(aarTemp,"classes.jar"); File classesDex=new File(aarTemp,"classes.dex"); String absolutePath = classesDex.getAbsolutePath(); String absolutePath1 = classesJar.getAbsolutePath(); //dx --dex --output out.dex in.jar //dx --dex --output //D:\Downloads\android_space\DexDEApplication\proxy_tools\temp\classes.dex //D:\Downloads\android_space\DexDEApplication\proxy_tools\temp\classes.jar Process process=Runtime.getRuntime().exec("cmd /c dx --dex --output "+classesDex.getAbsolutePath() +" "+classesJar.getAbsolutePath()); process.waitFor(); if(process.exitValue()!=0){ throw new RuntimeException("dex error"); } 複製程式碼
-
加密 apk 中的 dex 檔案
File apkFile=new File("app/build/outputs/apk/debug/app-debug.apk"); File apkTemp=new File("app/build/outputs/apk/debug/temp"); Zip.unZip(apkFile,apkTemp); //只要dex檔案拿出來加密 File[] dexFiles=apkTemp.listFiles(new FilenameFilter() { @Override public boolean accept(File file, String s) { return s.endsWith(".dex"); } }); //AES加密了 AES.init(AES.DEFAULT_PWD); for (File dexFile : dexFiles) { byte[] bytes = Utils.getBytes(dexFile); byte[] encrypt = AES.encrypt(bytes); FileOutputStream fos=new FileOutputStream(new File(apkTemp, "secret-"+dexFile.getName())); fos.write(encrypt); fos.flush(); fos.close(); dexFile.delete(); } 複製程式碼
-
把 dex 放入 apk 加壓目錄,重新壓成 apk 檔案
File apkTemp=new File("app/build/outputs/apk/debug/temp"); File aarTemp=new File("proxy_tools/temp"); File classesDex=new File(aarTemp,"classes.dex"); classesDex.renameTo(new File(apkTemp,"classes.dex")); File unSignedApk=new File("app/build/outputs/apk/debug/app-unsigned.apk"); Zip.zip(apkTemp,unSignedApk); 複製程式碼
現在可以看下加密後的檔案,和未加密的檔案
未加密 apk:
加密後的 apk (現在只能看見代理 Application )
打包
對齊
//apk整理對齊工具 未壓縮的資料開頭均相對於檔案開頭部分執行特定的位元組對齊,減少應用執行記憶體。
zipalign -f 4 in.apk out.apk
//比對 apk 是否對齊
zipalign -c -v 4 output.apk
//最後提示 Verification succesful 說明對齊成功了
236829 res/mipmap-xxxhdpi-v4/ic_launcher.png (OK - compressed)
245810 res/mipmap-xxxhdpi-v4/ic_launcher_round.png (OK - compressed)
260956 resources.arsc (OK - compressed)
317875 secret-classes.dex (OK - compressed)
2306140 secret-classes2.dex (OK - compressed)
2477544 secret-classes3.dex (OK - compressed)
Verification succesful
複製程式碼
簽名打包 apksigner
//sdk\build-tools\24.0.3 以上,apk簽名工具
apksigner sign --ks jks檔案地址 --ks-key-alias 別名 --ks-pass pass:jsk密碼 --key-pass pass:別名密碼 --out out.apk in.apk
複製程式碼
總結
其實原理就是把主要程式碼通過命令 dx 生成 dex 檔案,然後把加密後的 dex 合併在代理 class.dex 中。這樣雖然還是能看見代理中的程式碼,但是主要程式碼已經沒有暴露出來了,就已經實現了我們想要的效果。如果封裝的好的話(JNI 中實現主要解密程式碼),基本上就哈也看不見了。ClassLoader 還是很重要的,熱修復跟熱載入都是這原理。學到這裡 DEX 加解密已經學習完了,如果想看自己試一試可以參考我的程式碼
專案如果使用: