Android dex解密與解密原理及其代碼實現

Android dex解密與解密原理及其代碼實現

       爲何要進行apk加密?答案是避免apk被有心人反編譯,竊取公司重要技術和算法等。可是要給Apk加密要如何實現呢?系統在加載類的時候都是從咱們apk的dex文件中加載的。ClassLoader會去維護一個這樣的dex文件數組。而咱們要作的就是將原apk中的dex都加密,而後將解密部分的代碼單獨編程成dex文件(咱們稱這樣的dex爲殼dex)連帶着加密的dex一塊兒加到新apk中。這樣新apk安裝後系統就可以找到咱們應用啓動的入口Application了,不至於因爲加密緻使系統找不到應用程序入口。而在這個程序入口中咱們要作的就是解密被加密的dex文件,而後從新插入到ClassLoader維護的dex文件數組中(這裏就涉及到大量的反射知識)。java

       dex解密與解密分爲如下幾個步驟:android

       1)把源 apk(要加固的apk)中的 dex 文件加密。加密以後就再也不是正常的 dex 文件,那麼市面上的反編譯工具就不能按照正常的dex去解析了。算法

       2)將加密後的dex文件與殼程序Apk的dex文件合併成新的dex文件,而後替換殼程序中的源dex文件,生成新的apk文件。編程

       3)在殼程序apk的application中進行解密源apk的dex文件。windows

       知道了原理,下面就是代碼實現了。這裏有三個工程:數組

       1)原程序項目 app module(須要加密的APK)。緩存

       2)殼項目 ApkShuck module(解密源程序APK和加載APK)。app

       3)對原APK進行加密和殼項目的DEX的合併項目 Encrypt module。ide

    1. 加密實現

       在Android studio 中建立一個java library module(Encrypt),此module主要用來進行dex的加密工做。工具

       1. 在開始加密以前首先創建臨時目錄,在 Encrypt 下創建一個source目錄,而後在 source 目錄創建 apk 目錄和 arr 目錄,最後分別在 apk 和 arr 目錄下創建一個 temp 目錄。apk 目錄下放置原apk文件,它的 temp 目錄主要用來放置原apk解壓以後的文件和原apk中dex加密後的文件。arr目錄放置殼apk(實際上是一個arr包),它的 temp 目錄主要用來放置殼apk解壓以後的文件。

       2. 清理臨時目錄的緩存文件,代碼以下所示:

public class ApkEncryptMain {
  public static void main(String[] args) {
        init();
 }
    /**
     * 初始化
     */
    private static void init() {
        // 刪除緩存
        FileUtils.delFolder(new File("Encrypt/source/apk/temp"));
        FileUtils.delFolder(new File("Encrypt/source/arr/temp"));
    }
}

       3. 加密算法實現,主要採用的是AES加密算法,代碼以下:

public class EncryptUtils {  
    private final byte[] KEY = "QUmkLrrISiud6RPU".getBytes(); // 加密使用的key
    private final byte[] IV = "eh7aJlOdHCNsGNcD".getBytes(); // 偏移值
    private final String ALGORITHM = "AES/CBC/PKCS5Padding"; // 加密算法
    private Cipher encryptCipher; // 加密

