【Android 安全】Android 应用 APK 加固总结 ( 加固原理 | 应用加固完整的实现方案 | 源码资源 )

article/2025/10/6 14:37:05

文章目录

  • 一、 APK 加固原理
    • 1、 Android 应用反编译
    • 2、 ProGuard 混淆
    • 3、 多 dex 加载原理
    • 4、 代理 Application 开发
    • 5、Java 工具开发
    • 6、Application 替换
  • 二、 应用加固完整的实现方案
    • 1、 代理 Application
      • ( 1 ) ProxyApplication
      • ( 2 ) OpenSSL 解码 Kotlin 类
      • ( 3 ) 反射工具类
      • ( 4 ) 压缩解压缩工具类
      • ( 5 ) OpenSSL 解码 C++ 代码
      • ( 6 ) OpenSSL 静态库涉及的 CMakeLists.txt 配置
    • 2、 Java 工具
      • ( 1 ) Java 工具主函数入口类
      • ( 2 ) dex 加密工具类
    • 3、 主应用程序
      • ( 1 ) AndroidManifest.xml 清单文件
      • ( 2 ) Application 主类
  • 三、 源码资源



对 Android 安全 专栏进行总结 ;





一、 APK 加固原理





1、 Android 应用反编译


Android 应用反编译 :

  • 【Android 安全】DEX 加密 ( 常用 Android 反编译工具 | apktool | dex2jar | enjarify | jd-gui | jadx )


2、 ProGuard 混淆


ProGuard 混淆 :

  • 【Android 安全】DEX 加密 ( Proguard 简介 | Proguard 相关网址 | Proguard 混淆配置 )
  • 【Android 安全】DEX 加密 ( Proguard 简介 | 默认 ProGuard 分析 )
  • 【Android 安全】DEX 加密 ( Proguard keep 用法 | Proguard 默认混淆结果 | 保留类及成员混淆结果 | 保留注解以及被注解修饰的类/成员/方法 )
  • 【Android 安全】DEX 加密 ( ProGuard 混淆 | -keepclassmembers 混淆效果 | -keepclasseswithmembernames 混淆效果 )
  • 【Android 安全】DEX 加密 ( Proguard 混淆 | 混淆后的报错信息 | Proguard 混淆映射文件 mapping.txt )
  • 【Android 安全】DEX 加密 ( Proguard 混淆 | 将混淆后的报错信息转为原始报错信息 | retrace.bat 命令执行目录 | 暴露更少信息 )


3、 多 dex 加载原理


