【Android 安全】DEX 加密 ( Java 工具开发 | 解压 apk 文件 | 加密生成 dex 文件 | 打包未签名 apk 文件 | 文件解压缩相关代码 )_dex加密-程序员宅基地

技术标签: Android 性能优化  # Android 安全  dex 加密  



参考博客 :


【Android 安全】DEX 加密 ( 支持多 DEX 的 Android 工程结构 ) 博客中介绍了 DEX 加密工程的基本结构 ,

app 是主应用 , 其 Module 类型是 “Phone & Tablet Module” ,

multiple-dex-core 是 Android 依赖库 , 其作用是解密并加载多 DEX 文件 , 其 Module 类型是 “Android Library” ,

multiple-dex-tools 是 Java 依赖库 , 其类型是 “Java or Kotlin Library” , 其作用是用于生成主 DEX ( 主 DEX 的作用就是用于解密与加载多 DEX ) , 并且还要为修改后的 APK 进行签名 ;


【Android 安全】DEX 加密 ( 代理 Application 开发 | multiple-dex-core 依赖库开发 | 配置元数据 | 获取 apk 文件并准备相关目录 ) 博客中讲解了 multiple-dex-core 依赖库开发 , 每次启动都要解密与加载 dex 文件 , 在该博客中讲解到了 获取 apk 文件 , 并准备解压目录 ;

【Android 安全】DEX 加密 ( 代理 Application 开发 | 解压 apk 文件 | 判定是否是第一次启动 | 递归删除文件操作 | 解压 Zip 文件操作 ) 博客中讲解了 apk 文件解压操作 ;

【Android 安全】DEX 加密 ( 代理 Application 开发 | 加载 dex 文件 | 反射获取系统的 Element[] dexElements )博客中讲解了 dex 文件加载第一阶段 , 获取系统中的 Element[] dexElements ;

【Android 安全】DEX 加密 ( 代理 Application 开发 | 加载 dex 文件 | 使用反射获取方法创建本应用的 dexElements | 各版本创建 dex 数组源码对比 ) 博客中讲解了讲解 dex 文件加载操作 第二阶段 , 创建本应用的 dex 文件数组 Element[] dexElements ;

【Android 安全】DEX 加密 ( 代理 Application 开发 | 加载 dex 文件 | 将系统的 dexElements 与 应用的 dexElements 合并 | 替换操作 ) 博客中讲解了剩余的两个操作 :

  • 系统加载的 Element[] dexElements 数组 与 我们 自己的 Element[] dexElements 数组 进行合并操作 ;
  • 替换 ClassLoader 加载过程中的 Element[] dexElements 数组 ( 封装在 DexPathList 中 )

【Android 安全】DEX 加密 ( Java 加密工具开发 | 加密解密算法 API | 编译代理 Application 依赖库 | 解压依赖库 aar 文件 ) ) 博客中介绍 加密解密算法 API , 编译代理 Application 依赖库 , 解压依赖库 aar 文件 ;

【Android 安全】DEX 加密 ( Java 加密工具开发 | 生成 dex 文件 | Java 命令行执行 ) 博客中介绍 使用 SDK 中的 dx 工具生成 dex 文件 ;


本博客中讲解 将 app 主应用的 apk 文件解压 , 加密其中的 classes.dex 文件 , 并将代理 Application 依赖库中的 classes.dex 打包到未签名的 apk 文件中 ;





一、解压 apk 文件



被解压的 apk 文件位置 app/build/outputs/apk/debug/app-debug.apk ,

将该 apk 文件解压到 app/build/outputs/apk/debug/unZipFile 目录中 ;

    // 解压 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)

unZip 方法中的代码在最后一节中 ;





二、加密生成 dex 文件



将 app-debug.apk 中的 dex 文件进行加密 , 使用上篇博客中的 AES 类进行加密 ,

加密后 的 dex 文件重命名为 secret-classes.dex ,

放到 app/build/outputs/apk/debug/unZipFile 目录中 ,

然后删除原来的文件 ;


    // 加密找到的 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()
    }




