文章目录

  • 一、主应用
  • 二、代理 Application 解析
    • 1、代理 Application 源码
    • 2、反射对象成员以及方法的工具类
    • 3、压缩解压缩工具类
    • 4、OpenSSL 解密工具类
    • 5、OpenSSL 解密相关 NDK 源码
    • 6、CmakeLists.txt 构建脚本
    • 7、NDK 日志头文件
    • 8、build.gradle 构建脚本
  • 三、Java 工具
    • 1、主函数
    • 2、加密相关工具类

相关资源 :

  • 本阶段源码下载 : https://download.csdn.net/download/han1202012/13214384 ( 快照 )
  • GitHub 地址 : https://github.com/han1202012/DexEncryption ( 完整代码 )

一、主应用


在主应用中 , 进行两个操作 :

  • 操作一 : 配置 AndroidManifest.xml 中的 代理 Application ;
  • 操作二 : 配置 真实 Application 全类名 , 以及 版本号 ;
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="kim.hsl.dex"><applicationandroid:name="kim.hsl.multipledex.ProxyApplication"android:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/AppTheme"><!-- app_name 值是该应用的 Application 的真实全类名真实 Application : kim.hsl.dex.MyApplication代理 Application : kim.hsl.multipledex.ProxyApplication --><meta-data android:name="app_name" android:value="kim.hsl.dex.MyApplication"/><!-- DEX 解密之后的目录名称版本号 , 完整目录名称为 :kim.hsl.dex.MyApplication_1.0 --><meta-data android:name="app_version" android:value="\1.0"/><activity android:name=".MainActivity"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application></manifest>