多 dex 加载原理 :

  • 【Android 安全】DEX 加密 ( DEX 加密原理 | DEX 加密简介 | APK 文件分析 | DEX 分割 )
  • 【Android 安全】DEX 加密 ( 多 DEX 加载 | 65535 方法数限制和 MultiDex 配置 | PathClassLoader 类加载源码分析 | DexPathList )
  • 【Android 安全】DEX 加密 ( 不同 Android 版本的 DEX 加载 | Android 8.0 版本 DEX 加载分析 | Android 5.0 版本 DEX 加载分析 )
  • 【Android 安全】DEX 加密 ( DEX 加密使用到的相关工具 | dx 工具 | zipalign 对齐工具 | apksigner 签名工具 )
  • 【Android 安全】DEX 加密 ( 支持多 DEX 的 Android 工程结构


4、 代理 Application 开发


  • 【Android 安全】DEX 加密 ( 代理 Application 开发 | multiple-dex-core 依赖库开发 | 配置元数据 | 获取 apk 文件并准备相关目录 )
  • 【Android 安全】DEX 加密 ( 代理 Application 开发 | 解压 apk 文件 | 判定是否是第一次启动 | 递归删除文件操作 | 解压 Zip 文件操作 )
  • 【Android 安全】DEX 加密 ( 代理 Application 开发 | 加载 dex 文件 | 反射获取系统的 Element[] dexElements )
  • 【Android 安全】DEX 加密 ( 代理 Application 开发 | 加载 dex 文件 | 使用反射获取方法创建本应用的 dexElements | 各版本创建 dex 数组源码对比 )
  • 【Android 安全】DEX 加密 ( 代理 Application 开发 | 加载 dex 文件 | 将系统的 dexElements 与 应用的 dexElements 合并 | 替换操作 )
  • 【Android 安全】DEX 加密 ( 代理 Application 开发 | 交叉编译 OpenSSL 开源库 )
  • 【Android 安全】DEX 加密 ( 代理 Application 开发 | 项目中配置 OpenSSL 开源库 | 使用 OpenSSL 开源库解密 dex 文件 )
  • 【Android 安全】DEX 加密 ( 阶段总结 | 主应用 | 代理 Application | Java 工具 | 代码示例 ) ★


5、Java 工具开发


Java 工具开发 :

  • 【Android 安全】DEX 加密 ( Java 工具开发 | 加密解密算法 API | 编译代理 Application 依赖库 | 解压依赖库 aar 文件 )
  • 【Android 安全】DEX 加密 ( Java 工具开发 | 生成 dex 文件 | Java 命令行执行 )
  • 【Android 安全】DEX 加密 ( Java 工具开发 | 解压 apk 文件 | 加密生成 dex 文件 | 打包未签名 apk 文件 | 文件解压缩相关代码 )
  • 【Android 安全】DEX 加密 ( Java 工具开发 | apk 文件对齐 )
  • 【Android 安全】DEX 加密 ( Java 工具开发 | apk 文件签名 )
  • 【Android 安全】DEX 加密 ( 阶段总结 | 主应用 | 代理 Application | Java 工具 | 代码示例 ) ★


6、Application 替换


Application 替换 :

  • 【Android 安全】DEX 加密 ( Application 替换 | Android 应用启动原理 )
  • 【Android 安全】DEX 加密 ( Application 替换 | Android 应用启动原理 | ActivityThread 源码分析 )
  • 【Android 安全】DEX 加密 ( Application 替换 | Android 应用启动原理 | LoadedApk 源码分析 )
  • 【Android 安全】DEX 加密 ( Application 替换 | Android 应用启动原理 | Instrumentation 源码分析 )
  • 【Android 安全】DEX 加密 ( Application 替换 | Android 应用启动原理 | LoadedApk 后续分析 )
  • 【Android 安全】DEX 加密 ( Application 替换 | Android 应用启动原理 | ActivityThread 后续分析 | Application 替换位置 )
  • 【Android 安全】DEX 加密 ( Application 替换 | 获取 ContextImpl、ActivityThread、LoadedApk 类型对象 | 源码分析 )
  • 【Android 安全】DEX 加密 ( Application 替换 | 获取 ContextImpl、ActivityThread、LoadedApk 类型对象 )
  • 【Android 安全】DEX 加密 ( Application 替换 | 判定自定义 Application 存在 | 获取 ContextImpl 对象 )
  • 【Android 安全】DEX 加密 ( Application 替换 | 创建用户自定义 Application | 替换 ContextImpl 对象的 mOuterContext 成员 )
  • 【Android 安全】DEX 加密 ( Application 替换 | 加密不侵入原则 | 替换 ActivityThread 的 mInitialApplication 成员 )
  • 【Android 安全】DEX 加密 ( Application 替换 | ActivityThread 中的 mAllApplications 集合添加 Application )
  • 【Android 安全】DEX 加密 ( Application 替换 | 替换 LoadedApk 中的 Application mApplication 成员 )
  • 【Android 安全】DEX 加密 ( Application 替换 | 修改 LoadedApk 中的 mApplicationInfo 成员的 className 名称 )
  • 【Android 安全】DEX 加密 ( Application 替换 | 分析 Activity 组件中获取的 Application | ActivityThread | LoadedApk )
  • 【Android 安全】DEX 加密 ( Application 替换 | 分析 Service 组件中调用 getApplication() 获取的 Application 是否替换成功 )
  • 【Android 安全】DEX 加密 ( Application 替换 | 分析 BroadcastReceiver 组件中调用 getApplication() 获取的 Application )
  • 【Android 安全】DEX 加密 ( Application 替换 | 分析 ContentProvider 组件中调用 getApplication() 获取的 Application )
  • 【Android 安全】DEX 加密 ( Application 替换 | 分析 ContentProvider 组件中调用 getApplication() 获取的 Application 二 )
  • 【Android 安全】DEX 加密 ( Application 替换 | 兼容 ContentProvider 操作 | 源码资源 )




二、 应用加固完整的实现方案





1、 代理 Application


( 1 ) ProxyApplication

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 完成");}@Overridepublic void onCreate() {super.onCreate();// 如果之前没有替换过 , 执行 Application 替换操作// 说明没有调用到 createPackageContext 方法// 该 createPackageContext 方法只有在创建 ContentProvider 时才调用到// 如果没有调用到 , 说明 AndroidManifest.xml 中没有配置 ContentProvider// 此时需要在此处进行 Application 替换if (delegate == null){applicationExchange();}}@Overridepublic String getPackageName() {if(TextUtils.isEmpty(app_name)){// 如果 AndroidManifest.xml 中配置的 Application 全类名为空// 那么 不做任何操作}else{// 如果 AndroidManifest.xml 中配置的 Application 全类名不为空// 为了使 ActivityThread 的 installProvider 方法// 无法命中如下两个分支// 分支一 : context.getPackageName().equals(ai.packageName)// 分支二 : mInitialApplication.getPackageName().equals(ai.packageName)// 设置该方法返回值为空 , 上述两个分支就无法命中return "";}return super.getPackageName();}@Overridepublic Context createPackageContext(String packageName, int flags)throws PackageManager.NameNotFoundException {if(TextUtils.isEmpty(app_name)){// 如果 AndroidManifest.xml 中配置的 Application 全类名为空// 说明没有进行 dex 加密操作 , 返回父类方法执行即可return super.createPackageContext(packageName, flags);}else{// 只有在创建 ContentProvider 时才调用到该 createPackageContext 方法 ,// 如果没有调用到该方法 , 说明该应用中没有配置 ContentProvider ;// 该方法不一定会调用到// 先进行 Application 替换applicationExchange();// Application 替换完成之后 , 再继续向下执行创建 ContentProviderreturn delegate;}}/*** 调用 applicationExchange 替换 Application* 该成员就是替换后的 Application*/private Application delegate;/*** Application 替换主方法*/private void applicationExchange(){try {/*在此处进行 Application 替换*/// 先判断是否有配置 Application ,// 那么在 Manifest.xml 中的 meta-data 元数据 app_name 不为空// 如果开发者没有自定义 Application , 没有配置元数据 , 直接退出if (TextUtils.isEmpty(app_name)) {return;}// 获取上下文对象 , 保存下来 , 之后要使用Context baseContext = getBaseContext();// 通过反射获取 Application , 系统也是进行的反射操作Class<?> delegateClass = Class.forName(app_name);// 创建用户真实配置的 Applicationdelegate = (Application) delegateClass.newInstance();// 调用 Application 的 attach 函数// 该函数无法直接调用 , 也需要通过反射调用// 这里先通过反射获取 Application 的 attach 函数Method attach = Application.class.getDeclaredMethod("attach", Context.class);// attach 方法是私有的 , 设置 attach 方法允许访问attach.setAccessible(true);// 获取上下文对象 ,// 该 Context 是通过调用 Application 的 attachBaseContext 方法传入的 ContextImpl// 将该上下文对象传入 Application 的 attach 方法中attach.invoke(delegate, baseContext);/*参考 : https://hanshuliang.blog.csdn.net/article/details/111569017 博客查询应该替换哪些对象中的哪些成员截止到此处, Application 创建完毕 , 下面开始逐个替换下面的 Application① ContextImpl 的 private Context mOuterContext成员是 kim.hsl.multipledex.ProxyApplication 对象 ;② ActivityThread 中的 ArrayList<Application> mAllApplications集合中添加了 kim.hsl.multipledex.ProxyApplication 对象 ;③ LoadedApk 中的 mApplication 成员是 kim.hsl.multipledex.ProxyApplication 对象 ;④ ActivityThread 中的 Application mInitialApplication成员是 kim.hsl.multipledex.ProxyApplication 对象 ;*/// I . 替换 ① ContextImpl 的 private Context mOuterContext//  成员是 kim.hsl.multipledex.ProxyApplication 对象Class<?> contextImplClass = Class.forName("android.app.ContextImpl");// 获取 ContextImpl 中的 mOuterContext 成员Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext");// mOuterContext 成员是私有的 , 设置可访问性mOuterContextField.setAccessible(true);// ContextImpl 就是应用的 Context , 直接通过 getBaseContext() 获取即可mOuterContextField.set(baseContext, delegate);// II . 替换 ④ ActivityThread 中的 Application mInitialApplication//                    成员是 kim.hsl.multipledex.ProxyApplication 对象 ;Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");// 获取 ActivityThread 中的 mInitialApplication 成员Field mInitialApplicationField =activityThreadClass.getDeclaredField("mInitialApplication");// mInitialApplication 成员是私有的 , 设置可访问性mInitialApplicationField.setAccessible(true);// 从 ContextImpl 对象中获取其 ActivityThread mMainThread 成员变量Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");mMainThreadField.setAccessible(true);// ContextImpl 就是本应用的上下文对象 , 调用 getBaseContext 方法获得Object mMainThread = mMainThreadField.get(baseContext);// ContextImpl 就是应用的 Context , 直接通过 getBaseContext() 获取即可mInitialApplicationField.set(mMainThread, delegate);// III . 替换 ② ActivityThread 中的 ArrayList<Application> mAllApplications//                    集合中添加了 kim.hsl.multipledex.ProxyApplication 对象 ;// 获取 ActivityThread 中的 mAllApplications 成员Field mAllApplicationsField =activityThreadClass.getDeclaredField("mAllApplications");// mAllApplications 成员是私有的 , 设置可访问性mAllApplicationsField.setAccessible(true);// 获取 ActivityThread 中的 ArrayList<Application> mAllApplications 队列ArrayList<Application> mAllApplications =(ArrayList<Application>) mAllApplicationsField.get(mMainThread);// 将真实的 Application 添加到上述队列中mAllApplications.add(delegate);// IV . 替换 ③ LoadedApk 中的 mApplication//          成员是 kim.hsl.multipledex.ProxyApplication 对象// 1. 先获取 LoadedApk 对象// LoadedApk 是 ContextImpl 中的 LoadedApk mPackageInfo 成员变量// 从 ContextImpl 对象中获取其 LoadedApk mPackageInfo 成员变量Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");mPackageInfoField.setAccessible(true);// ContextImpl 就是本应用的上下文对象 , 调用 getBaseContext 方法获得Object mPackageInfo = mPackageInfoField.get(baseContext);// 2. 获取 LoadedApk 对象中的 mApplication 成员Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");// 获取 ActivityThread 中的 mInitialApplication 成员Field mApplicationField =loadedApkClass.getDeclaredField("mApplication");// LoadedApk 中的 mApplication 成员是私有的 , 设置可访问性mApplicationField.setAccessible(true);// 3. 将 Application 设置给 LoadedApk 中的 mApplication 成员mApplicationField.set(mPackageInfo, delegate);// V . 下一步操作替换替换 ApplicationInfo 中的 className , 该操作不是必须的 , 不替换也不会报错// 在应用中可能需要操作获取应用的相关信息 , 如果希望获取准确的信息 , 需要替换 ApplicationInfo// ApplicationInfo 在 LoadedApk 中Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");// 设置该字段可访问mApplicationInfoField.setAccessible(true);// mPackageInfo 就是 LoadedApk 对象// mApplicationInfo 就是从 LoadedApk 对象中获得的 mApplicationInfo 字段ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);// 设置 ApplicationInfo 中的 className 字段值mApplicationInfo.className = app_name;// 再次调用 onCreate 方法delegate.onCreate();} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (NoSuchFieldException exception) {exception.printStackTrace();}}
}