三、打包未签名 apk 文件



将代理 Application 所在的 Android 依赖库的 dex 文件拷贝到 app/build/outputs/apk/debug/unZipFile 目录中 ,

将上述目录打包文件重命名为 app-unsigned.apk ,

存放路径为 app/build/outputs/apk/debug/app-unsigned.apk ;


    /*
        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)




四、完整代码示例



@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 D:/001_Programs/001_Android/002_Sdk/Sdk/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 D:/001_Programs/001_Android/002_Sdk/Sdk/build-tools/30.0.2/dx.bat --dex --output ${classesDexFile.absolutePath} ${classesJarFile.absolutePath}")
    // 等待上述命令执行完毕
    process.waitFor()

    // 执行结果提示
    if(process.exitValue() == 0){
    
        println("执行成功");
    } else {
    
        println("执行失败");
    }


    /*
        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)

}




五、文件解压缩相关代码



/**
 * 删除文件, 如果有目录, 则递归删除
 */
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()
}




六、执行结果



下图中 红色矩形框 中是 解压后的 apk 文件 ,

紫色矩形框 中的 secret-classes.dex 文件是加密后的 dex 文件 ,

蓝色矩形框 中是生成的 未签名的 apk 文件 ;

在这里插入图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/han1202012/article/details/109810588

智能推荐

Python可视化matplotlib&seborn14-普通热力图heatmap_python matplotlib 热力图浅色-程序员宅基地

文章浏览阅读2.1k次,点赞2次,收藏21次。详细介绍python seaborn绘制热图。_python matplotlib 热力图浅色

最好的产品经理社区或者讨论圈有哪几个?_软件产品经理交流社区-程序员宅基地

文章浏览阅读1.8k次。本文将从社区、公众号(产品分析、深度互联网评论)、大佬博客、科技媒体、豆瓣书单等多个方面来阐述目前国内产品经理最好的一些讨论圈和内容,欢迎收藏~产品社区人人都是产品经理官网: http://www.woshipm.com“人人都是产品经理”应该是产品经理日常逛得最多的垂直社区,它里面的内容也主要以产品和运营为主,很多知名的产品经理KOL都会在上面发布文章。职景的学员也经常会把自己的产品分析报告上传发布到“人人都是产品经理”上。这中间也有一些有趣的事,比如两个人上传同一篇文章,只是因为排版格式的_软件产品经理交流社区

西门子PLC与IFIX软件以太网通讯_ifix与西门子plc连接-程序员宅基地

文章浏览阅读4.9k次,点赞2次,收藏16次。摘要IFX组态监控软件与西门子S7-200、S7-300系列PLC通讯,通常采用以太网通讯方式,IFIX软件中,采用S7A驱动的S7TCP/IP的通讯方式。西门子PLC采用第三方工业通讯桥接器实现以太网通讯。方案实施介绍本文以S7 TCP驱动(S7A)连接1台S7300,1台S7200为例:一、配置以太网通讯桥接器的参数用西门子S7TCP驱动来通讯,需要注意参数“S7TCP目标PLC地址”,需要填入PLC的站地址。例:S7-300,IP地址:192...._ifix与西门子plc连接

linux jstack 分析,使用top和jstack分析高CPU问题-程序员宅基地

文章浏览阅读431次。通常我们所说的 CPU 使用率过高,这里面其实隐含着一个用来比较高与低的基准值,比如 JVM 在峰值负载下的平均 CPU 利用率为 40%,如果 CPU 使用率飙到 80% 就可以被认为是不正常的。典型的 JVM 进程包含多个 Java 线程,其中一些在等待工作,另一些则正在执行任务。在单个 Java 程序的情况下,线程数可以非常低,而对于处理大量并发事务的互联网后台来说,线程数可能会比较高。对于..._对于 cpu 的问题,最重要的是要找到是哪些线程在消耗 cpu,通过线程栈定位到问题代

TypeScript手册翻译系列1-基础类型-程序员宅基地

文章浏览阅读111次。为什么80%的码农都做不了架构师?>>> ..._typescript 文档翻译