配置 NDK 的 CPU 架构 : 只配置 armeabi-v7a 架构即可 ;

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{// 配置要编译动态库的 CPU 架构, 这里编译 armeabi-v7a 版本的动态库// 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 文件 : 获取本应用的 APK 文件 ;
  • 2 . 获取相关元数据 : 获取在主应用 AndroidManifest.xml 中配置的 真实 Application 全类名 , 以及版本号信息 ;
  • 3 . 创建工作目录 : 创建用户私有目录 , 将 APK 文件解压到该目录中 ;
  • 4 . 解密 dex 文件 : 遍历被解压的目录 , 发现被加密的 dex 文件后 , 将该 dex 文件解密为可以直接使用的 dex 文件 ;
  • 5 . 获取 DexPathList 对象 : 反射获取 BaseDexClassLoader 中的 DexPathList 成员 ;
  • 6 . 获取 Element[] dexElements 数组 : 反射获取 DexPathList 中的 Element[] dexElements 数组成员 ;
  • 7 . 获取创建 Element[] dexElements 数组方法 : 6.06.06.0 以下系统获取 makeDexElements 方法 , 7.07.07.0 以上系统获取 makePathElements 方法 ;
  • 8 . 创建 Element[] dexElements 数组 : 调用上述反射的方法创建 Element[] dexElements 数组 ;
  • 9 . 合并并设置 Element[] dexElements 数组 : 将上述创建的 Element[] dexElements 数组 与 原本的 Element[] dexElements 数组 合并 , 设置给 DexPathList 中的 Element[] dexElements 数组成员 ;

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";/*** 应用真实的 Application 全类名*/String app_name;/*** DEX 解密之后的目录名称*/String app_version;/*** 在 Application 在 ActivityThread 中被创建之后,* 第一个调用的方法是 attachBaseContext 函数.* 该函数是 Application 中最先执行的函数.*/@Overrideprotected void attachBaseContext(Context base) {super.attachBaseContext(base);try {Log.i(TAG, "attachBaseContext");/*在该 Application 中主要进行两个操作 :1 . 解密并加载多个 DEX 文件2 . 将真实的 Application 替换成应用的主 Application*//*I . 解密与加载多 DEX 文件先进行解密, 然后再加载解密之后的 DEX 文件1. 先获取当前的 APK 文件2. 然后解压该 APK 文件*/// 获取当前的 APK 文件, 下面的 getApplicationInfo().sourceDir 就是本应用 APK 安装文件的全路径File apkFile = new File(getApplicationInfo().sourceDir);// 获取在 app Module 下的 AndroidManifest.xml 中配置的元数据,// 应用真实的 Application 全类名// 解密后的 dex 文件存放目录ApplicationInfo applicationInfo = null;applicationInfo = getPackageManager().getApplicationInfo(getPackageName(),PackageManager.GET_META_DATA);Bundle metaData = applicationInfo.metaData;if (metaData != null) {// 检查是否存在 app_name 元数据if (metaData.containsKey("app_name")) {app_name = metaData.getString("app_name").toString();}// 检查是否存在 app_version 元数据if (metaData.containsKey("app_version")) {app_version = metaData.getString("app_version").toString();}}// 创建用户的私有目录 , 将 apk 文件解压到该目录中File privateDir = getDir(app_name + "_" + app_version, MODE_PRIVATE);Log.i(TAG, "attachBaseContext 创建用户的私有目录 : " + privateDir.getAbsolutePath());// 在上述目录下创建 app 目录// 创建该目录的目的是存放解压后的 apk 文件的File appDir = new File(privateDir, "app");// app 中存放的是解压后的所有的 apk 文件// app 下创建 dexDir 目录 , 将所有的 dex 目录移动到该 dexDir 目录中// dexDir 目录存放应用的所有 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 目录内容 : " + 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 数据// 这些 Byte 数组就是加密后的 dex 数据byte[] bytes = OpenSSL.getBytes(file);// 解密该二进制数据, 并替换原来的加密 dex, 直接覆盖原来的文件即可OpenSSL.decrypt(bytes, file.getAbsolutePath());// 将解密完毕的 dex 文件放在需要加载的 dex 集合中dexFiles.add(file);// 拷贝到 dexDir 中Log.i(TAG, "attachBaseContext 解密完成 被解密文件是 : " + file);}// 判定是否是需要解密的 dex 文件}// 遍历 apk 解压后的文件} else {// 已经解密完成, 此时不需要解密, 直接获取 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());}// 截止到此处 , 已经拿到了解密完毕 , 需要加载的 dex 文件// 加载自己解密的 dex 文件loadDex(dexFiles, privateDir);Log.i(TAG, "attachBaseContext 完成");} catch (PackageManager.NameNotFoundException e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}}/*** 加载 dex 文件集合* 这些 dex 文件已经解密* 参考博客 : https://hanshuliang.blog.csdn.net/article/details/109608605* <p>* 创建自己的 Element[] dexElements 数组* ( libcore/dalvik/src/main/java/dalvik/system/DexPathList.java )* 然后将 系统加载的 Element[] dexElements 数组 与 我们自己的 Element[] dexElements 数组进行合并操作*/void loadDex(ArrayList<File> dexFiles, File optimizedDirectory)throwsIllegalAccessException,InvocationTargetException,NoSuchFieldException,NoSuchMethodException {Log.i(TAG, "loadDex");/*需要执行的步骤1 . 获得系统 DexPathList 中的 Element[] dexElements 数组( libcore/dalvik/src/main/java/dalvik/system/DexPathList.java )2 . 在本应用中创建 Element[] dexElements 数组 , 用于存放解密后的 dex 文件3 . 将 系统加载的 Element[] dexElements 数组与 我们自己的 Element[] dexElements 数组进行合并操作4 . 替换 ClassLoader 加载过程中的 Element[] dexElements 数组 ( 封装在 DexPathList 中 )*//*1 . 获得系统 DexPathList 中的 Element[] dexElements 数组第一阶段 : 在 Context 中调用 getClassLoader() 方法 , 可以拿到 PathClassLoader ;第二阶段 : 从 PathClassLoader 父类 BaseDexClassLoader 中找到 DexPathList ;第三阶段 : 获取封装在 DexPathList 类中的 Element[] dexElements 数组 ;上述的 DexPathList 对象 是 BaseDexClassLoader 的私有成员Element[] dexElements 数组 也是 DexPathList 的私有成员因此只能使用反射获取 Element[] dexElements 数组*/// 阶段一二 : 调用 getClassLoader() 方法可以获取 PathClassLoader 对象// 从 PathClassLoader 对象中获取 private final DexPathList pathList 成员Field pathListField = ReflexUtils.reflexField(getClassLoader(), "pathList");// 获取 classLoader 对象对应的 DexPathList pathList 成员Object pathList = pathListField.get(getClassLoader());//阶段三 : 获取封装在 DexPathList 类中的 Element[] dexElements 数组Field dexElementsField = ReflexUtils.reflexField(pathList, "dexElements");// 获取 pathList 对象对应的 Element[] dexElements 数组成员Object[] dexElements = (Object[]) dexElementsField.get(pathList);/*2 . 在本应用中创建 Element[] dexElements 数组 , 用于存放解密后的 dex 文件不同的 Android 版本中 , 创建 Element[] dexElements 数组的方法不同 , 这里需要做兼容*/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 . 将 系统加载的 Element[] dexElements 数组与 我们自己的 Element[] dexElements 数组进行合并操作首先创建数组 , 数组类型与 dexElements 数组类型相同将 dexElements 数组中的元素拷贝到 newElements 前半部分, 拷贝元素个数是 dexElements.size将 addElements 数组中的元素拷贝到 newElements 后半部分, 拷贝元素个数是 dexElements.size*/Object[] newElements = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(),dexElements.length + addElements.length);// 将 dexElements 数组中的元素拷贝到 newElements 前半部分, 拷贝元素个数是 dexElements.sizeSystem.arraycopy(dexElements, 0, newElements, 0, dexElements.length);// 将 addElements 数组中的元素拷贝到 newElements 后半部分, 拷贝元素个数是 dexElements.sizeSystem.arraycopy(addElements, 0, newElements, dexElements.length, addElements.length);/*4 . 替换 ClassLoader 加载过程中的 Element[] dexElements 数组 ( 封装在 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 {// 获取字节码类Class clazz = instance.getClass();// 循环通过反射获取// 可能存在通过反射没有找到成员的情况 , 此时查找其父类是否有该成员// 循环次数就是其父类层级个数while (clazz != null) {try {// 获取成员Field memberField = clazz.getDeclaredField(memberName);// 如果不是 public , 无法访问 , 设置可访问if (!memberField.isAccessible()) {memberField.setAccessible(true);}return memberField;} catch (NoSuchFieldException exception){// 如果找不到, 就到父类中查找clazz = clazz.getSuperclass();}}// 如果没有拿到成员 , 则直接中断程序 , 加载无法进行下去throw new NoSuchFieldException("没有在 " + clazz.getName() + " 类中找到 " + memberName +  "成员");}/*** 通过反射方法获取 instance 类中的 参数为 parameterTypes , 名称为 methodName 的成员方法* @param instance 成员方法所在对象* @param methodName 成员方法名称* @param parameterTypes 成员方法参数* @return* @throws NoSuchMethodException*/public static Method reflexMethod(Object instance, String methodName, Class... parameterTypes)throws NoSuchMethodException {// 获取字节码类Class clazz = instance.getClass();// 循环通过反射获取// 可能存在通过反射没有找到成员方法的情况 , 此时查找其父类是否有该成员方法// 循环次数就是其父类层级个数while (clazz != null) {try {// 获取成员方法Method method = clazz.getDeclaredMethod(methodName, parameterTypes);// 如果不是 public , 无法访问 , 设置可访问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);//创建目录if (!file.getParentFile().exists()) {file.getParentFile().mkdirs();}// 向刚才创建的目录中写出文件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();}}// 关闭 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 数组* @param file* @return* @throws Exception*/public static byte[] getBytes(File file) throws Exception {try {// 创建随机读取文件RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");// 获取文件字节数 , 创建保存文件数据的缓冲区byte[] buffer = new byte[(int) randomAccessFile.length()];// 读取整个文件数据randomAccessFile.readFully(buffer);// 关闭文件randomAccessFile.close();return buffer;} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return null;}/*** 调用 OpenSSL 解密 dex 文件* @param data* @param path*/public static native void decrypt(byte[] data, String path);
}