( 2 ) OpenSSL 解码 Kotlin 类

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);
}

( 3 ) 反射工具类

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 +  "成员方法");}}

( 4 ) 压缩解压缩工具类

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();}}

( 5 ) OpenSSL 解码 C++ 代码

需要交叉编译 OpenSSL 得到 libcrypto.a 静态库 , 在应用中使用该静态库进行解码操作 ;

#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 ) OpenSSL 静态库涉及的 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})


2、 Java 工具


( 1 ) Java 工具主函数入口类

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 = "Y:/001_DevelopTools/002_Android_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 ) dex 加密工具类

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}}


3、 主应用程序


( 1 ) AndroidManifest.xml 清单文件

<?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><service android:name=".MyService" /><receiver android:name=".MyBroadCastReciver"><intent-filter><action android:name="kim.hsl.dex.broadcast" /></intent-filter></receiver><providerandroid:exported="true"android:name=".MyProvider"android:authorities="kim.hsl.dex.myprovider" /></application></manifest>

( 2 ) Application 主类

package kim.hsl.dex;import android.app.Application;
import android.util.Log;public class MyApplication extends Application {@Overridepublic void onCreate() {super.onCreate();/*验证 Application 是否替换成功打印 Application , ApplicationContext , ApplicationInfo*/Log.i("octopus.MyApplication", "Application : " + this);Log.i("octopus.MyApplication", "ApplicationContext : " + getApplicationContext());Log.i("octopus.MyApplication", "ApplicationInfo.className : " + getApplicationInfo().className);}
}