基于Jenkins的DevOps流水线实践教程|2020全新制作|端到端研发效能提升-程序员宅基地

文章浏览阅读319次。讲师介绍课程寄语我认为Jenkins是对我这些年运维经验的总结,起初我们运维同学都是编写脚本完成一些自动化的操作。而有了Jenkins我们可以将我们的运维经验与Jenkins设计理念融合..._基于jenkins的devops工程实践

随便推点

JavaScript 禁止鼠标选择事件_js 禁止元素鼠标可用-程序员宅基地

文章浏览阅读1.4k次。1、这是通过CSS样式来实现的禁止用鼠标选择功能:unselectable为IE准备onselectstart为Chrome、Safari准备-moz-user-select是FF的css style:html,body{-moz-user-select: none; -khtml-user-select: none; user-select: none;}2、或者使用下面的方法<div unselectable="on" onselectstart="return false;" s_js 禁止元素鼠标可用

ESP32的学习之路(一),基本知识介绍和了解_esp32工作电压-程序员宅基地

文章浏览阅读4.2w次,点赞25次,收藏206次。(一)ESP32麻雀虽小,但也五脏俱全ESP32是Espressif乐鑫信息科技推出的一块WiFi芯片。拥有40nm工艺、双核32位MCU、2.4GHz双模Wi-Fi和蓝牙芯片、主频高达230MHz,计算能力可达600DMIPS。-涵盖精细分辨时钟门控、省电模式和动态电压调整等特征。-它集成了天线和射频巴伦,功率放大器,低噪声放大器,滤波器和电源管理模块等元器件,性能稳定,易于制造,工作温度范围从-40℃到125℃。-支持多种通信协议,如:I2C. I2S. SPI. UART. CAN_esp32工作电压

window下编译lua源码,编译lua的库文件,编译lua解释器,编译lua编译器_lua库和源码一起编译-程序员宅基地

文章浏览阅读448次。网上有很多博客讲如何在windows下编译lua源文件。两上大概是有两种方案:一种是用VS来编译,一种是自己写批处理文件,直接编译。附上以上两种方法的博客:借助VS开发在src文件夹下写批处理_lua库和源码一起编译

国内外优秀的计算机视觉团队汇总_知乎 大连理工大学李培华-程序员宅基地

文章浏览阅读2.1k次。本帖还在更新中,国内外优秀的计算机视觉团队有很多,我这里只是列举了自己从知乎、CSDN等网站上收集到的,排名不分先后,如有遗漏,还请谅解。同时欢迎小伙伴回帖补充,我会更新到本帖,谢谢~感谢极市平台微信公众号的粉丝Alan、Andy、陈、蓝色格调、亚辉、邵帅、城邑、SuperMAN和知乎好友:Shihua Huang的补充贡献最后更新于2020/7/22,已累计更新 10次国内高校研究团队北京清华大学:龙明盛,黄高,艾海舟,张长水(Big eyes laboratory 大眼睛实验室),._知乎 大连理工大学李培华

IntelliJ IDEA 无法预览 Markdown 文件的解决思路_idea markdown预览-程序员宅基地

文章浏览阅读1.4w次,点赞8次,收藏2次。文章目录一、问题描述二、解决思路一、问题描述  在项目开发中,一般情况都需要写一个 readme.md 来介绍项目的整体情况。但是有些同事的 IntelliJ IDEA 确无法显示 Markdown 的文件内容预览,无疑为团队开发交流形成了阻碍。二、解决思路确认 IntelliJ IDEA 是否关联了 .md 文件设置面板 Editor File Types Markdown 查看是否以及注册了 .markdown 或者 .md 的文件格式,如果没有请加上查看 Mark_idea markdown预览

IDL学习——处理自带经纬度文件的遥感影像,以哨兵5P数据为例_envi_glt_doit-程序员宅基地

文章浏览阅读2.7k次,点赞7次,收藏31次。@TOC欢迎使用Markdown编辑器你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。新的改变我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:全新的界面设计 ,将会带来全新的写作体验;在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;增_envi_glt_doit