5、OpenSSL 解密相关 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 数组转为 C 数组jbyte *src = (*env)->GetByteArrayElements(env, data, NULL);// 将 Java String 字符串转为 C char* 字符串const char *filePath = (*env)->GetStringUTFChars(env, path, 0);// 获取 Java Byte 数组长度int srcLen = (*env)->GetArrayLength(env, data);/** 下面的代码是从 OpenSSL 源码跟目录下 demos/evp/aesccm.c 中拷贝并修改*/// 加密解密的上下文EVP_CIPHER_CTX *ctx;int outlen;// 创建加密解密上下文ctx = EVP_CIPHER_CTX_new();/* Select cipher 配置上下文解码参数* 配置加密模式 :* 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);// 申请解密输出数据内存, 申请内存长度与密文长度一样即可// AES 加密密文比明文要长uint8_t *out = malloc(srcLen);// 将申请的内存设置为 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* 解密的输入数据是 src, 长度为 srcLen 字节, 注意该长度是 int 类型* 解密的输出数据是 out, 长度为 srcLen 字节, 注意该长度是 int* 指针类型*/EVP_DecryptUpdate(ctx, out, &outlen, src, srcLen);totalLen += outlen; //更新总长度/** int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm,int *outl);* 解密时, 每次解密 16 字节, 如果超过了 16 字节 , 就会剩余一部分无法解密,* 之前的 out 指针已经解密了 outlen 长度, 此时接着后续解密, 指针需要进行改变 out + outlen* 此时需要调用该函数 , 解密剩余内容*/EVP_DecryptFinal_ex(ctx, out + outlen, &outlen);totalLen += outlen; //更新总长度, 此时 totalLen 就是总长度// 解密完成, 释放上下文对象EVP_CIPHER_CTX_free(ctx);// 将解密出的明文, 写出到给定的 Java 文件中FILE *file = fopen(filePath, "wb");// 写出 out 指针指向的数据 , 写出个数 totalLen * 1 , 写出到 file 文件中fwrite(out, totalLen, 1, file);// 关闭文件fclose(file);// 释放解密出的密文内存free(out);// 释放 Java 引用(*env)->ReleaseByteArrayElements(env, data, src, 0);(*env)->ReleaseStringUTFChars(env, path, filePath);
}