三、 源码资源



DEX 加密源码资源 :

  • GitHub 地址 : https://github.com/han1202012/DexEncryption
  • CSDN 源码快照 : https://download.csdn.net/download/han1202012/16490814
  • 注意事项 : DexEncryption\multiple-dex-tools\src\main\java\kim\hsl\multiple_dex_tools 中的 Main.kt 中 , sdkDirectory 修改成你自己电脑上的 SDK 配置 , 需要使用其中的 build-tools 下的 签名工具 , 对齐工具 等 ;
val sdkDirectory = "Y:/001_DevelopTools/002_Android_SDK/"
  • 执行流程 : 先按照 【Android 安全】DEX 加密 ( Java 工具开发 | 加密解密算法 API | 编译代理 Application 依赖库 | 解压依赖库 aar 文件 ) 生成依赖库的 aar 文件 , 然后选择 菜单栏 -> Build -> Build Bundle(s) / APK (s) 选项 , 最后执行 DexEncryption\multiple-dex-tools\src\main\java\kim\hsl\multiple_dex_tools 中的 Main.kt 文件 ;

在这里插入图片描述


http://chatgpt.dhexx.cn/article/YY8PfWBc.shtml

相关文章

android资源加固,Android apk加固实现原理

apk加固是每一个app发布之前必须要做的事情;如果一个apk没有加固那么别人就很容易被别人反编译&#xff0c;看到这其中的原码&#xff0c;虽然现在有代码混淆、把业务写到native层&#xff0c;但是这都是治标不治本。反编译的技术在更新&#xff0c;那么保护Apk的技术就不能停止…

