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

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 【Android 安全】DEX 加密 ( 阶段总结 | 主应用 | 代理 Application | Java 工具 | 代码示例 ) ★(一)

文章目录

一、主应用

二、代理 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 架构即可 ;


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 数组方法 : 6.0 6.06.0 以下系统获取 makeDexElements 方法 , 7.0 7.07.0 以上系统获取 makePathElements 方法 ;

8 . 创建 Element[] dexElements 数组 : 调用上述反射的方法创建 Element[] dexElements 数组 ;

9 . 合并并设置 Element[] dexElements 数组 : 将上述创建的 Element[] dexElements 数组 与 原本的 Element[] dexElements 数组 合并 , 设置给 DexPathList 中的 Element[] dexElements 数组成员 ;



1、代理 Application 源码


package kim.hsl.multipledex;
import android.app.Application;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
public class ProxyApplication extends Application {
    public static final String TAG = "ProxyApplication";
    /**
     * 应用真实的 Application 全类名
     */
    String app_name;
    /**
     * DEX 解密之后的目录名称
     */
    String app_version;
    /**
     * 在 Application 在 ActivityThread 中被创建之后,
     * 第一个调用的方法是 attachBaseContext 函数.
     * 该函数是 Application 中最先执行的函数.
     */
    @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 完成");
    }
}





目录
相关文章
|
12天前
|
存储 Java
Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。
【10月更文挑战第19天】本文详细介绍了Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。HashMap以其高效的插入、查找和删除操作著称,而TreeMap则擅长于保持元素的自然排序或自定义排序,两者各具优势,适用于不同的开发场景。
23 1
|
4天前
|
人工智能 前端开发 Java
基于开源框架Spring AI Alibaba快速构建Java应用
本文旨在帮助开发者快速掌握并应用 Spring AI Alibaba,提升基于 Java 的大模型应用开发效率和安全性。
基于开源框架Spring AI Alibaba快速构建Java应用
|
7天前
|
SQL 监控 Java
技术前沿:Java连接池技术的最新发展与应用
本文探讨了Java连接池技术的最新发展与应用,包括高性能与低延迟、智能化管理和监控、扩展性与兼容性等方面。同时,结合最佳实践,介绍了如何选择合适的连接池库、合理配置参数、使用监控工具及优化数据库操作,为开发者提供了一份详尽的技术指南。
20 7
|
5天前
|
SQL Java 数据库连接
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率。本文介绍了连接池的工作原理、优势及实现方法,并提供了HikariCP的示例代码。
16 3
|
5天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
15 2
|
6天前
|
缓存 Java 数据库连接
Hibernate:Java持久层框架的高效应用
通过上述步骤,可以在Java项目中高效应用Hibernate框架,实现对关系数据库的透明持久化管理。Hibernate提供的强大功能和灵活配置,使得开发者能够专注于业务逻辑的实现,而不必过多关注底层数据库操作。
8 1
|
10天前
|
移动开发 前端开发 JavaScript
java家政系统成品源码的关键特点和技术应用
家政系统成品源码是已开发完成的家政服务管理软件,支持用户注册、登录、管理个人资料,家政人员信息管理,服务项目分类,订单与预约管理,支付集成,评价与反馈,地图定位等功能。适用于各种规模的家政服务公司,采用uniapp、SpringBoot、MySQL等技术栈,确保高效管理和优质用户体验。
|
10天前
|
SQL 监控 Java
Java性能优化:提升应用效率与响应速度的全面指南
【10月更文挑战第21】Java性能优化:提升应用效率与响应速度的全面指南
|
12天前
|
存储 Java API
详细解析HashMap、TreeMap、LinkedHashMap等实现类,帮助您更好地理解和应用Java Map。
【10月更文挑战第19天】深入剖析Java Map:不仅是高效存储键值对的数据结构,更是展现设计艺术的典范。本文从基本概念、设计艺术和使用技巧三个方面,详细解析HashMap、TreeMap、LinkedHashMap等实现类,帮助您更好地理解和应用Java Map。
31 3
|
12天前
|
Java 数据处理