    /**
     * 使用單例
     */
    private EncryptUtils() {
        try {
            // 初始化加密算法
            encryptCipher = Cipher.getInstance(ALGORITHM);
            SecretKeySpec key = new SecretKeySpec(KEY, "AES");
            encryptCipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(IV));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static class SingletonHolder {
        private static final EncryptUtils INSTANCE = new EncryptUtils();
    }

    public static EncryptUtils getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

       4. 解壓原apk,並加密原apk中的dex文件。

public class ApkEncryptMain {
    private static final String SOURCE_APK_PATH = "Encrypt/source/apk/app-debug.apk";

    public static void main(String[] args) {
        LogUtils.i("start encrypt");
        init();
        /**
         * 1. 解壓源apk文件到 ../source/apk/temp目錄下,並加密dex文件
         */
        File sourceApk = new File(SOURCE_APK_PATH);
        File newApkDir = new File(sourceApk.getParent() + File.separator + "temp");
        if (!newApkDir.exists()) {
            newApkDir.mkdirs();
        }
        // 解壓Apk並加密dex文件
        EncryptUtils.getInstance().encryptApkFile(sourceApk, newApkDir);
    }
}

public class EncryptUtils {
   /**
     * 加密apk
     *
     * @param srcApkFile 源apk文件的地址
     * @param dstApkFile 新apk文件的地址
     */
    public void encryptApkFile(File srcApkFile, File dstApkFile) {
        if (srcApkFile == null || !srcApkFile.exists()) {
            LogUtils.e("srcAPKFile not exist");
            return;
        }
        // 解壓apk到指定文件夾
        ZipUtils.unZip(srcApkFile, dstApkFile);

        // 獲取全部的dex(可能存在分包的狀況,即有多個dex文件)
        File[] dexFiles = dstApkFile.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File file, String s) {
                // 提取全部的.dex文件
                return s.endsWith(".dex");
            }
        });

       if (dexFiles == null || dexFiles.length <= 0) {
            LogUtils.i("this apk is invalidate");
            return;
        }