Android中Apk加固代码实现

前言&#xff1a;上一篇博客已经把Apk加固的思路详细的介绍过了&#xff0c;也开始创建了一个空的demo进行&#xff0c;然后在项目中添加一个代理module&#xff08;解密&#xff0c;和系统源码交互功能&#xff09;和tools工具加密Java library 的module &#xff0c;这里开始…

Android APK加固原理

一、前言 Android作为开源框架&#xff0c;开放之余&#xff0c;所要面临的就是安全问题&#xff0c;世间之事&#xff0c;有正就有邪&#xff0c;有攻就有守&#xff0c;作为开发者虽然不需要进入专业安全领域&#xff0c;但还是需要掌握基本的安全常识和原理。 二、加壳 加…

APK加固原理详解

一、前言 之前使用的360加固&#xff0c;挺好用的&#xff0c;从2021年底的时候限制每天每个账号仅上传2次apk&#xff08;免费的&#xff0c;不知道VIP的是不是这样&#xff09;。通过这个事情&#xff0c;感觉技术还是掌握在自己手里稳妥点&#xff0c;不用受制于人&#xf…

Android中的Apk的加固(加壳)原理解析和实现

本文转载自&#xff1a;Android中的Apk的加固(加壳)原理解析和实现 - roccheung - 博客园 一、前言 今天又到周末了&#xff0c;憋了好久又要出博客了&#xff0c;今天来介绍一下Android中的如何对Apk进行加固的原理。现阶段。我们知道Android中的反编译工作越来越让人操作熟…

浅谈安卓apk加固原理和实现

转载本文需注明出处&#xff1a;微信公众号EAWorld&#xff0c;违者必究。 引言&#xff1a; 在安卓开发中&#xff0c;打包发布是开发的最后一个环节&#xff0c;apk是整个项目的源码和资源的结合体&#xff1b;对于懂点反编译原理的人可以轻松编译出apk的源码资源&#xff0c…

安卓逆向笔记--apk加固

安卓逆向笔记–apk加固 资料来源: 浅谈安卓apk加固原理和实现 Android中的Apk的加固(加壳)原理解析和实现 前两个太老了所以具体代码借鉴下面的 Android Apk加壳技术实战详解 一、apk常见加固方法 (1)代码层级加密–代码混淆 代码混淆是一种常见的加密方式。本质是把工程中原…

imx6ull uboot移植

以下内容来自&#xff1a;正点原子Linux驱动文档 一、简介 uboot移植主要是根据原厂的uboot移植&#xff1a;芯片厂商通常会做一块自己的评估板并发布BSP&#xff0c;当我们需要定制自己的开发板时可以根据自己的需求&#xff08;硬件上的不同&#xff09;&#xff0c;对原厂…

全志V3S嵌入式驱动开发(uboot移植)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 安装了ubuntu操作系统,有了开发板,下面就可以进行我们的开发工作了。第一步,我们要面临的问题就是uboot移植。一般来说,嵌入式的基础环境就是uboot、linux和rootfs。uboot完成一…

2021-09-14 uboot移植开发

