【Android 安全】DEX 加密 ( 阶段总结 | 主应用 | 代理 Application | Java 工具 | 代码示例 ) ★
生活随笔
收集整理的這篇文章主要介紹了
【Android 安全】DEX 加密 ( 阶段总结 | 主应用 | 代理 Application | Java 工具 | 代码示例 ) ★
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
文章目錄
- 一、主應(yīng)用
- 二、代理 Application 解析
- 1、代理 Application 源碼
- 2、反射對象成員以及方法的工具類
- 3、壓縮解壓縮工具類
- 4、OpenSSL 解密工具類
- 5、OpenSSL 解密相關(guān) NDK 源碼
- 6、CmakeLists.txt 構(gòu)建腳本
- 7、NDK 日志頭文件
- 8、build.gradle 構(gòu)建腳本
- 三、Java 工具
- 1、主函數(shù)
- 2、加密相關(guān)工具類
相關(guān)資源 :
- 本階段源碼下載 : https://download.csdn.net/download/han1202012/13214384 ( 快照 )
- GitHub 地址 : https://github.com/han1202012/DexEncryption ( 完整代碼 )
一、主應(yīng)用
在主應(yīng)用中 , 進行兩個操作 :
- 操作一 : 配置 AndroidManifest.xml 中的 代理 Application ;
- 操作二 : 配置 真實 Application 全類名 , 以及 版本號 ;
配置 NDK 的 CPU 架構(gòu) : 只配置 armeabi-v7a 架構(gòu)即可 ;
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions'android {compileSdkVersion 29buildToolsVersion "30.0.2"defaultConfig {applicationId "kim.hsl.dex"minSdkVersion 18targetSdkVersion 29versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"externalNativeBuild{cmake{// 配置要編譯動態(tài)庫的 CPU 架構(gòu), 這里編譯 armeabi-v7a 版本的動態(tài)庫// arm64-v8a, armeabi-v7a, x86, x86_64abiFilters 'armeabi-v7a'}}}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}externalNativeBuild {cmake {path "src/main/cpp/CMakeLists.txt"version "3.10.2"}} }dependencies {implementation fileTree(dir: 'libs', include: ['*.jar'])implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"implementation 'androidx.appcompat:appcompat:1.2.0'implementation 'androidx.core:core-ktx:1.3.2'implementation 'androidx.constraintlayout:constraintlayout:2.0.4'testImplementation 'junit:junit:4.12'androidTestImplementation 'androidx.test.ext:junit:1.1.2'androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'implementation project(':multiple-dex-core') }二、代理 Application 解析
代理 Application 操作步驟 :
- 1 . 獲取 APK 文件 : 獲取本應(yīng)用的 APK 文件 ;
- 2 . 獲取相關(guān)元數(shù)據(jù) : 獲取在主應(yīng)用 AndroidManifest.xml 中配置的 真實 Application 全類名 , 以及版本號信息 ;
- 3 . 創(chuàng)建工作目錄 : 創(chuàng)建用戶私有目錄 , 將 APK 文件解壓到該目錄中 ;
- 4 . 解密 dex 文件 : 遍歷被解壓的目錄 , 發(fā)現(xiàn)被加密的 dex 文件后 , 將該 dex 文件解密為可以直接使用的 dex 文件 ;
- 5 . 獲取 DexPathList 對象 : 反射獲取 BaseDexClassLoader 中的 DexPathList 成員 ;
- 6 . 獲取 Element[] dexElements 數(shù)組 : 反射獲取 DexPathList 中的 Element[] dexElements 數(shù)組成員 ;
- 7 . 獲取創(chuàng)建 Element[] dexElements 數(shù)組方法 : 6.06.06.0 以下系統(tǒng)獲取 makeDexElements 方法 , 7.07.07.0 以上系統(tǒng)獲取 makePathElements 方法 ;
- 8 . 創(chuàng)建 Element[] dexElements 數(shù)組 : 調(diào)用上述反射的方法創(chuàng)建 Element[] dexElements 數(shù)組 ;
- 9 . 合并并設(shè)置 Element[] dexElements 數(shù)組 : 將上述創(chuàng)建的 Element[] dexElements 數(shù)組 與 原本的 Element[] dexElements 數(shù)組 合并 , 設(shè)置給 DexPathList 中的 Element[] dexElements 數(shù)組成員 ;
1、代理 Application 源碼
package kim.hsl.multipledex;import android.app.Application; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.text.TextUtils; import android.util.Log;import java.io.File; import java.io.IOException; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List;public class ProxyApplication extends Application {public static final String TAG = "ProxyApplication";/*** 應(yīng)用真實的 Application 全類名*/String app_name;/*** DEX 解密之后的目錄名稱*/String app_version;/*** 在 Application 在 ActivityThread 中被創(chuàng)建之后,* 第一個調(diào)用的方法是 attachBaseContext 函數(shù).* 該函數(shù)是 Application 中最先執(zhí)行的函數(shù).*/@Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base);try {Log.i(TAG, "attachBaseContext");/*在該 Application 中主要進行兩個操作 :1 . 解密并加載多個 DEX 文件2 . 將真實的 Application 替換成應(yīng)用的主 Application*//*I . 解密與加載多 DEX 文件先進行解密, 然后再加載解密之后的 DEX 文件1. 先獲取當(dāng)前的 APK 文件2. 然后解壓該 APK 文件*/// 獲取當(dāng)前的 APK 文件, 下面的 getApplicationInfo().sourceDir 就是本應(yīng)用 APK 安裝文件的全路徑File apkFile = new File(getApplicationInfo().sourceDir);// 獲取在 app Module 下的 AndroidManifest.xml 中配置的元數(shù)據(jù),// 應(yīng)用真實的 Application 全類名// 解密后的 dex 文件存放目錄ApplicationInfo applicationInfo = null;applicationInfo = getPackageManager().getApplicationInfo(getPackageName(),PackageManager.GET_META_DATA);Bundle metaData = applicationInfo.metaData;if (metaData != null) {// 檢查是否存在 app_name 元數(shù)據(jù)if (metaData.containsKey("app_name")) {app_name = metaData.getString("app_name").toString();}// 檢查是否存在 app_version 元數(shù)據(jù)if (metaData.containsKey("app_version")) {app_version = metaData.getString("app_version").toString();}}// 創(chuàng)建用戶的私有目錄 , 將 apk 文件解壓到該目錄中File privateDir = getDir(app_name + "_" + app_version, MODE_PRIVATE);Log.i(TAG, "attachBaseContext 創(chuàng)建用戶的私有目錄 : " + privateDir.getAbsolutePath());// 在上述目錄下創(chuàng)建 app 目錄// 創(chuàng)建該目錄的目的是存放解壓后的 apk 文件的File appDir = new File(privateDir, "app");// app 中存放的是解壓后的所有的 apk 文件// app 下創(chuàng)建 dexDir 目錄 , 將所有的 dex 目錄移動到該 dexDir 目錄中// dexDir 目錄存放應(yīng)用的所有 dex 文件// 這些 dex 文件都需要進行解密File dexDir = new File(appDir, "dexDir");// 遍歷解壓后的 apk 文件 , 將需要加載的 dex 放入如下集合中ArrayList<File> dexFiles = new ArrayList<File>();// 如果該 dexDir 不存在 , 或者該目錄為空 , 并進行 MD5 文件校驗if (!dexDir.exists() || dexDir.list().length == 0) {// 將 apk 中的文件解壓到了 appDir 目錄ZipUtils.unZipApk(apkFile, appDir);// 獲取 appDir 目錄下的所有文件File[] files = appDir.listFiles();Log.i(TAG, "attachBaseContext appDir 目錄路徑 : " + appDir.getAbsolutePath());Log.i(TAG, "attachBaseContext appDir 目錄內(nèi)容 : " + files);// 遍歷文件名稱集合for (int i = 0; i < files.length; i++) {File file = files[i];Log.i(TAG, "attachBaseContext 遍歷 " + i + " . " + file);// 如果文件后綴是 .dex , 并且不是 主 dex 文件 classes.dex// 符合上述兩個條件的 dex 文件放入到 dexDir 中if (file.getName().endsWith(".dex") &&!TextUtils.equals(file.getName(), "classes.dex")) {// 篩選出來的 dex 文件都是需要解密的// 解密需要使用 OpenSSL 進行解密// 獲取該文件的二進制 Byte 數(shù)據(jù)// 這些 Byte 數(shù)組就是加密后的 dex 數(shù)據(jù)byte[] bytes = OpenSSL.getBytes(file);// 解密該二進制數(shù)據(jù), 并替換原來的加密 dex, 直接覆蓋原來的文件即可OpenSSL.decrypt(bytes, file.getAbsolutePath());// 將解密完畢的 dex 文件放在需要加載的 dex 集合中dexFiles.add(file);// 拷貝到 dexDir 中Log.i(TAG, "attachBaseContext 解密完成 被解密文件是 : " + file);}// 判定是否是需要解密的 dex 文件}// 遍歷 apk 解壓后的文件} else {// 已經(jīng)解密完成, 此時不需要解密, 直接獲取 dexDir 中的文件即可for (File file : dexDir.listFiles()) {dexFiles.add(file);}}Log.i(TAG, "attachBaseContext 解密完成 dexFiles : " + dexFiles);for(int i = 0; i < dexFiles.size(); i ++){Log.i(TAG, i + " . " + dexFiles.get(i).getAbsolutePath());}// 截止到此處 , 已經(jīng)拿到了解密完畢 , 需要加載的 dex 文件// 加載自己解密的 dex 文件loadDex(dexFiles, privateDir);Log.i(TAG, "attachBaseContext 完成");} catch (PackageManager.NameNotFoundException e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}}/*** 加載 dex 文件集合* 這些 dex 文件已經(jīng)解密* 參考博客 : https://hanshuliang.blog.csdn.net/article/details/109608605* <p>* 創(chuàng)建自己的 Element[] dexElements 數(shù)組* ( libcore/dalvik/src/main/java/dalvik/system/DexPathList.java )* 然后將 系統(tǒng)加載的 Element[] dexElements 數(shù)組 與 我們自己的 Element[] dexElements 數(shù)組進行合并操作*/void loadDex(ArrayList<File> dexFiles, File optimizedDirectory)throwsIllegalAccessException,InvocationTargetException,NoSuchFieldException,NoSuchMethodException {Log.i(TAG, "loadDex");/*需要執(zhí)行的步驟1 . 獲得系統(tǒng) DexPathList 中的 Element[] dexElements 數(shù)組( libcore/dalvik/src/main/java/dalvik/system/DexPathList.java )2 . 在本應(yīng)用中創(chuàng)建 Element[] dexElements 數(shù)組 , 用于存放解密后的 dex 文件3 . 將 系統(tǒng)加載的 Element[] dexElements 數(shù)組與 我們自己的 Element[] dexElements 數(shù)組進行合并操作4 . 替換 ClassLoader 加載過程中的 Element[] dexElements 數(shù)組 ( 封裝在 DexPathList 中 )*//*1 . 獲得系統(tǒng) DexPathList 中的 Element[] dexElements 數(shù)組第一階段 : 在 Context 中調(diào)用 getClassLoader() 方法 , 可以拿到 PathClassLoader ;第二階段 : 從 PathClassLoader 父類 BaseDexClassLoader 中找到 DexPathList ;第三階段 : 獲取封裝在 DexPathList 類中的 Element[] dexElements 數(shù)組 ;上述的 DexPathList 對象 是 BaseDexClassLoader 的私有成員Element[] dexElements 數(shù)組 也是 DexPathList 的私有成員因此只能使用反射獲取 Element[] dexElements 數(shù)組*/// 階段一二 : 調(diào)用 getClassLoader() 方法可以獲取 PathClassLoader 對象// 從 PathClassLoader 對象中獲取 private final DexPathList pathList 成員Field pathListField = ReflexUtils.reflexField(getClassLoader(), "pathList");// 獲取 classLoader 對象對應(yīng)的 DexPathList pathList 成員Object pathList = pathListField.get(getClassLoader());//階段三 : 獲取封裝在 DexPathList 類中的 Element[] dexElements 數(shù)組Field dexElementsField = ReflexUtils.reflexField(pathList, "dexElements");// 獲取 pathList 對象對應(yīng)的 Element[] dexElements 數(shù)組成員Object[] dexElements = (Object[]) dexElementsField.get(pathList);/*2 . 在本應(yīng)用中創(chuàng)建 Element[] dexElements 數(shù)組 , 用于存放解密后的 dex 文件不同的 Android 版本中 , 創(chuàng)建 Element[] dexElements 數(shù)組的方法不同 , 這里需要做兼容*/Method makeDexElements;Object[] addElements = null;if (Build.VERSION.SDK_INT <=Build.VERSION_CODES.M) { // 5.0, 5.1 makeDexElements// 反射 5.0, 5.1, 6.0 版本的 DexPathList 中的 makeDexElements 方法makeDexElements = ReflexUtils.reflexMethod(pathList, "makeDexElements",ArrayList.class, File.class, ArrayList.class);ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles,optimizedDirectory,suppressedExceptions);} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // 7.0 以上版本 makePathElements// 反射 7.0 以上版本的 DexPathList 中的 makeDexElements 方法makeDexElements = ReflexUtils.reflexMethod(pathList, "makePathElements",List.class, File.class, List.class);ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles,optimizedDirectory,suppressedExceptions);}/*3 . 將 系統(tǒng)加載的 Element[] dexElements 數(shù)組與 我們自己的 Element[] dexElements 數(shù)組進行合并操作首先創(chuàng)建數(shù)組 , 數(shù)組類型與 dexElements 數(shù)組類型相同將 dexElements 數(shù)組中的元素拷貝到 newElements 前半部分, 拷貝元素個數(shù)是 dexElements.size將 addElements 數(shù)組中的元素拷貝到 newElements 后半部分, 拷貝元素個數(shù)是 dexElements.size*/Object[] newElements = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(),dexElements.length + addElements.length);// 將 dexElements 數(shù)組中的元素拷貝到 newElements 前半部分, 拷貝元素個數(shù)是 dexElements.sizeSystem.arraycopy(dexElements, 0, newElements, 0, dexElements.length);// 將 addElements 數(shù)組中的元素拷貝到 newElements 后半部分, 拷貝元素個數(shù)是 dexElements.sizeSystem.arraycopy(addElements, 0, newElements, dexElements.length, addElements.length);/*4 . 替換 ClassLoader 加載過程中的 Element[] dexElements 數(shù)組 ( 封裝在 DexPathList 中 )*/dexElementsField.set(pathList, newElements);Log.i(TAG, "loadDex 完成");} }
2、反射對象成員以及方法的工具類
反射對象成員以及方法的工具類 :
package kim.hsl.multipledex;import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Arrays;public class ReflexUtils {/*** 通過反射方法獲取 instance 類中的 memberName 名稱的成員* @param instance 成員所在對象* @param memberName 成員變量名稱* @return 返回 Field 類型成員* @throws NoSuchFieldException*/public static Field reflexField(Object instance, String memberName) throws NoSuchFieldException {// 獲取字節(jié)碼類Class clazz = instance.getClass();// 循環(huán)通過反射獲取// 可能存在通過反射沒有找到成員的情況 , 此時查找其父類是否有該成員// 循環(huán)次數(shù)就是其父類層級個數(shù)while (clazz != null) {try {// 獲取成員Field memberField = clazz.getDeclaredField(memberName);// 如果不是 public , 無法訪問 , 設(shè)置可訪問if (!memberField.isAccessible()) {memberField.setAccessible(true);}return memberField;} catch (NoSuchFieldException exception){// 如果找不到, 就到父類中查找clazz = clazz.getSuperclass();}}// 如果沒有拿到成員 , 則直接中斷程序 , 加載無法進行下去throw new NoSuchFieldException("沒有在 " + clazz.getName() + " 類中找到 " + memberName + "成員");}/*** 通過反射方法獲取 instance 類中的 參數(shù)為 parameterTypes , 名稱為 methodName 的成員方法* @param instance 成員方法所在對象* @param methodName 成員方法名稱* @param parameterTypes 成員方法參數(shù)* @return* @throws NoSuchMethodException*/public static Method reflexMethod(Object instance, String methodName, Class... parameterTypes)throws NoSuchMethodException {// 獲取字節(jié)碼類Class clazz = instance.getClass();// 循環(huán)通過反射獲取// 可能存在通過反射沒有找到成員方法的情況 , 此時查找其父類是否有該成員方法// 循環(huán)次數(shù)就是其父類層級個數(shù)while (clazz != null) {try {// 獲取成員方法Method method = clazz.getDeclaredMethod(methodName, parameterTypes);// 如果不是 public , 無法訪問 , 設(shè)置可訪問if (!method.isAccessible()) {method.setAccessible(true);}return method;} catch (NoSuchMethodException e) {// 如果找不到, 就到父類中查找clazz = clazz.getSuperclass();}}// 如果沒有拿到成員 , 則直接中斷程序 , 加載無法進行下去throw new NoSuchMethodException("沒有在 " + clazz.getName() + " 類中找到 " + methodName + "成員方法");}}3、壓縮解壓縮工具類
壓縮解壓縮工具類 :
package kim.hsl.multipledex;import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.util.Enumeration; import java.util.zip.CRC32; import java.util.zip.CheckedOutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream;public class ZipUtils {/*** 刪除文件, 如果有目錄, 則遞歸刪除*/private static void deleteFile(File file){if (file.isDirectory()){File[] files = file.listFiles();for (File f: files) {deleteFile(f);}}else{file.delete();}}/*** 解壓文件* @param zip 被解壓的壓縮包文件* @param dir 解壓后的文件存放目錄*/public static void unZipApk(File zip, File dir) {try {// 如果存放文件目錄存在, 刪除該目錄deleteFile(dir);// 獲取 zip 壓縮包文件ZipFile zipFile = new ZipFile(zip);// 獲取 zip 壓縮包中每一個文件條目Enumeration<? extends ZipEntry> entries = zipFile.entries();// 遍歷壓縮包中的文件while (entries.hasMoreElements()) {ZipEntry zipEntry = entries.nextElement();// zip 壓縮包中的文件名稱 或 目錄名稱String name = zipEntry.getName();// 如果 apk 壓縮包中含有以下文件 , 這些文件是 V1 簽名文件保存目錄 , 不需要解壓 , 跳過即可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(dir, name);//創(chuàng)建目錄if (!file.getParentFile().exists()) {file.getParentFile().mkdirs();}// 向剛才創(chuàng)建的目錄中寫出文件FileOutputStream fos = new FileOutputStream(file);InputStream is = zipFile.getInputStream(zipEntry);byte[] buffer = new byte[2048];int len;while ((len = is.read(buffer)) != -1) {fos.write(buffer, 0, len);}is.close();fos.close();}}// 關(guān)閉 zip 文件zipFile.close();} catch (Exception e) {e.printStackTrace();}}/*** 壓縮目錄為zip* @param dir 待壓縮目錄* @param zip 輸出的zip文件* @throws Exception*/public static void zip(File dir, File zip) throws Exception {zip.delete();// 對輸出文件做CRC32校驗CheckedOutputStream cos = new CheckedOutputStream(new FileOutputStream(zip), new CRC32());ZipOutputStream zos = new ZipOutputStream(cos);//壓縮compress(dir, zos, "");zos.flush();zos.close();}/*** 添加目錄/文件 至zip中* @param srcFile 需要添加的目錄/文件* @param zos zip輸出流* @param basePath 遞歸子目錄時的完整目錄 如 lib/x86* @throws Exception*/private static void compress(File srcFile, ZipOutputStream zos,String basePath) throws Exception {if (srcFile.isDirectory()) {File[] files = srcFile.listFiles();for (File file : files) {// zip 遞歸添加目錄中的文件compress(file, zos, basePath + srcFile.getName() + "/");}} else {compressFile(srcFile, zos, basePath);}}private static void compressFile(File file, ZipOutputStream zos, String dir)throws Exception {// temp/lib/x86/libdn_ssl.soString fullName = dir + file.getName();// 需要去掉tempString[] fileNames = fullName.split("/");//正確的文件目錄名 (去掉了temp)StringBuffer sb = new StringBuffer();if (fileNames.length > 1){for (int i = 1;i<fileNames.length;++i){sb.append("/");sb.append(fileNames[i]);}}else{sb.append("/");}//添加一個zip條目ZipEntry entry = new ZipEntry(sb.substring(1));zos.putNextEntry(entry);//讀取條目輸出到zip中FileInputStream fis = new FileInputStream(file);int len;byte data[] = new byte[2048];while ((len = fis.read(data, 0, 2048)) != -1) {zos.write(data, 0, len);}fis.close();zos.closeEntry();}}4、OpenSSL 解密工具類
package kim.hsl.multipledex;import android.util.Log;import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile;public class OpenSSL {static {System.loadLibrary("openssl");}/*** 從文件中讀取 Byte 數(shù)組* @param file* @return* @throws Exception*/public static byte[] getBytes(File file) throws Exception {try {// 創(chuàng)建隨機讀取文件RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");// 獲取文件字節(jié)數(shù) , 創(chuàng)建保存文件數(shù)據(jù)的緩沖區(qū)byte[] buffer = new byte[(int) randomAccessFile.length()];// 讀取整個文件數(shù)據(jù)randomAccessFile.readFully(buffer);// 關(guān)閉文件randomAccessFile.close();return buffer;} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return null;}/*** 調(diào)用 OpenSSL 解密 dex 文件* @param data* @param path*/public static native void decrypt(byte[] data, String path); }
5、OpenSSL 解密相關(guān) NDK 源碼
#include <jni.h> #include <stdio.h> #include <android/log.h> #include <malloc.h> #include <string.h> #include <openssl/evp.h> #include "logging_macros.h"JNIEXPORT void JNICALL Java_kim_hsl_multipledex_OpenSSL_decrypt(JNIEnv *env, jobject instance, jbyteArray data, jstring path) {// 將 Java Byte 數(shù)組轉(zhuǎn)為 C 數(shù)組jbyte *src = (*env)->GetByteArrayElements(env, data, NULL);// 將 Java String 字符串轉(zhuǎn)為 C char* 字符串const char *filePath = (*env)->GetStringUTFChars(env, path, 0);// 獲取 Java Byte 數(shù)組長度int srcLen = (*env)->GetArrayLength(env, data);/** 下面的代碼是從 OpenSSL 源碼跟目錄下 demos/evp/aesccm.c 中拷貝并修改*/// 加密解密的上下文EVP_CIPHER_CTX *ctx;int outlen;// 創(chuàng)建加密解密上下文ctx = EVP_CIPHER_CTX_new();/* Select cipher 配置上下文解碼參數(shù)* 配置加密模式 :* Java 中的加密算法類型 "AES/ECB/PKCS5Padding" , 使用 ecb 模式* EVP_aes_192_ecb() 配置 ecb 模式* AES 有五種加密模式 : CBC、ECB、CTR、OCF、CFB* 配置密鑰 :* Java 中定義的密鑰是 "kimhslmultiplede"*/EVP_DecryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, "kimhslmultiplede", NULL);// 申請解密輸出數(shù)據(jù)內(nèi)存, 申請內(nèi)存長度與密文長度一樣即可// AES 加密密文比明文要長uint8_t *out = malloc(srcLen);// 將申請的內(nèi)存設(shè)置為 0memset(out, 0, srcLen);// 記錄解密總長度int totalLen = 0;/** 解密操作* int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out,int *outl, const unsigned char *in, int inl);* 解密 inl 長度的 in , 解密為 outl 長度的 out* 解密的輸入數(shù)據(jù)是 src, 長度為 srcLen 字節(jié), 注意該長度是 int 類型* 解密的輸出數(shù)據(jù)是 out, 長度為 srcLen 字節(jié), 注意該長度是 int* 指針類型*/EVP_DecryptUpdate(ctx, out, &outlen, src, srcLen);totalLen += outlen; //更新總長度/** int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm,int *outl);* 解密時, 每次解密 16 字節(jié), 如果超過了 16 字節(jié) , 就會剩余一部分無法解密,* 之前的 out 指針已經(jīng)解密了 outlen 長度, 此時接著后續(xù)解密, 指針需要進行改變 out + outlen* 此時需要調(diào)用該函數(shù) , 解密剩余內(nèi)容*/EVP_DecryptFinal_ex(ctx, out + outlen, &outlen);totalLen += outlen; //更新總長度, 此時 totalLen 就是總長度// 解密完成, 釋放上下文對象EVP_CIPHER_CTX_free(ctx);// 將解密出的明文, 寫出到給定的 Java 文件中FILE *file = fopen(filePath, "wb");// 寫出 out 指針指向的數(shù)據(jù) , 寫出個數(shù) totalLen * 1 , 寫出到 file 文件中fwrite(out, totalLen, 1, file);// 關(guān)閉文件fclose(file);// 釋放解密出的密文內(nèi)存free(out);// 釋放 Java 引用(*env)->ReleaseByteArrayElements(env, data, src, 0);(*env)->ReleaseStringUTFChars(env, path, filePath); }
6、CmakeLists.txt 構(gòu)建腳本
cmake_minimum_required(VERSION 3.4.1)# 配置編譯選項, 編譯類型 動態(tài)庫, C++ 源碼為 native-lib.c add_library(opensslSHAREDnative-lib.c)find_library(log-liblog)# 設(shè)置 openssl 函數(shù)庫的靜態(tài)庫地址 方式一 報錯 set(LIB_DIR ${CMAKE_SOURCE_DIR}/lib/${ANDROID_ABI}) add_library(crypto STATIC IMPORTED)# 預(yù)編譯 openssl 靜態(tài)庫 set_target_properties(cryptoPROPERTIESIMPORTED_LOCATION${LIB_DIR}/libcrypto.a) # 指定頭文件 include_directories(${CMAKE_SOURCE_DIR}/include) # 方式一配置完畢# 設(shè)置 openssl 函數(shù)庫的靜態(tài)庫地址 方式二# 指定 openssl 頭文件查找目錄 # CMAKE_SOURCE_DIR 指的是當(dāng)前的文件地址 #include_directories(${CMAKE_SOURCE_DIR}/include)# 指定 openssl 靜態(tài)庫 # CMAKE_CXX_FLAGS 表示會將 C++ 的參數(shù)傳給編譯器 # CMAKE_C_FLAGS 表示會將 C 參數(shù)傳給編譯器# 參數(shù)設(shè)置 : 傳遞 CMAKE_CXX_FLAGS C+= 參數(shù)給編譯器時 , 在 該參數(shù)后面指定庫的路徑 # CMAKE_SOURCE_DIR 指的是當(dāng)前的文件地址 # -L 參數(shù)指定動態(tài)庫的查找路徑 #set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -L${CMAKE_SOURCE_DIR}/lib/armeabi-v7a")#message("CMake octopus ${CMAKE_SOURCE_DIR} , ${ANDROID_ABI}, {CMAKE_SOURCE_DIR}/lib/${ANDROID_ABI}")# 鏈接動態(tài)庫 target_link_libraries(opensslcryptoandroid${log-lib})
7、NDK 日志頭文件
#ifndef __SAMPLE_ANDROID_DEBUG_H__ #define __SAMPLE_ANDROID_DEBUG_H__ #include <android/log.h>#if 1 #ifndef MODULE_NAME #define MODULE_NAME "octopus" #endif#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, MODULE_NAME, __VA_ARGS__) #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, MODULE_NAME, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, MODULE_NAME, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN,MODULE_NAME, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,MODULE_NAME, __VA_ARGS__) #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,MODULE_NAME, __VA_ARGS__)#define ASSERT(cond, ...) if (!(cond)) {__android_log_assert(#cond, MODULE_NAME, __VA_ARGS__);} #else#define LOGV(...) #define LOGD(...) #define LOGI(...) #define LOGW(...) #define LOGE(...) #define LOGF(...) #define ASSERT(cond, ...)#endif#endif // __SAMPLE_ANDROID_DEBUG_H__
8、build.gradle 構(gòu)建腳本
apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions'android {compileSdkVersion 29buildToolsVersion "30.0.2"defaultConfig {minSdkVersion 16targetSdkVersion 29versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"consumerProguardFiles 'consumer-rules.pro'externalNativeBuild{cmake{// 配置要編譯動態(tài)庫的 CPU 架構(gòu), 這里編譯 arm 和 x86 兩個版本的動態(tài)庫// arm64-v8a, armeabi-v7a, x86, x86_64abiFilters 'armeabi-v7a'}}//配置 APK 打包 哪些動態(tài)庫// 示例 : 如在工程中集成了第三方庫 , 其提供了 arm, x86, mips 等指令集的動態(tài)庫// 那么為了控制打包后的應(yīng)用大小, 可以選擇性打包一些庫 , 此處就是進行該配置ndk{// 打包生成的 APK 文件指揮包含 ARM 指令集的動態(tài)庫abiFilters "armeabi-v7a"}}externalNativeBuild{cmake{// 配置編譯的 CMake 腳本位置, 默認當(dāng)前目錄是 app 目錄// build.gradle 構(gòu)建腳本所在目錄path 'src/main/cpp/CMakeLists.txt'}}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}}dependencies {implementation fileTree(dir: 'libs', include: ['*.jar'])implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"implementation 'androidx.appcompat:appcompat:1.2.0'implementation 'androidx.core:core-ktx:1.3.2'testImplementation 'junit:junit:4.12'androidTestImplementation 'androidx.test.ext:junit:1.1.2'androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' }
三、Java 工具
Java 工具要執(zhí)行的操作 :
- 1 . 解壓依賴庫 : 解壓代理 Application 編譯生成的 aar 文件 , 目的是拿到其中的 classes.jar 文件 ;
- 2 . 生成 dex 文件 : 使用 dx 工具 , 將上述 classes.jar 生成為 classes.dex ;
- 3 . 解壓主應(yīng)用 : 解壓主應(yīng)用的 app-debug.apk 文件 , 目的是為了拿到其真實的 dex 文件 ;
- 4 . 加密 dex : 加密從 app-debug.apk 中拿到的 dex 文件 ;
- 5 . 拷貝 dex 文件 : 將上面生成的 代理 Application 的 classes.dex 拷貝到 app-debug.apk 文件解壓目錄 ;
- 6 . 壓縮打包 : 將上述加密后的 dex 文件 , 以及 拷貝了 代理 Application 的 classes.dex 所在的目錄壓縮打包為 app-unsigned.apk 文件 ;
- 7 . 對齊操作 : 使用 zipalign 工具 , 對齊 app-unsigned.apk , 對齊后的文件為 app-unsigned-aligned.apk ;
- 8 . 簽名操作 : 使用 apksigner 為 app-unsigned-aligned.apk 文件簽名 , 生成 app-signed-aligned.apk 簽名后文件 ;
最終生成的 app-signed-aligned.apk 簽名后文件就是 dex 加密的安裝包 , 該安裝包中的 dex 文件無法被直接查看 ;
1、主函數(shù)
package kim.hsl.multiple_dex_toolsimport java.io.* import java.util.zip.*/*** 此處配置 SDK 根目錄絕對路徑* D:/001_Programs/001_Android/002_Sdk/Sdk/* Y:/001_DevelopTools/002_Android_SDK/*/ val sdkDirectory = "D:/001_Programs/001_Android/002_Sdk/Sdk/"@ExperimentalStdlibApi fun main() {/*1 . 生成 dex 文件 , 該 dex 文件中只包含解密 其它 dex 的功能編譯工程會生成 Android 依賴庫的 aar 文件生成目錄是 module/build/outputs/aar/ 目錄下前提是需要在 菜單欄 / File / Setting / Build, Execution, Deployment / Compiler設(shè)置界面中 , 勾選 Compile independent modules in parallel (may require larger )將 D:\002_Project\002_Android_Learn\DexEncryption\multiple-dex-core\build\outputs\aar路徑下的 multiple-dex-core-debug.aar 文件后綴修改為 .zip解壓上述文件拿到 classes.jar 文件即可 ;*/// 獲取 multiple-dex-core-debug.aar 文件對象var aarFile = File("multiple-dex-core/build/outputs/aar/multiple-dex-core-debug.aar")// 解壓上述 multiple-dex-core-debug.aar 文件到 aarUnzip 目錄中// 創(chuàng)建解壓目錄var aarUnzip = File("multiple-dex-tools/aarUnzip")// 解壓操作unZip(aarFile, aarUnzip)// 拿到 multiple-dex-core-debug.aar 中解壓出來的 classes.jar 文件var classesJarFile = File(aarUnzip, "classes.jar")// 創(chuàng)建轉(zhuǎn)換后的 dex 目的文件, 下面會開始創(chuàng)建該 dex 文件var classesDexFile = File(aarUnzip, "classes.dex")// 打印要執(zhí)行的命令println("cmd /c ${sdkDirectory}build-tools/30.0.2/dx.bat --dex --output ${classesDexFile.absolutePath} ${classesJarFile.absolutePath}")/*將 jar 包變成 dex 文件使用 dx 工具命令注意 : Windows 命令行命令之前需要加上 "cmd /c " 信息 , Linux 與 MAC 命令行不用添加*/var process = Runtime.getRuntime().exec("cmd /c ${sdkDirectory}build-tools/30.0.2/dx.bat --dex --output ${classesDexFile.absolutePath} ${classesJarFile.absolutePath}")// 等待上述命令執(zhí)行完畢process.waitFor()// 執(zhí)行結(jié)果提示if(process.exitValue() == 0){println("生成 dex 操作 , 執(zhí)行成功");} else {println("生成 dex 操作 , 執(zhí)行失敗");}/*2 . 加密 apk 中的 dex 文件*/// 解壓 apk 文件 , 獲取所有的 dex 文件// 被解壓的 apk 文件var apkFile = File("app/build/outputs/apk/debug/app-debug.apk")// 解壓的目標(biāo)文件夾var apkUnZipFile = File("app/build/outputs/apk/debug/unZipFile")// 解壓文件unZip(apkFile, apkUnZipFile)// 從被解壓的 apk 文件中找到所有的 dex 文件, 小項目只有 1 個, 大項目可能有多個// 使用文件過濾器獲取后綴是 .dex 的文件var dexFiles : Array<File> = apkUnZipFile.listFiles({ file: File, s: String ->s.endsWith(".dex")})// 加密找到的 dex 文件var aes = AES(AES.DEFAULT_PWD)// 遍歷 dex 文件for(dexFile: File in dexFiles){// 讀取文件數(shù)據(jù)var bytes = getBytes(dexFile)// 加密文件數(shù)據(jù)var encryptedBytes = aes.encrypt(bytes)// 將加密后的數(shù)據(jù)寫出到指定目錄var outputFile = File(apkUnZipFile, "secret-${dexFile.name}")// 創(chuàng)建對應(yīng)輸出流var fileOutputStream = FileOutputStream(outputFile)// 將加密后的 dex 文件寫出, 然后刷寫 , 關(guān)閉該輸出流fileOutputStream.write(encryptedBytes)fileOutputStream.flush()fileOutputStream.close()// 刪除原來的文件dexFile.delete()}/*3 . 將代理 Application 中的 classes.dex 解壓到上述app/build/outputs/apk/debug/unZipFile 目錄中*/// 拷貝文件到 app/build/outputs/apk/debug/unZipFile 目錄中classesDexFile.renameTo(File(apkUnZipFile, "classes.dex"))// 壓縮打包 , 該壓縮包是未簽名的壓縮包var unSignedApk = File("app/build/outputs/apk/debug/app-unsigned.apk")// 壓縮打包操作zip(apkUnZipFile, unSignedApk)/*4 . 對齊操作*/// 對齊操作的輸出結(jié)果, 將 app-unsigned.apk 對齊, 對齊后的文件輸出到 app-unsigned-aligned.apk 中var unSignedAlignApk = File("app/build/outputs/apk/debug/app-unsigned-aligned.apk")// 打印要執(zhí)行的命令println("cmd /c ${sdkDirectory}build-tools/30.0.2/zipalign -f 4 ${unSignedApk.absolutePath} ${unSignedAlignApk.absolutePath}")/*將 app-unsigned.apk 對齊使用 zipalign 工具命令注意 : Windows 命令行命令之前需要加上 "cmd /c " 信息 , Linux 與 MAC 命令行不用添加*/process = Runtime.getRuntime().exec("cmd /c ${sdkDirectory}build-tools/30.0.2/zipalign -f 4 ${unSignedApk.absolutePath} ${unSignedAlignApk.absolutePath}")// 等待上述命令執(zhí)行完畢process.waitFor()// 執(zhí)行結(jié)果提示if(process.exitValue() == 0){println("對齊操作 執(zhí)行成功");} else {println("對齊操作 執(zhí)行失敗");}/*5 . 簽名操作*/// 簽名 apk 輸出結(jié)果, 將 app-unsigned-aligned.apk 簽名, 簽名后的文件輸出到 app-signed-aligned.apk 中var signedAlignApk = File("app/build/outputs/apk/debug/app-signed-aligned.apk")// 獲取簽名 jks 文件var jksFile = File("dex.jks")// 打印要執(zhí)行的命令println("cmd /c ${sdkDirectory}build-tools/30.0.2/apksigner sign --ks ${jksFile.absolutePath} --ks-key-alias Key0 --ks-pass pass:000000 --key-pass pass:000000 --out ${signedAlignApk.absolutePath} ${unSignedAlignApk.absolutePath}")/*將 app-unsigned.apk 對齊使用 zipalign 工具命令注意 : Windows 命令行命令之前需要加上 "cmd /c " 信息 , Linux 與 MAC 命令行不用添加*/process = Runtime.getRuntime().exec("cmd /c ${sdkDirectory}build-tools/30.0.2/apksigner sign --ks ${jksFile.absolutePath} --ks-key-alias Key0 --ks-pass pass:000000 --key-pass pass:000000 --out ${signedAlignApk.absolutePath} ${unSignedAlignApk.absolutePath}")// 打印錯誤日志var br = BufferedReader(InputStreamReader(process.errorStream))while ( true ){var line = br.readLine()if(line == null){break}else{println(line)}}br.close()// 等待上述命令執(zhí)行完畢process.waitFor()// 執(zhí)行結(jié)果提示if(process.exitValue() == 0){println("簽名操作 執(zhí)行成功");} else {println("簽名操作 執(zhí)行失敗");}}/*** 刪除文件, 如果有目錄, 則遞歸刪除*/ private fun deleteFile(file: File) {if (file.isDirectory) {val files = file.listFiles()for (f in files) {deleteFile(f)}} else {file.delete()} }/*** 解壓文件* @param zip 被解壓的壓縮包文件* @param dir 解壓后的文件存放目錄*/ fun unZip(zip: File, dir: File) {try {// 如果存放文件目錄存在, 刪除該目錄deleteFile(dir)// 獲取 zip 壓縮包文件val zipFile = ZipFile(zip)// 獲取 zip 壓縮包中每一個文件條目val entries = zipFile.entries()// 遍歷壓縮包中的文件while (entries.hasMoreElements()) {val zipEntry = entries.nextElement()// zip 壓縮包中的文件名稱 或 目錄名稱val name = zipEntry.name// 如果 apk 壓縮包中含有以下文件 , 這些文件是 V1 簽名文件保存目錄 , 不需要解壓 , 跳過即可if (name == "META-INF/CERT.RSA" || name == "META-INF/CERT.SF" || (name== "META-INF/MANIFEST.MF")) {continue}// 如果該文件條目 , 不是目錄 , 說明就是文件if (!zipEntry.isDirectory) {val file = File(dir, name)// 創(chuàng)建目錄if (!file.parentFile.exists()) {file.parentFile.mkdirs()}// 向剛才創(chuàng)建的目錄中寫出文件val fileOutputStream = FileOutputStream(file)val inputStream = zipFile.getInputStream(zipEntry)val buffer = ByteArray(1024)var len: Intwhile (inputStream.read(buffer).also { len = it } != -1) {fileOutputStream.write(buffer, 0, len)}inputStream.close()fileOutputStream.close()}}// 關(guān)閉 zip 文件zipFile.close()} catch (e: Exception) {e.printStackTrace()} }fun zip(dir: File, zip: File) {// 如果目標(biāo)壓縮包存在 , 刪除該壓縮包zip.delete()// 對輸出文件做 CRC32 校驗val cos = CheckedOutputStream(FileOutputStream(zip), CRC32())val zos = ZipOutputStream(cos)// 壓縮文件compress(dir, zos, "")zos.flush()zos.close() }private fun compress(srcFile: File, zos: ZipOutputStream, basePath: String) {if (srcFile.isDirectory) {val files = srcFile.listFiles()for (file in files) {// zip 遞歸添加目錄中的文件compress(file, zos, basePath + srcFile.name + "/")}} else {compressFile(srcFile, zos, basePath)} }private fun compressFile(file: File, zos: ZipOutputStream, dir: String) {// 拼接完整的文件路徑名稱val fullName = dir + file.name// app/build/outputs/apk/debug/unZipFile 路徑val fileNames = fullName.split("/").toTypedArray()// 正確的文件目錄名val sb = StringBuffer()if (fileNames.size > 1) {for (i in 1 until fileNames.size) {sb.append("/")sb.append(fileNames[i])}} else {sb.append("/")}// 添加 zip 條目val entry = ZipEntry(sb.substring(1))zos.putNextEntry(entry)// 讀取 zip 條目輸出到文件中val fis = FileInputStream(file)var len: Intval data = ByteArray(2048)while (fis.read(data, 0, 2048).also { len = it } != -1) {zos.write(data, 0, len)}fis.close()zos.closeEntry() }/*** 讀取文件到數(shù)組中*/ fun getBytes(file: File): ByteArray {// 創(chuàng)建隨機方位文件對象val randomAccessFile = RandomAccessFile(file, "r")// 獲取文件大小 , 并創(chuàng)建同樣大小的數(shù)據(jù)組val buffer = ByteArray(randomAccessFile.length().toInt())// 讀取真?zhèn)€文件到數(shù)組中randomAccessFile.readFully(buffer)// 關(guān)閉文件randomAccessFile.close()return buffer }
2、加密相關(guān)工具類
package kim.hsl.multiple_dex_toolsimport java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.RandomAccessFile import java.util.zip.* import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpecclass AES {// Kotlin 類中的靜態(tài)變量companion object{/*** 加密密鑰, 16 字節(jié)*/val DEFAULT_PWD = "kimhslmultiplede"}/*** 加密解密算法類型*/val algorithm = "AES/ECB/PKCS5Padding"/*** 加密算法, 目前本應(yīng)用中只需要加密, 不需要解密*/lateinit var encryptCipher: Cipher;/*** 解密算法*/lateinit var decryptCipher: Cipher;@ExperimentalStdlibApiconstructor(pwd: String){// 初始化加密算法encryptCipher = Cipher.getInstance(algorithm)// 初始化解密算法decryptCipher = Cipher.getInstance(algorithm)// 將密鑰字符串轉(zhuǎn)為字節(jié)數(shù)組var keyByte = pwd.toByteArray()// 創(chuàng)建密鑰val key = SecretKeySpec(keyByte, "AES")// 設(shè)置算法類型, 及密鑰encryptCipher.init(Cipher.ENCRYPT_MODE, key);// 設(shè)置算法類型, 及密鑰decryptCipher.init(Cipher.DECRYPT_MODE, key);}/*** 加密操作*/fun encrypt(contet: ByteArray) : ByteArray{var result : ByteArray = encryptCipher.doFinal(contet)return result}/*** 解密操作*/fun decrypt(contet: ByteArray) : ByteArray{var result : ByteArray = decryptCipher.doFinal(contet)return result}}
總結(jié)
以上是生活随笔為你收集整理的【Android 安全】DEX 加密 ( 阶段总结 | 主应用 | 代理 Application | Java 工具 | 代码示例 ) ★的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【错误记录】NDK 报错 java.la
- 下一篇: 【Android 属性动画】属性动画 P