6、CmakeLists.txt 构建脚本

cmake_minimum_required(VERSION 3.4.1)# 配置编译选项, 编译类型 动态库, C++ 源码为 native-lib.c
add_library(opensslSHAREDnative-lib.c)find_library(log-liblog)# 设置 openssl 函数库的静态库地址 方式一 报错
set(LIB_DIR ${CMAKE_SOURCE_DIR}/lib/${ANDROID_ABI})
add_library(crypto STATIC IMPORTED)# 预编译 openssl 静态库
set_target_properties(cryptoPROPERTIESIMPORTED_LOCATION${LIB_DIR}/libcrypto.a)
# 指定头文件
include_directories(${CMAKE_SOURCE_DIR}/include)
# 方式一配置完毕# 设置 openssl 函数库的静态库地址 方式二# 指定 openssl 头文件查找目录
#           CMAKE_SOURCE_DIR 指的是当前的文件地址
#include_directories(${CMAKE_SOURCE_DIR}/include)# 指定 openssl 静态库
# CMAKE_CXX_FLAGS 表示会将 C++ 的参数传给编译器
# CMAKE_C_FLAGS 表示会将 C 参数传给编译器# 参数设置 : 传递 CMAKE_CXX_FLAGS C+= 参数给编译器时 , 在 该参数后面指定库的路径
#   CMAKE_SOURCE_DIR 指的是当前的文件地址
#   -L 参数指定动态库的查找路径
#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}")# 链接动态库
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 构建脚本

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{// 配置要编译动态库的 CPU 架构, 这里编译 arm 和 x86 两个版本的动态库// arm64-v8a, armeabi-v7a, x86, x86_64abiFilters 'armeabi-v7a'}}//配置 APK 打包 哪些动态库//  示例 : 如在工程中集成了第三方库 , 其提供了 arm, x86, mips 等指令集的动态库//        那么为了控制打包后的应用大小, 可以选择性打包一些库 , 此处就是进行该配置ndk{// 打包生成的 APK 文件指挥包含 ARM 指令集的动态库abiFilters "armeabi-v7a"}}externalNativeBuild{cmake{// 配置编译的 CMake 脚本位置, 默认当前目录是 app 目录// build.gradle 构建脚本所在目录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 工具要执行的操作 :

  • 1 . 解压依赖库 : 解压代理 Application 编译生成的 aar 文件 , 目的是拿到其中的 classes.jar 文件 ;
  • 2 . 生成 dex 文件 : 使用 dx 工具 , 将上述 classes.jar 生成为 classes.dex ;
  • 3 . 解压主应用 : 解压主应用的 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、主函数

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设置界面中 , 勾选 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 目录中// 创建解压目录var aarUnzip = File("multiple-dex-tools/aarUnzip")// 解压操作unZip(aarFile, aarUnzip)// 拿到 multiple-dex-core-debug.aar 中解压出来的 classes.jar 文件var classesJarFile = File(aarUnzip, "classes.jar")// 创建转换后的 dex 目的文件, 下面会开始创建该 dex 文件var classesDexFile = File(aarUnzip, "classes.dex")// 打印要执行的命令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}")// 等待上述命令执行完毕process.waitFor()// 执行结果提示if(process.exitValue() == 0){println("生成 dex 操作 , 执行成功");} else {println("生成 dex 操作 , 执行失败");}/*2 . 加密 apk 中的 dex 文件*/// 解压 apk 文件 , 获取所有的 dex 文件// 被解压的 apk 文件var apkFile = File("app/build/outputs/apk/debug/app-debug.apk")// 解压的目标文件夹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){// 读取文件数据var bytes = getBytes(dexFile)// 加密文件数据var encryptedBytes = aes.encrypt(bytes)// 将加密后的数据写出到指定目录var outputFile = File(apkUnZipFile, "secret-${dexFile.name}")// 创建对应输出流var fileOutputStream = FileOutputStream(outputFile)// 将加密后的 dex 文件写出, 然后刷写 , 关闭该输出流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 . 对齐操作*/// 对齐操作的输出结果, 将 app-unsigned.apk 对齐, 对齐后的文件输出到 app-unsigned-aligned.apk 中var unSignedAlignApk = File("app/build/outputs/apk/debug/app-unsigned-aligned.apk")// 打印要执行的命令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}")// 等待上述命令执行完毕process.waitFor()// 执行结果提示if(process.exitValue() == 0){println("对齐操作 执行成功");} else {println("对齐操作 执行失败");}/*5 . 签名操作*/// 签名 apk 输出结果, 将 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")// 打印要执行的命令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()// 等待上述命令执行完毕process.waitFor()// 执行结果提示if(process.exitValue() == 0){println("签名操作 执行成功");} else {println("签名操作 执行失败");}}/*** 删除文件, 如果有目录, 则递归删除*/
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)// 创建目录if (!file.parentFile.exists()) {file.parentFile.mkdirs()}// 向刚才创建的目录中写出文件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()}}// 关闭 zip 文件zipFile.close()} catch (e: Exception) {e.printStackTrace()}
}fun zip(dir: File, zip: File) {// 如果目标压缩包存在 , 删除该压缩包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()
}/*** 读取文件到数组中*/
fun getBytes(file: File): ByteArray {// 创建随机方位文件对象val randomAccessFile = RandomAccessFile(file, "r")// 获取文件大小 , 并创建同样大小的数据组val buffer = ByteArray(randomAccessFile.length().toInt())// 读取真个文件到数组中randomAccessFile.readFully(buffer)// 关闭文件randomAccessFile.close()return buffer
}

