文章目录
- 一、 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
代码语言:javascript复制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 中最先执行的函数.
*/
@Override
protected 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)
throws
IllegalAccessException,
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.size
System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
// 将 addElements 数组中的元素拷贝到 newElements 后半部分, 拷贝元素个数是 dexElements.size
System.arraycopy(addElements, 0, newElements, dexElements.length, addElements.length);
/*
4 . 替换 ClassLoader 加载过程中的 Element[] dexElements 数组 ( 封装在 DexPathList 中 )
*/
dexElementsField.set(pathList, newElements);
Log.i(TAG, "loadDex 完成");
}
@Override
public void onCreate() {
super.onCreate();
// 如果之前没有替换过 , 执行 Application 替换操作
// 说明没有调用到 createPackageContext 方法
// 该 createPackageContext 方法只有在创建 ContentProvider 时才调用到
// 如果没有调用到 , 说明 AndroidManifest.xml 中没有配置 ContentProvider
// 此时需要在此处进行 Application 替换
if (delegate == null){
applicationExchange();
}
}
@Override
public 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();
}
@Override
public 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 替换完成之后 , 再继续向下执行创建 ContentProvider
return 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);
// 创建用户真实配置的 Application
delegate = (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 类
代码语言:javascript复制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 ) 反射工具类
代码语言:javascript复制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 ) 压缩解压缩工具类
代码语言:javascript复制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.so
String fullName = dir file.getName();
// 需要去掉temp
String[] 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 静态库 , 在应用中使用该静态库进行解码操作 ;
代码语言:javascript复制#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);
// 将申请的内存设置为 0
memset(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 配置
代码语言:javascript复制cmake_minimum_required(VERSION 3.4.1)
# 配置编译选项, 编译类型 动态库, C 源码为 native-lib.c
add_library(
openssl
SHARED
native-lib.c)
find_library(
log-lib
log)
# 设置 openssl 函数库的静态库地址 方式一 报错
set(LIB_DIR ${CMAKE_SOURCE_DIR}/lib/${ANDROID_ABI})
add_library(crypto STATIC IMPORTED)
# 预编译 openssl 静态库
set_target_properties(
crypto
PROPERTIES
IMPORTED_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(
openssl
crypto
android
${log-lib})
2、 Java 工具
( 1 ) Java 工具主函数入口类
代码语言:javascript复制package kim.hsl.multiple_dex_tools
import 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: 02_Project 02_Android_LearnDexEncryptionmultiple-dex-corebuildoutputsaar
路径下的 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: Int
while (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: Int
val 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 加密工具类
代码语言:javascript复制package kim.hsl.multiple_dex_tools
import 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.SecretKeySpec
class AES {
// Kotlin 类中的静态变量
companion object{
/**
* 加密密钥, 16 字节
*/
val DEFAULT_PWD = "kimhslmultiplede"
}
/**
* 加密解密算法类型
*/
val algorithm = "AES/ECB/PKCS5Padding"
/**
* 加密算法, 目前本应用中只需要加密, 不需要解密
*/
lateinit var encryptCipher: Cipher;
/**
* 解密算法
*/
lateinit var decryptCipher: Cipher;
@ExperimentalStdlibApi
constructor(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 清单文件
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="kim.hsl.dex">
<application
android: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>
<provider
android:exported="true"
android:name=".MyProvider"
android:authorities="kim.hsl.dex.myprovider" />
</application>
</manifest>
( 2 ) Application 主类
代码语言:javascript复制package kim.hsl.dex;
import android.app.Application;
import android.util.Log;
public class MyApplication extends Application {
@Override
public 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
- 注意事项 : DexEncryptionmultiple-dex-toolssrcmainjavakimhslmultiple_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) 选项 , 最后执行 DexEncryptionmultiple-dex-toolssrcmainjavakimhslmultiple_dex_tools 中的 Main.kt 文件 ;