        for (File dexFile : dexFiles) {
            // 讀取dex中的數據
            byte[] buffer = FileUtils.getBytes(dexFile);
            if (buffer != null) {
                // 加密
                byte[] encryptBytes = encrypt(buffer);
                if (encryptBytes != null) {
                    //修改.dex名爲_.dex,避免等會與aar中的.dex重名
                    int indexOf = dexFile.getName().indexOf(".dex");
                    String newName = dexFile.getParent() + File.separator +
                            dexFile.getName().substring(0, indexOf) + "_.dex";
                    // 寫數據, 替換原來的數據
                    FileUtils.wirte(new File(newName), encryptBytes);
                    dexFile.delete();
                } else {
                    LogUtils.e("Failed to encrypt dex data");
                    return;
                }
            } else {
                LogUtils.e("Failed to read dex data");
                return;
            }
        }
    }

    /**
     * 加密
     * @param data
     * @return
     */
    private byte[] encrypt(byte[] data) {
        try {
            return encryptCipher.doFinal(data);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

public class ZipUtils {
    /**
     * 解壓zip文件
     *
     * @param srcFile 須要解壓的zip文件
     * @param dstFile 解壓後的文件
     */
    public static void unZip(File srcFile, File dstFile) {
        if (srcFile == null) {
            LogUtils.e("unZip: srcFile is null");
            return;
        }
        try {
            ZipFile zipFile = new ZipFile(srcFile);
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            while (entries.hasMoreElements()){
                ZipEntry zipEntry = entries.nextElement();
                String name = zipEntry.getName();
                if (name.equals("META-INF/CERT.RSA") || name.equals("META-INF/CERT.SF") || name
                        .equals("META-INF/MANIFEST.MF")) {
                    continue;
                }
                if(!zipEntry.isDirectory()){
                    File file = new File(dstFile, name);
                    if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
                    FileOutputStream fos = new FileOutputStream(file);
                    InputStream is = zipFile.getInputStream(zipEntry);
                    byte[] buffer = new byte[1024];
                    int len;
                    while ((len = is.read(buffer)) != -1) {
                        fos.write(buffer, 0, len);
                    }
                    is.close();
                    fos.close();
                }
            }
            zipFile.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

       5. 解壓arr文件,並生成殼dex。

public class ApkEncryptMain {
    public static void main(String[] args) {

        /**
         * 2. 解壓arr文件(不能進行加密的部分),將其中的dex文件拷貝到apk/temp目錄中。
         */
        File shuckApk = new File(SHUCK_APK_PATH);
        File newShuckDir = new File(shuckApk.getParent() + File.separator + "temp");
        if (!newShuckDir.exists()) {
            newShuckDir.mkdirs();
        }
        // 解壓arr文件,並將arr中的jar文件轉化爲dex文件
        DxUtils.jar2Dex(shuckApk, newShuckDir);
        // 拷貝arr中的classes.dex 到 apk/temp 目錄中
        File copyDstFile = new File("Encrypt/source/apk/temp/classes.dex");
        FileUtils.copyFile(dstDex, copyDstFile);
    }
}

public class DxUtils {
    /**
     * 解壓arr並將jar轉化爲 dex
     *
     * @param srcFile
     * @param dstFile
     */
    public static void jar2Dex(File srcFile, File dstFile) {
        if (srcFile == null || !srcFile.exists()) {
            LogUtils.e("shuck arr file not exist");
            return;
        }
        // 解壓apk到指定文件夾
        ZipUtils.unZip(srcFile, dstFile);

        // 獲取全部的jar
        File[] jarFiles = dstFile.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File file, String s) {
                // 提取全部的.dex文件
                return s.endsWith(".jar");
            }
        });

        if (jarFiles == null || jarFiles.length <= 0) {
            LogUtils.i("this arr is invalidate");
            return;
        }
        // 通常狀況下這個殼arr中只會有一個classes.jar文件,這裏classes_jar就是classes.jar文件
        File classes_jar = jarFiles[0];
        // 將classes_jar 轉爲爲 classes.dex
        File dstDex = new File(classes_jar.getParent() + File.separator + "classes.dex");
        // 使用 android tools 裏面的dx.bat 命令將 jar 轉化爲 dex
        dxCommand(classes_jar, dstDex);
    }


    private static void dxCommand(File jarFile, File dexFile) {
        Runtime runtime = Runtime.getRuntime();
        // 這裏使用的是dx.bat的絕對路徑。
        String command = "cmd.exe /C E:\\development_tools\\android_sdk\\android_sdk\\build-tools\\29.0.2\\dx --dex --output=" + dexFile.getAbsolutePath()
                + " " + jarFile.getAbsolutePath();
        Process process = null;
        BufferedReader buffer = null;
        try {
            process = runtime.exec(command);
            process.waitFor();
            String line;
            if (process.exitValue() != 0) {
                buffer = new BufferedReader(new InputStreamReader(process.getErrorStream()));
                while ((line = buffer.readLine()) != null) {
                    LogUtils.e(line);
                }
            } else {
                buffer = new BufferedReader(new InputStreamReader(process.getInputStream()));
                while ((line = buffer.readLine()) != null) {
                    LogUtils.i(line);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (process != null) {
                process.destroy();
            }
            if(buffer!=null){
                try {
                    buffer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

       7. 打包apk/temp目錄生成新的未簽名的apk文件

                /**
                 *    特別注意!!!
                 *    這裏必定要用"/" 千萬不要用File.separator
                 *    由於這裏是java 工程,它運行在 windows環境,在Windows環境下 File.separator 獲取的是 "\"
                 *    而在 Android 系統中 File.separator 獲取的是 "/"
                 *    所以在這裏若是使用 File.separator 時獲取的 "\" 在 android中識別不了,將會致使程序運行不起來。
                 *
                 */






public class ApkEncryptMain {
    private static final String SOURCE_APK_PATH = "Encrypt/source/apk/app-debug.apk";
    private static final String SHUCK_APK_PATH = "Encrypt/source/arr/ApkShuck-release.aar";

    public static void main(String[] args) {
        /**
         * 3. 打包apk/temp目錄生成新的未簽名的apk文件
         */
        File unsignedApk = new File("Encrypt/result/apk-unsigned.apk");
        unsignedApk.getParentFile().mkdirs();
        ZipUtils.zip(newApkDir, unsignedApk);
    }
}

public class ZipUtils {
  
    /**
     * 壓縮
     * @param sourceFile
     * @param zipFile
     */
    public static void zip(File sourceFile, File zipFile) {
        if (sourceFile == null) {
            LogUtils.e("The original file that needs to be compressed does not exist");
            return;
        }
        zipFile.delete();
        // 對輸出文件作CRC32校驗
        ZipOutputStream zos = null;
        try {
            zos = new ZipOutputStream(new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32()));
            compress(sourceFile, zos, "");
            zos.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (zos != null) {
                try {
                    zos.close();
                    zos = null;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

   private static void compress(File srcFile, ZipOutputStream zos, String dir) throws IOException {
        if (srcFile.isDirectory()) {
            File[] files = srcFile.listFiles();
            for (File file : files) {
                /**
                 *    特別注意!!!
                 *    這裏必定要用"/" 千萬不要用File.separator
                 *    由於這裏是java 工程,它運行在 windows環境,在Windows環境下 File.separator 獲取的是 "\"
                 *    而在 Android 系統中 File.separator 獲取的是 "/"
                 *    所以在這裏若是使用 File.separator 時獲取的 "\" 在 android中識別不了,將會致使程序運行不起來。
                 *
                 */
                compress(file, zos, dir + srcFile.getName() + "/");
            }
        } else {
            compressFile(srcFile, zos, dir);
        }
    }


    private static void compressFile(File file, ZipOutputStream zos, String dir) throws IOException {
        // temp/classes.dex
        String fullName = dir + file.getName();
        // 須要去掉temp
        String[] dirNames = fullName.split("/");
        // 正確的文件目錄名(去掉了temp)
        StringBuffer sb = new StringBuffer();
        if (dirNames.length > 1) {
            for (int i = 1; i < dirNames.length; i++) {
                sb.append("/");
                sb.append(dirNames[i]);
            }
        } else {
            sb.append("/");
        }
        ZipEntry entry = new ZipEntry(sb.toString().substring(1));
        zos.putNextEntry(entry);
        FileInputStream fis = new FileInputStream(file);
        int count;
        byte[] bytes = new byte[1024];
        while ((count = fis.read(bytes, 0, 1024)) != -1) {
            zos.write(bytes, 0, count);
        }
        fis.close();
        zos.closeEntry();
    }
}

       7. 給新的未簽名的apk簽名

public class ApkEncryptMain {
    public static void main(String[] args) {
        /**
         * 4 .給新apk添加簽名,生成簽名apk
         */
        File signedApk = new File("Encrypt/result/apk-signed.apk");
        SignUtils.signature(unsignedApk, signedApk);
    }
}

public class SignUtils {
    public static void signature(File unSignApk, File signApk) {
        if (unSignApk == null || !unSignApk.exists()) {
            LogUtils.e("The APK that needs to be signed does not exist");
            return;
        }
        String command = "cmd.exe /C jarsigner -sigalg SHA1withRSA -digestalg SHA1 " +
                "-keystore C:/Users/Administrator/.android/debug.keystore -storepass android -keypass android " +
                "-signedjar " + signApk.getAbsolutePath() + " " + unSignApk.getAbsolutePath() + " androiddebugkey";
        Process process = null;
        BufferedReader buffer = null;
        try {
            LogUtils.i(command);
            process = Runtime.getRuntime().exec(command);
            process.waitFor();
            String line;
            if (process.exitValue() != 0) {
                LogUtils.i("sign fail");
                buffer = new BufferedReader(new InputStreamReader(process.getErrorStream()));
                while ((line = buffer.readLine()) != null) {
                    LogUtils.e(line);
                }
            } else {
                LogUtils.i("sign success");
                buffer = new BufferedReader(new InputStreamReader(process.getInputStream()));
                while ((line = buffer.readLine()) != null) {
                    LogUtils.i(line);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (process != null) {
                process.destroy();
                process = null;
            }

            if (buffer != null) {
                try {
                    buffer.close();
                    buffer = null;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

       到此dex加密過程就完成了,生成的apk-signed.apk就是簽名了的apk,能夠直接安裝使用。

    2. 解密實現

       要完成解密,咱們須要完成以下幾個步驟:

       1)找到合適的解密時機。

       2)殼 dex 並無被加密,須要排除在解密的 dex 文件以外。

       3)解密後的 dex 文件須要從新插入到 ClassLoader 中,這與熱修復思想是同樣的。

       1. 解密時機

      做爲一個被加密的應用,安裝的時候咱們應用自己是沒法控制。因此應用第一次啓動的時候就成了咱們最佳的解密時機了。 因此咱們將解密的邏輯放到Application的attachBaseContext()方法中。

       2. 解壓apk、脫殼並解密被加密的原apk中的 dex

       說明:  這裏只是爲了實現功能而將解密的代碼用java實現了,而這部分代碼沒有被加密,因此仍是很容易被反編譯查看到解密方法,這樣被加密的dex也很容易被破解,所以最好的方案就是把加密和解密代碼用JNI方式實現,這樣即便人家反編譯殼apk的dex文件也沒有辦法知道加密和解密方法,也就沒法破解原apk的dex文件了。

/**
     * 解壓apk並解密被加密了的dex文件
     *
     * @param apkFile 被加密了的 apk 文件
     * @param app     存放解壓和解密後的apk文件目錄
     */
    private void unZipAndDecryptDex(File apkFile, File app) {
        if (!app.exists() || app.listFiles().length == 0) {
            // 當app文件不存在,或者 app 文件是一個空文件夾是須要解壓。

            // 解壓apk到指定目錄
            ZipUtils.unZip(apkFile, app);
            // 獲取全部的dex
            File[] dexFiles = app.listFiles(new FilenameFilter() {
                @Override
                public boolean accept(File file, String s) {
                    // 提取全部的.dex文件
                    return s.endsWith(".dex");
                }
            });

            if (dexFiles == null || dexFiles.length <= 0) {
                LogUtils.i("this apk is invalidate");
                return;
            }

            for (File file : dexFiles) {
                if (file.getName().equals("classes.dex")) {
                    /**
                     * 咱們在加密的時候將不能加密的殼dex命名爲classes.dex並拷貝到新apk中打包生成新的apk中了。
                     * 因此這裏咱們作脫殼,殼dex不須要進行解密操做。
                     */
                } else {
                    /**
                     * 加密的dex進行解密,對應加密流程中的_.dex文件
                     */
                    byte[] buffer = FileUtils.getBytes(file);
                    if (buffer != null) {
                        // 解密
                        byte[] decryptBytes = EncryptUtils.getInstance().decrypt(buffer);
                        if (decryptBytes != null) {
                            //修改.dex名爲_.dex,避免等會與aar中的.dex重名
                            int indexOf = file.getName().indexOf(".dex");
                            String newName = file.getParent() + File.separator +
                                    file.getName().substring(0, indexOf) + "new.dex";
                            // 寫數據, 替換原來的數據
                            FileUtils.wirte(new File(newName), decryptBytes);
                            file.delete();
                        } else {
                            LogUtils.e("Failed to encrypt dex data");
                            return;
                        }
                    } else {
                        LogUtils.e("Failed to read dex data");
                        return;
                    }
                }
            }
        }
    }

       3. 將解密後的dex文件從新插入dexElements數組中。在這個過程當中須要對不一樣的版本作處理。這裏提供一個能夠在線查看源碼的地址,方便你們閱讀源碼。http://androidxref.com/

public class LoaderDexUtils {
    public static void loader(ClassLoader loader, ArrayList<File> dexList, File dir) {

        try {
            /**
             * 1. 經過反射找到BaseDexClassLoader中的pathList屬性,pathList是DexPathList類型的對象。
             * DexPathList中維護了一個dex文件數組(dexElements數組),ClassLoader加載類的時候就會從這dex數組中去查找。
             * 咱們須要將解密出來的dex從新插入到這個數組裏面。
             */
            // 這裏的loader是PathClassLoader,PathClassLoader繼承自BaseDexClassLoader
            Class<?> baseDexClassLoaderClass = loader.getClass().getSuperclass();
            Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
            pathListField.setAccessible(true);
            Object pathList = pathListField.get(loader);


            /**
             * 2. 建立咱們本身的dex文件數組,可查看源碼中的makeDexElements方法
             */
            ArrayList suppressedExceptions = new ArrayList();
            Class<?> dexPathListClass = pathList.getClass();
            Object[] elements = null;
            if (Build.VERSION.SDK_INT >= 24) {
                Method makeDexElementsMethod = dexPathListClass.getDeclaredMethod("makeDexElements", List.class, File.class, List.class, ClassLoader.class);
                makeDexElementsMethod.setAccessible(true);
                elements = (Object[]) makeDexElementsMethod.invoke(pathList, dexList, dir, suppressedExceptions, loader);
            } else if (Build.VERSION.SDK_INT >= 23) {
                Method makeDexElementsMethod = dexPathListClass.getDeclaredMethod("makePathElements", List.class, File.class, List.class);
                makeDexElementsMethod.setAccessible(true);
                elements = (Object[]) makeDexElementsMethod.invoke(pathList, dexList, dir, suppressedExceptions);
            } else {
                Method makeDexElementsMethod = dexPathListClass.getDeclaredMethod("makeDexElements", ArrayList.class, File.class, ArrayList.class);
                makeDexElementsMethod.setAccessible(true);
                elements = (Object[]) makeDexElementsMethod.invoke(pathList, dexList, dir, suppressedExceptions);
            }

            if (elements == null) {
                LogUtils.e("makeDexElements fail");
                return;
            }
            /**
             * 3. 將解密後的dex文件插入到DexPathList的dexElements數組中。
             */
            Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            Object[] oldDexElements = (Object[]) dexElementsField.get(pathList);
            Object[] newDexElements = (Object[]) (Array.newInstance(oldDexElements.getClass()
                    .getComponentType(), oldDexElements.length + elements.length));
            System.arraycopy(oldDexElements, 0, newDexElements, 0, oldDexElements.length);
            System.arraycopy(elements, 0, newDexElements, oldDexElements.length, elements.length);
            dexElementsField.set(pathList, newDexElements);

            // 異常處理
            if (suppressedExceptions.size() > 0) {
                Iterator iterator = suppressedExceptions.iterator();

                while (iterator.hasNext()) {
                    IOException dexElementsSuppressedExceptions = (IOException)
                            iterator.next();
                    Log.w("MultiDex", "Exception in makeDexElement",
                            dexElementsSuppressedExceptions);
                }

                Field suppressedExceptionsField = dexPathListClass.getDeclaredField("dexElementsSuppressedExceptions");
                suppressedExceptionsField.setAccessible(true);
                IOException[] dexElementsSuppressedExceptions = (IOException[])
                        suppressedExceptionsField.get(pathList);
                if (dexElementsSuppressedExceptions == null) {
                    dexElementsSuppressedExceptions = (IOException[]) suppressedExceptions
                            .toArray(new IOException[suppressedExceptions.size()]);
                } else {
                    IOException[] combined = new IOException[suppressedExceptions.size() +
                            dexElementsSuppressedExceptions.length];
                    suppressedExceptions.toArray(combined);
                    System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
                            suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
                    dexElementsSuppressedExceptions = combined;
                }

                suppressedExceptionsField.set(pathList, dexElementsSuppressedExceptions);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

       3. 測試

       從上面的測試過程能夠看出,加密dex以後再解密dex能夠正常運行,而後加密事後的dex是看不到內容的,而沒有加密的dex是能夠看獲得裏面的內容的。 

代碼下載

相關文章
相關標籤/搜索