2、加密相关工具类

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 类中的静态变量companion object{/*** 加密密钥, 16 字节*/val DEFAULT_PWD = "kimhslmultiplede"}/*** 加密解密算法类型*/val algorithm = "AES/ECB/PKCS5Padding"/*** 加密算法, 目前本应用中只需要加密, 不需要解密*/lateinit var encryptCipher: Cipher;/*** 解密算法*/lateinit var decryptCipher: Cipher;@ExperimentalStdlibApiconstructor(pwd: String){// 初始化加密算法encryptCipher = Cipher.getInstance(algorithm)// 初始化解密算法decryptCipher = Cipher.getInstance(algorithm)// 将密钥字符串转为字节数组var keyByte = pwd.toByteArray()// 创建密钥val key = SecretKeySpec(keyByte, "AES")// 设置算法类型, 及密钥encryptCipher.init(Cipher.ENCRYPT_MODE, key);// 设置算法类型, 及密钥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}}

【Android 安全】DEX 加密 ( 阶段总结 | 主应用 | 代理 Application | Java 工具 | 代码示例 ) ★相关推荐

  1. 【Android 安全】DEX 加密 ( Java 工具开发 | 加密解密算法 API | 编译代理 Application 依赖库 | 解压依赖库 aar 文件 )

    文章目录 一.加密解密算法 API 二.编译代理 Application 依赖库 三.解压代理 Application 依赖库 aar 文件 参考博客 : [Android 安全]DEX 加密 ( 常 ...

  2. Android车载系统(HVAC) 原理+源码分析(代码示例)

    前言 Android车载系统(HVAC)是一种用于控制车内温度.空气质量和湿度的系统,它是一项重要的汽车电子技术.在这种系统中,CarHvacManager控制器是一个重要的组件,它通过与车载传感器和 ...

  3. android 发短信意图,安卓实现发送短信小程序代码示例

    这篇文章主要介绍了Android开发中实现发送短信的小程序示例,文中还附带了一个监听广播接收者的升级版短信发送例子,需要的朋友可以参考下 上图为代码结构图. 现在我们看下具体的代码. Send.jav ...

  4. 每个开发阶段对应的最流行的Java工具

    就因为某个工具是最流行的,并不意味着它就是"最好的".在编程领域,什么是最好的取决于具体环境.然而,在一批类似的工具当中选择时,人气或受欢迎程度无疑是个重要的衡量标准.倒不是人气本 ...

  5. 【Android 安全】DEX 加密 ( Application 替换 | 兼容 ContentProvider 操作 | 源码资源 )

    文章目录 一. 命中 ActivityThread 中 installProvider 方法的分支三 1. 原理分析 2. 代码实现 二. 在 ContextImpl 的 createPackageC ...

  6. 【Android 安全】DEX 加密 ( 代理 Application 开发 | 项目中配置 OpenSSL 开源库 | 使用 OpenSSL 开源库解密 dex 文件 )

    文章目录 一.项目中配置 OpenSSL 开源库 二.OpenSSL 开源库解密参考代码 三.解密 dex 文件的 Java 代码 四.解密 dex 文件的 Jni 代码 参考博客 : [Androi ...

  7. 【Android 安全】DEX 加密 ( 代理 Application 开发 | 交叉编译 OpenSSL 开源库 )

    文章目录 一.OpenSSL 开源库简介 二.OpenSSL 源码及环境变量脚本下载 三.修改环境变量脚本 四.OpenSSL 交叉编译 五.OpenSSL 交叉编译相关资源下载 参考博客 : [An ...

  8. 【Android 安全】DEX 加密 ( Java 工具开发 | apk 文件签名 )

    文章目录 一.生成 jks 文件 二.签名命令 三.执行结果 四.处理 Unsupported major.minor version 52.0 错误 参考博客 : [Android 安全]DEX 加 ...

  9. 【Android 安全】DEX 加密 ( Java 工具开发 | apk 文件对齐 )

    文章目录 一.apk 对齐操作 二.apk 对齐命令 三.apk 对齐操作代码示例 四.apk 对齐执行结果 参考博客 : [Android 安全]DEX 加密 ( 常用 Android 反编译工具 ...

最新文章

  1. vs2017开发Node.js控制台程序
  2. IntelliJ IDEA 2020.3.3 发布:新增概念“可信赖项目”
  3. 我是如何用Worktile进行敏捷开发的
  4. nginx反向代理和rewrite进行解决跨域问题、去掉url中的一部分字符串,通过nginx正则生成新的url
  5. hdu 3367 Pseudoforest (最大生成树 最多存在一个环)
  6. COM编程之一 组件
  7. linux 怎么设置静态ip,如何在Linux中设置静态IP地址和配置网络
  8. 一张图学习常见this的指向
  9. android novate乱码,Novate 一款Android RxStyle的网络框架
  10. 对空进行判断需要注意什么?
  11. 找到的The LEGEND of the DRAGON的新下载地址
  12. sql外键需要输入吗_sql_外键(foreignkey)
  13. 做一个有批判性思维的程序员
  14. 本周成长记录及跟踪(2019年-11月-第4周)
  15. AMD新旗舰:短小精悍,干掉双芯卡
  16. php判断号码归属,PHP验证手机号码和查询归属地查询的代码
  17. 使用Fiddler修改百度的Logo
  18. 在CubieTruck上安装TF卡版的armbian
  19. Python利用SMTP/IMTP制作简单邮件(QQ邮箱)发送程序(保姆级)
  20. SEO网站优化基础解决方案[快速入门]

热门文章

  1. [原创].NET 业务框架开发实战之十 第一阶段总结,深入浅出,水到渠成(前篇)...
  2. Python脚本导出为exe程序
  3. 构建之法读书笔记03
  4. web主题公园版权信息破解:script.js加密文件
  5. INotifyPropertyChanged 接口 CallerMemberName属性
  6. 在CentOS上搭建PHP服务器环境
  7. hibernate一级缓存和二级缓存的区别[转]
  8. 树形数据深度排序处理示例(递归法).sql
  9. 从人的角度分析进销存管理的需求
  10. 同一局域网内_Pycharm访问服务器