文章目录
- 一、主应用
- 二、代理 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">
<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>
</application>
</manifest>
配置 NDK 的 CPU 架构 : 只配置 armeabi-v7a 架构即可 ;
代码语言:javascript复制apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "kim.hsl.dex"
minSdkVersion 18
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild{
cmake{
// 配置要编译动态库的 CPU 架构, 这里编译 armeabi-v7a 版本的动态库
// arm64-v8a, armeabi-v7a, x86, x86_64
abiFilters 'armeabi-v7a'
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles 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 数组方法 :
以下系统获取 makeDexElements 方法 ,
以上系统获取 makePathElements 方法 ;
- 8 . 创建 Element[] dexElements 数组 : 调用上述反射的方法创建 Element[] dexElements 数组 ;
- 9 . 合并并设置 Element[] dexElements 数组 : 将上述创建的 Element[] dexElements 数组 与 原本的 Element[] dexElements 数组 合并 , 设置给 DexPathList 中的 Element[] dexElements 数组成员 ;
1、代理 Application 源码
代码语言: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 完成");
}
}
2、反射对象成员以及方法的工具类
反射对象成员以及方法的工具类 :
代码语言: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 "成员方法");
}
}
3、压缩解压缩工具类
压缩解压缩工具类 :
代码语言: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();
}
}
4、OpenSSL 解密工具类
代码语言: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);
}
5、OpenSSL 解密相关 NDK 源码
代码语言: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、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})
7、NDK 日志头文件
代码语言:javascript复制#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 构建脚本
代码语言:javascript复制apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
buildToolsVersion "30.0.2"
defaultConfig {
minSdkVersion 16
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
externalNativeBuild{
cmake{
// 配置要编译动态库的 CPU 架构, 这里编译 arm 和 x86 两个版本的动态库
// arm64-v8a, armeabi-v7a, x86, x86_64
abiFilters '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 false
proguardFiles 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、主函数
代码语言: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 = "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: 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、加密相关工具类
代码语言: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
}
}