App加固(dex加密)

什么是Dex文件?

classes.dex是apk組成的一部分,包含了能被Dalvik/Art理解的可執(zhí)行文件,類似Windows的exe文件;

APK組成:

  • 1. assets目錄:存放assets目錄下的文件,可以通過(guò)AssetManager對(duì)象獲取

  • 2. lib目錄:存放所支持的CPU架構(gòu)對(duì)應(yīng)的二進(jìn)制文件(so文件),這些文件用來(lái)各自支持自己CPU架構(gòu)的二進(jìn)制接口(ABI)

  • 3. res目錄:存放res目錄下沒(méi)有被編譯到arsc文件的資源,layout,drawable,mipmap等

  • 4. META-INF目錄:存放簽名的目錄,

  • 5. classes.dex文件:dvm的可執(zhí)行文件,將R.java,java source Coed,java interface打包成dex文件

  • 6. resources.arsc文件(資源映射表):res/values目錄下的所有配置內(nèi)容,以及在APK res目錄下文件文件的映射方式

  • 7. Manifest文件:配置文件,同項(xiàng)目中的manifest文件

Dex文件內(nèi)容:

  • 文件頭:記錄了dex文件的一些基本信息, dex文件大小,dex文件頭大小,sha1簽名,checksum校驗(yàn)和,以及大致的數(shù)據(jù)分布

  • 索引區(qū):存放數(shù)據(jù)的偏移量

  • 數(shù)據(jù)區(qū):真實(shí)的數(shù)據(jù)存放在data數(shù)據(jù)區(qū)

APK打包流程:

1. aapt將資源文件打包成R.Java, resource.arsc ,res目錄
2. aidl 將aidl 接口解析成對(duì)應(yīng)的java接口
3. Javac 將源代碼編譯成.class字節(jié)碼
4. dx.bat將class字節(jié)碼轉(zhuǎn)化成dvm字節(jié)碼(dex文件)
5. 打包生成APK
6. JarSignerapk進(jìn)行debug或release簽名
7. zipalign對(duì)其操作,APK包中所有的資源文件距離文件起始偏移為4字節(jié)整數(shù)倍,這樣通過(guò)內(nèi)存映射訪問(wèn)apk文件時(shí)的速度會(huì)更快

上述的操作都是通過(guò)Android SDK自帶的工具來(lái)完成;

Dex加密
下面通過(guò)一個(gè)簡(jiǎn)單的demo描述APK加固的整體流程

加密流程:
  1. 首先將未加密的APK進(jìn)行解壓,獲取到Dex文件,然后對(duì)dex文件的每一個(gè)字節(jié)進(jìn)行加密(AES),加密完成生成新的dex文件(classes2.dex)
  2. 下面創(chuàng)建一個(gè)dex殼,通過(guò)對(duì)arr文件(android module 的打包文件)的解壓可以獲取到一個(gè)classes.jar文件,再通過(guò)cmd的命令,將jar轉(zhuǎn)成殼dex
  3. 將殼dex 和 加密的dex(源dex)一起打包成新的APK,然后再對(duì)APK進(jìn)行簽名;簽名后可以正常安裝,

1. 解壓APK,對(duì)Dex文件加密

  • 解壓apk
        File apkFile = new File("source/apk/app-debug.apk");

        // 解壓apk文件到unzip目錄
        File apkUnZipFile = new File("source/apk/unzip");
        Zip.unZip(apkFile,apkUnZipFile);
  • 將dex文件轉(zhuǎn)為內(nèi)存中的字節(jié)數(shù)組
        // 創(chuàng)建dexFile,將dexFile寫(xiě)入內(nèi)存—dexBytes
        File dexFile = new File("source/apk/unzip/classes.dex");
        RandomAccessFile inputStream = new RandomAccessFile(dexFile,"r");
        byte[] dexBytes = new byte[(int) inputStream.length()];
        inputStream.readFully(dexBytes);
        inputStream.close();
  • AES加密初始化
        //  AES加密操作初始化
        Cipher encoder = Cipher.getInstance("AES/ECB/PKCS5Padding");
        Cipher decoder = Cipher.getInstance("AES/ECB/PKCS5Padding");
        String key = "abcdefghijklmnop";
        byte[] keyBytes = key.getBytes();
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes,"AES");
        encoder.init(Cipher.ENCRYPT_MODE,secretKeySpec);
        decoder.init(Cipher.DECRYPT_MODE,secretKeySpec);
  • 字節(jié)數(shù)組加密
        //  對(duì)dex字節(jié)數(shù)組加密
        byte[] dexBytesEncrypted = encoder.doFinal(dexBytes);
  • 加密后的字節(jié)數(shù)組轉(zhuǎn)為dex文件
        //  將加密后的dex字節(jié)數(shù)組寫(xiě)入原來(lái)的文件,
        FileOutputStream fos = new FileOutputStream(dexFile);
        fos.write(dexBytesEncrypted);
        fos.close();

        //  將加密后的dex文件改名為classes1.dex(源)
        dexFile.renameTo(new File("source/apk/unzip/classes1.dex"));

2. 解壓aar,獲取殼

  • 解壓aar文件,獲取classes.jar
        // 將arr文件解壓
        File arrFile = new File("source/aar/mylibrary-debug.aar");
        Zip.unZip(arrFile,new File("source/aar/unzip"));
