性能優化 (七) APK 加固之 Dex 加解密,反編譯都看不到項目主要代碼。

性能優化系列

APP 啓動優化html

UI 繪製優化java

內存優化android

圖片壓縮git

長圖優化github

電量優化數組

Dex 加解密性能優化

動態替換 Application架構

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

APP 持續運行之進程保活實現ide

ProGuard 對代碼和資源壓縮

APK 極限壓縮

簡介

如今隨意在應用市場下載一個 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 限制

  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 加密與解密

流程:

  1. 拿到 APK 解壓獲得全部的 dex 文件。
  2. 經過 Tools 來進行加密,並把加密後的 dex 和代理應用 class.dex 合併,而後從新簽名,對齊,打包。
  3. 當用戶安裝 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;

  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 (如今只能看見代理 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 加解密已經學習完了,若是想看本身試一試能夠參考個人代碼

代碼傳送陣

相關文章
相關標籤/搜索