引言&#xff1a;最近要改动uboot&#xff0c;实现像微软PC上&#xff0c;u盘一键刷机或手机上安全模式下刷机的操作 专门去好好研究了点uboot的启动过程&#xff1b;以下为总结&#xff1a; 嵌入式系统 微软-PC ——…

iTOP4412 uboot移植教程

好多刚开始学习uboot移植的同学总是觉得uboot好难&#xff0c;但是再难的问题如果把它一步步拆开&#xff0c;一个个解决&#xff0c;问题也就将迎刃而解。做uboot移植&#xff0c;我们首先就得了解uboot的编译流程&#xff0c;这里以在iTOP4412精英版2G内存的板子上移植u-boot…

<Linux开发> -之-系统移植 uboot移植过程详细记录(第二部分)

&#xff1c;Linux开发&#xff1e; -之-系统移植 uboot移植过程详细记录&#xff08;第二部分&#xff09; 第一部分链接&#xff1a;系统移植-之-uboot移植第一部分 第一部分主要讲解了&#xff0c;uboot移植过程中使用的一些工具的安装&#xff0c;以及测试nxp远程uboot&a…

X210开发板(S5PV210芯片)uboot移植DM9000驱动移植

前言 本文是介绍在uboot中如何移植DM9000的驱动&#xff0c;并不深入去讲解DM9000芯片的操作时序和内部寄存器&#xff0c;想要读懂驱动代码要仔细阅读DM9000芯片的数据手册。移植的基础是手里有DM9000芯片可以用的驱动代码&#xff0c;只需要根据开发板中DM9000芯片的接线方式…

嵌入式linux UBoot移植篇

如何在U-boot添加自己的linux板卡并启动呢&#xff1f; uboot 的移植并不是说我们完完全全的从零开始将 uboot 移植到我们现在所使用的开发板或者开发平台上。这个对于我们来说基本是不可能的&#xff0c;这个工作一般是半导体厂商做的&#xff0c; 半导体厂商负责将 uboot 移…

Uboot移植流程

linux-Bootloader&#xff08;Uboot&#xff09;移植流程 前言 最近在做ZigBee的温室大棚项目&#xff0c;将自己学习的过程和经验分享给大家。本文基于linux3.4.39内核版本&#xff0c;s5p6818开发板实现。 1、uboot启动简介 uboot启动的过程比较复杂&#xff0c;这里就只…

IMX6ULL Uboot 移植

使用的开发板&#xff1a;正点原子ALPHA V2.2 Uboot简介 在学习STM32的过程中使用过IAP在线升级就会知道&#xff0c;有引导程序APP程序&#xff0c;即bootloader程序APP。在学习嵌入式Linux的时候也一样&#xff0c;这个引导程序就是Uboot. uboot移植主要是根据原厂的uboot移…

二、uboot移植

二、uboot移植 版本作者时间备注V 1.0bug设计工程师2021/11/10创建文件软件网盘链接0交叉编译工具链接:https://pan.baidu.com/s/1yFO2NDMet9_b1E1q1rMwEA提取码:42kluboot源码同上linux源码同上文件系统工具同上tftp工具同上2.1 简单说明 uboot制作结束会生成 u-boot-etc44…

linux-uboot 移植四 uboot的移植

概述 前边的章节中介绍到如果要移植uboot的话&#xff0c;最好的参考就是由官方提供的demo。 1、移植 1.1 添加board对应的板级文件夹 uboot 中每个板子都有一个对应的文件夹来存放板级文件&#xff0c;比如开发板上外设驱动文件等等。 NXP 的 I.MX 系列芯片的所有板级文件…

[uboot 移植]uboot 移植过程

文章目录 uboot 移植1 修改顶层 Makefile2 在 board 文件夹下添加开发板对应的板级文件2.1 imximage_lpddr2.cfg 和 imximage.cfg 文件2.2 plugin.S 文件2.3 Kconfig 文件2.4 igkboard.c 文件2.5 MAINTAINERS 文件2.6 Makefile 文件 3 添加 igkboard_defconfig 配置文件4 添加开…

UBoot 移植

1 NXP官方开发板uboot编译测试 1 查找 NXP 官方的开发板默认配置文件 因为我们的开发板是参考 NXP 官方的 I.MX6ULL EVK 开发板做的硬件&#xff0c;因此我们在移植 uboot 的时候就可以以 NXP 官方的 I.MX6ULL EVK 开发板为蓝本。 在 NXP 官方 I.MX6UL/6ULL 默认配置文件中找…