arr解壓
  • 通過(guò)cmd調(diào)用dx將jar轉(zhuǎn)為dex
        // 通過(guò)cmd 調(diào)用 dx 將jar轉(zhuǎn)為dex(殼)
        File jarFile = new File("source/aar/unzip/classes.jar");
        File desDexFile = new File("source/apk/unzip/classes.dex");
        Runtime runtime = Runtime.getRuntime();
        Process process = runtime.exec("cmd.exe /C dx --dex --output="
                + desDexFile.getAbsolutePath()
                + " "
                + jarFile.getAbsolutePath());
        process.waitFor(); // 等待子進(jìn)程完成
        process.destroy();

通過(guò)RunTime啟動(dòng)cmd命令,jvm會(huì)創(chuàng)建一個(gè)子進(jìn)程Process,waitFor()表示當(dāng)前進(jìn)程阻塞直到子進(jìn)程完成;

3. 打包新APK,通過(guò)cmd調(diào)用jarsigner重新簽名

  • 打包成新的apk文件
        // 殼 和 源 打包成新apk
        File unsignedApk = new File("source/result/unsigned.apk");
        Zip.zip(apkUnZipFile,unsignedApk);
  • 簽名
        // 簽名
        File signedApk = new File("source/result/signed.apk");
        Signature.signature(unsignedApk,signedApk);

public class Signature {
    public static void signature(File unsignedApk, File signedApk) throws InterruptedException, IOException {
        String cmd[] = {"cmd.exe", "/C ","jarsigner",  "-sigalg", "MD5withRSA",
                "-digestalg", "SHA1",
                "-keystore", "C:/Users/allen/.android/debug.keystore",
                "-storepass", "android",
                "-keypass", "android",
                "-signedjar", signedApk.getAbsolutePath(),
                unsignedApk.getAbsolutePath(),
                "androiddebugkey"};
        Process process = Runtime.getRuntime().exec(cmd);
        System.out.println("start sign");
//        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
//        String line;
//        while ((line = reader.readLine()) != null)
//            System.out.println("tasklist: " + line);
        try {
            int waitResult = process.waitFor();
            System.out.println("waitResult: " + waitResult);
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw e;
        }
        System.out.println("process.exitValue() " + process.exitValue() );
        if (process.exitValue() != 0) {
            InputStream inputStream = process.getErrorStream();
            int len;
            byte[] buffer = new byte[2048];
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            while((len=inputStream.read(buffer)) != -1){
                bos.write(buffer,0,len);
            }
            System.out.println(new String(bos.toByteArray(),"GBK"));
            throw new RuntimeException("簽名執(zhí)行失敗");
        }
        System.out.println("finish signed");
        process.destroy();
    }
}
APK加密過(guò)程

殼中是未加密的module代碼,可以直接運(yùn)行,并且負(fù)責(zé)源dex的解密工作

Dex解密(脫殼)

脫殼實(shí)現(xiàn):

脫殼解密過(guò)程一般是在殼Module的Application中進(jìn)行,參考Tinker的脫殼實(shí)現(xiàn):首先將apk進(jìn)行解壓獲取到加密的classes1.dex文件,然后通過(guò)流轉(zhuǎn)成Byte數(shù)組,再進(jìn)行AES解密,解密后重新寫(xiě)回到原來(lái)的classes1.dex;至此,解密過(guò)程完成,下面需要將解密后的dex文件運(yùn)行起來(lái),

在Application中重寫(xiě)attachBaseContext()
    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }

所有的脫殼,尋找dex中的class,classloader進(jìn)行類加載,都是在attachBaseContext()中完成的(即App運(yùn)行啟動(dòng)時(shí))

根據(jù)指定目錄獲取apk,解壓,尋找源dex,解密

        File apkFile = new File(getApplicationInfo().sourceDir);
        //data/data/包名/files/fake_apk/
        File unZipFile = getDir("fake_apk", MODE_PRIVATE);
        File app = new File(unZipFile, "app");  // 根據(jù)指定的目錄找到apk文件
        if (!app.exists()) {
            Zip.unZip(apkFile, app);    // 解壓apk
            File[] files = app.listFiles();
            for (File file : files) {
                String name = file.getName();
                if (name.equals("classes.dex")) {   //  過(guò)濾殼dex

                } else if (name.endsWith(".dex")) {   //  選擇源dex 解密
                    try {
                            byte[] bytes = getBytes(file);
                            FileOutputStream fos = new FileOutputStream(file);
                            byte[] decrypt = AES.decrypt(bytes);
//                        fos.write(bytes);
                            fos.write(decrypt);  //  將解密后的字節(jié)數(shù)組寫(xiě)回源文件(源dex文件)
                            fos.flush();
                            fos.close();
                        } catch (Exception e) {
                            e.printStackTrace();
                    }
                }
            }
        }
將所有的dex文件從apk取出,進(jìn)行類加載
        List list = new ArrayList<>();  // 解密后的dex文件
        Log.d("FAKE", Arrays.toString(app.listFiles()));
        for (File file : app.listFiles()) {
            if (file.getName().endsWith(".dex")) {
                list.add(file);
                System.gc();
            }
        }

對(duì)源dex中加密的類進(jìn)行類加載

ClassLoader原理
先看一下上文類加載的原理,在介紹加固脫殼的類加載思路:

  1. 通過(guò)反射獲取classLoaderpathList(DexPathList類)
  2. 再獲取pathListDexElements(element[])
  3. 傳入源dex,通過(guò)反射調(diào)用DexPathList類makeDexElements創(chuàng)建新的Element[],
  4. 合并兩個(gè)數(shù)組
  5. 反射將新的element[] setclassLoader
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容