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是能夠看獲得裏面的內容的。