效能優化 (七) APK 加固之 Dex 加解密,反編譯都看不到專案主要程式碼。

DevYK發表於2019-06-02

效能優化系列

APP 啟動優化

UI 繪製優化

記憶體優化

圖片壓縮

長圖優化

電量優化

Dex 加解密

動態替換 Application

APP 穩定性之熱修復原理探索

APP 持續執行之程式保活實現

ProGuard 對程式碼和資源壓縮

APK 極限壓縮

簡介

現在隨意在應用市場下載一個 APK 檔案然後反編譯,95% 以上基本上都是經過混淆,加密,或第三方加固(第三方加固也是這個原理),那麼今天我們就對 Dex 來進行加密解密。讓反編譯無法正常閱讀專案原始碼。

加密後的結構

APK 分析

效能優化 (七) APK 加固之 Dex 加解密,反編譯都看不到專案主要程式碼。
通過 AS 工具分析加密後的 APK 檔案,檢視 dex 是報錯的,要的就是這個效果。

反編譯效果

效能優化 (七) 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 限制

  1. 如果您的 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);

  2. 通過混淆 開啟 ProGuard 移除未使用的程式碼,構建程式碼壓縮。

  3. 減少第三方庫的直接依賴,儘可能下載原始碼,需要什麼就用什麼沒必要依賴整個專案。

Dex 加密與解密

效能優化 (七) APK 加固之 Dex 加解密,反編譯都看不到專案主要程式碼。

流程:

  1. 拿到 APK 解壓得到所有的 dex 檔案。
  2. 通過 Tools 來進行加密,並把加密後的 dex 和代理應用 class.dex 合併,然後重新簽名,對齊,打包。
  3. 當使用者安裝 APK 開啟進入代理解密的 Application 時,反射得到 dexElements 並將解密後的 dex 替換 DexPathList 中的 dexElements .

Dex 檔案載入過程

既然要查 Dex 載入過程,那麼得先知道從哪個原始碼 class 入手,既然不知道那麼我們就先列印下 ClassLoader ;

效能優化 (七) APK 加固之 Dex 加解密,反編譯都看不到專案主要程式碼。

下面就以一個流程圖來詳細瞭解下 Dex 載入過程吧

效能優化 (七) APK 加固之 Dex 加解密,反編譯都看不到專案主要程式碼。

最後我們得知在 findClass(String name,List sup) 遍歷 dexElements 找到 Class 並交給 Android 載入。

Dex 解密

現在我們知道 dex 載入流程了 , 那麼我們怎麼進行來對 dex 解密勒,剛剛我們得知需要遍歷 dexElements 來找到 Class 那麼我們是不是可以在遍歷之前 ,初始化 dexElements 的時候。反射得到 dexElements 將我們解密後的 dex 交給 dexElements 。下面我們就通過程式碼來進行解密 dex 並替換 DexPathList 中的 dexElements;

  1. 得到當前加密了的 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");
    複製程式碼
  2. 得到我們需要載入的 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();
         }
      }
    }
    複製程式碼
  3. 把解密後的 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 加密

  1. 製作只包含解密程式碼的 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");
            }
    複製程式碼
  2. 加密 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();
    }
    複製程式碼
  3. 把 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 加固之 Dex 加解密,反編譯都看不到專案主要程式碼。

    加密後的 apk (現在只能看見代理 Application )

    效能優化 (七) APK 加固之 Dex 加解密,反編譯都看不到專案主要程式碼。

打包

對齊

//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 加解密已經學習完了,如果想看自己試一試可以參考我的程式碼

程式碼傳送陣

專案如果使用:

效能優化 (七) APK 加固之 Dex 加解密,反編譯都看不到專案主要程式碼。

相關文章