Android Apk加固原理解析

news2024/11/15 6:47:12

前言

为什么要加固

对APP进行加固,可以有效防止移动应用被破解、盗版、二次打包、注入、反编译等,保障程序的安全性、稳定性。

常见的加固方案有很多,本文主要介绍如果通过对dex文件进行加密来达到apk加固的目的;

APK加固整体思路

APK加固
加固整体思路:先解压apk文件,取出dex文件,对dex文件进行加密,然后组合壳中的dex文件(Android类加载机制),结合之前的apk资源(解压apk除dex以外的其他资源,如manifest、res等),打包新的apk文件,并对新的apk文件进行对齐、签名。

从上述流程可以看出,我们需要清楚的知道apk打包流程;
APK打包流程

具体实现

理解了上述APK加固思路,我们就可以按照思路进行对应代码实现;

项目整体结构如下:
项目整体结构

  • 新建shell模块,编写apk壳代码;

这里主要是增加代理ProxyApplication,重写Application中的attachBaseContext(app运行起来最先执行的方法)方法,主要做如下几件事:

  1. dex文件解密;
  2. 反射加载解密后的dex文件;
  3. 反射ActivityThread流程,替换真实的Application(源dex中的application);
public class ProxyApplication extends Application {
    private String applicationName;
    private boolean isRestoreRealApp;
    private Application realApp;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        PackageManager packageManager = base.getPackageManager();
        PackageInfo packageInfo = null;
        try {
            packageInfo = packageManager.getPackageInfo(this.getPackageName(), 0);
            ApplicationInfo applicationInfo = packageManager.getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
            Bundle metaData = applicationInfo.metaData;
            if (metaData.containsKey("application_name")) {
                applicationName = metaData.getString("application_name");
            }

            //应用最后一次更新时间
            long lastUpdateTime = packageInfo.lastUpdateTime;

            // 获取当前应用的apk文件
            File apkFile = new File(getApplicationInfo().sourceDir);
            // 在应用私有存储空间创建一个存放解压apk后的文件地址。
            File unZipFile = getDir("fake_apk", MODE_PRIVATE);
            /**
             * 这里根据 "app_" + 应用最后一次更新的时间 作为解压的文件目录
             * 作用:  应用每更新一次时,我们都需要重新解压apk文件。
             *        当应用没有更新是,如果apk已经解压就不需要再次解压,加快第二次启动的时间。
             *
             */
            File app = new File(unZipFile, "app_" + lastUpdateTime);
            unZipAndDecryptDex(apkFile, app);
            // 存放所有的dex文件
            ArrayList<File> dexList = new ArrayList<>();
            for (File file : app.listFiles()) {
                if (file.getName().endsWith(".dex")) {
                    dexList.add(file);
                }
            }
            LogUtils.i(dexList.toString());
            // 注意这里通过getClassLoader()获取的ClassLoader是PathClassLoader,而PathClassLoader是
            // BaseDexClassLoader的子类。
            LoaderDexUtils.loader(getClassLoader(), dexList, unZipFile);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
    }


    /**
     * 解压apk并解密被加密了的dex文件
     *
     * @param apkFile 被加密了的 apk 文件
     * @param app     存放解压和解密后的apk文件目录
     */
    private void unZipAndDecryptDex(File apkFile, File app) {
        if (!app.exists() || app.listFiles().length == 0) {
            // 当app文件不存在,或者 app 文件是一个空文件夹是需要解压。

            // 解压apk到指定目录
            ZipUtils.unZip(apkFile, app);
            // 获取所有的dex
            File[] dexFiles = app.listFiles(new FilenameFilter() {
                @Override
                public boolean accept(File file, String s) {
                    // 提取所有的.dex文件
                    return s.endsWith(".dex");
                }
            });

            if (dexFiles == null || dexFiles.length <= 0) {
                LogUtils.i("this apk is invalidate");
                return;
            }

            for (File file : dexFiles) {
                if (file.getName().equals("classes.dex")) {
                    /**
                     * 我们在加密的时候将不能加密的壳dex命名为classes.dex并拷贝到新apk中打包生成新的apk中了。
                     * 所以这里我们做脱壳,壳dex不需要进行解密操作。
                     */
                } else {
                    /**
                     * 加密的dex进行解密,对应加密流程中的_.dex文件
                     */
                    byte[] buffer = FileUtils.getBytes(file);
                    if (buffer != null) {
                        // 解密
                        byte[] decryptBytes = EncryptUtils.getInstance().decrypt(buffer);
                        if (decryptBytes != null) {
                            //修改.dex名为_.dex,避免等会与aar中的.dex重名
                            int indexOf = file.getName().indexOf(".dex");
                            String newName = file.getParent() + File.separator +
                                    file.getName().substring(0, indexOf) + "new.dex";
                            // 写数据, 替换原来的数据
                            FileUtils.wirte(new File(newName), decryptBytes);
                            file.delete();
                        } else {
                            LogUtils.e("Failed to encrypt dex data");
                            return;
                        }
                    } else {
                        LogUtils.e("Failed to read dex data");
                        return;
                    }
                }
            }
        }
    }


    @Override
    public void onCreate() {
        super.onCreate();
        // 替换真实的Application,不然壳的入侵性太强,而且原apk的Application不能运行。
        restoreRealApp();
    }

    private void restoreRealApp() {
        if (isRestoreRealApp) {
            return;
        }
        if (TextUtils.isEmpty(applicationName)) {
            return;
        }

        try {

            // 得到 attachBaseContext(context) 传入的上下文 ContextImpl
            Context baseContext = getBaseContext();
            // 拿到真实 APK Application 的 class
            Class<?> realAppClass = Class.forName(applicationName);
            // 反射实例化,其实 Android 中四大组件都是这样实例化的。
            realApp = (Application) realAppClass.newInstance();

            // 得到 Application attach() 方法 也就是最先初始化的
            Method attach = Application.class.getDeclaredMethod("attach", Context.class);
            attach.setAccessible(true);
            //执行 Application#attach(Context)
            //将真实的 Application 和假的 Application 进行替换。想当于自己手动控制 真实的 Application 生命周期
            attach.invoke(realApp, baseContext);


            // ContextImpl---->mOuterContext(app)   通过Application的attachBaseContext回调参数获取
            Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
            // 获取 mOuterContext 属性
            Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext");
            mOuterContextField.setAccessible(true);
            mOuterContextField.set(baseContext, realApp);

            //拿到 ActivityThread 变量
            Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
            mMainThreadField.setAccessible(true);
            // 拿到 ActivityThread 对象
            Object mMainThread = mMainThreadField.get(baseContext);

            //  ActivityThread--->>mInitialApplication
            //  反射拿到 ActivityThread class
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            // 得到当前加载的 Application 类
            Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
            mInitialApplicationField.setAccessible(true);
            // 将 ActivityThread 中的 mInitialApplication 替换为 真实的 Application 可以用于接收相应的声明周期和一些调用等
            mInitialApplicationField.set(mMainThread, realApp);


            //   ActivityThread--->mAllApplications(ArrayList)       ContextImpl的mMainThread属性
            //   拿到 ActivityThread 中所有的 Application 集合对象
            Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
            mAllApplicationsField.setAccessible(true);
            ArrayList<Application> mAllApplications = (ArrayList<Application>) mAllApplicationsField.get(mMainThread);
            // 删除 ProxyApplication
            mAllApplications.remove(this);
            //  添加真实的 Application
            mAllApplications.add(realApp);

            //  LoadedApk------->mApplication         ContextImpl的mPackageInfo属性
            Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
            mPackageInfoField.setAccessible(true);
            Object mPackageInfo = mPackageInfoField.get(baseContext);
            Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");
            Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
            mApplicationField.setAccessible(true);
            //将 LoadedApk 中的 mApplication 替换为 真实的 Application
            mApplicationField.set(mPackageInfo, realApp);

            //修改ApplicationInfo className   LooadedApk
            // 拿到 LoadApk 中的 mApplicationInfo 变量
            Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
            mApplicationInfoField.setAccessible(true);
            ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
            // 将我们真实的 Application ClassName 名称赋值于它
            mApplicationInfo.className = applicationName;

            // 执行真实 Application onCreate 声明周期
            realApp.onCreate();

            //解码完成
            isRestoreRealApp = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public String getPackageName() {
        if (!TextUtils.isEmpty(applicationName)) {
            return "";
        }
        return super.getPackageName();
    }

    /**
     * 这个函数是如果在 AndroidManifest.xml 中定义了 ContentProvider 那么就会执行此处 : installProvider,简介调用该函数
     *
     * @param packageName
     * @param flags
     * @return
     * @throws PackageManager.NameNotFoundException
     */
    @Override
    public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException {
        if (TextUtils.isEmpty(applicationName)) {
            return super.createPackageContext(packageName, flags);
        }
        try {
            restoreRealApp();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return realApp;
    }
}
  • 打包shell模块成aar文件;
  • 编写App模块代码并打包apk文件;
    App模块主要修改启动application为代理ProxyApplication,并依赖shell模块;
    App模块Manifest文件
  • 新建encrypt模块,编写dex加密、打包apk流程方法;
object ApkEncryptMain {
	//源apk存放路径
    private const val SOURCE_APK_PATH = "encrypt/source/apk/app-debug.apk"
    //壳aar文件存放路径
    private const val SHELL_APK_PATH = "encrypt/source/arr/shell-release.aar"

    @JvmStatic
    fun main(args: Array<String>) {
        LogUtils.i("start encrypt")
        init()
        //解压源apk文件到../source/apk/temp目录下,并加密dex文件
        val sourceApk = File(SOURCE_APK_PATH)
        val newApkDir = File(sourceApk.parent + File.separator + "temp")
        if (!newApkDir.exists()) {
            newApkDir.mkdirs()
        }
        //解压apk并加密dex文件
        EncryptUtils.getInstance().encryptApkFile(sourceApk, newApkDir)
        //解压arr文件(不加密的部分),并将其中的dex文件拷贝到apk/temp目录下
        val shellApk = File(SHELL_APK_PATH)
        val newShellDir = File(shellApk.parent + File.separator + "temp")
        if (!newShellDir.exists()){
            newShellDir.mkdirs()
        }
        //解压aar文件,并将aar中的jar文件转换为dex文件,然后拷贝aar中的classes.dex到apk/temp目录下
        DxUtils.jar2Dex(shellApk,newShellDir)

        //3.打包apk/temp目录生成新的未签名的apk文件
        val unsignedApk = File("encrypt/result/apk-unsigned.apk")
        unsignedApk.parentFile.mkdirs()
        unsignedApk.delete()
        ZipUtils.zip(newApkDir,unsignedApk)

        //4.对齐
        val unAlignApk = File("encrypt/result/apk-unAlign.apk")
        unAlignApk.parentFile.mkdirs()
        unAlignApk.delete()
        ZipUtils.zipalign(unsignedApk,unAlignApk)

        //5.给新的apk添加签名,生成签名apk
        val signedApk = File("encrypt/result/apk-signed.apk")
        signedApk.parentFile.mkdirs()
        signedApk.delete()
        SignUtils.signature(unAlignApk,signedApk)
    }


    //初始化
    private fun init() {
        FileUtils.deleteFolder("encrypt/source/apk/temp")
        FileUtils.deleteFolder("encrypt/source/arr/temp")
    }
}
  • 将源apk和壳aar文件拷贝到SOURCE_APK_PATHSHELL_APK_PATH对应目录,执行Main方法,最终产物如图:
    最终产物
    我们解压apk-signed.apk,点开dex文件,可以看到如图:
    加密apk
    证明我们dex加密成功!!

备注:shell壳模块没有使用kotlin编写代码原因:kotlin项目的apk加固后安装会崩溃

代码链接

代码链接

结语

如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/453428.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【计算机视觉 | 目标检测】OVD:Open-Vocabulary Object Detection 论文工作总结(共八篇)

文章目录 一、2D open-vocabulary object detection的发展和研究现状二、基于大规模外部图像数据集2.1 OVR-CNN&#xff1a;Open-Vocabulary Object Detection Using Captions&#xff0c;CVPR 20212.2 Open Vocabulary Object Detection with Pseudo Bounding-Box Labels&…

Springboot创建项目bug

问题 今天创建maven项目&#xff0c;由于和教程不太一样&#xff0c;结果报错了 核心报错如下 Cannot instantiate interface org.springframework.context.ApplicationListener : org.springframework.boot.context.logging.LoggingApplicationListener 梳理 我的idea创建…

系统集成项目管理工程师 笔记(第六章:项目整体管理)

文章目录 项目整体管理6个过程制定项目章程过程 6.3 制订项目管理计划 2476.4 指导与管理项目工作 2516.5 监控项目工作 255监控项目工作的输入监控项目工作的工具与技术监控项目工作的输出 6.6 实施整体变更控制6.7结束项目或阶段 6.1 项目整体管理概述 242 6.1.1 项目整体管理…

【过程8】——能量守恒视角总结感受

一、背景 另一个角度的看到&#xff0c;观望着过程中自己曾经类似的经历(小舅子的工作)。 时间久了&#xff0c;经历多了&#xff0c;感悟会更加的充实&#xff1b;最近自己对于人在维持能量的过程中也有很多的感悟&#xff0c;一并做一下总结 二、过程 1.人为什么天性不愿意…

npm和yarn的相同点和不同点

官网 npmhttps://www.npmjs.com Home | Yarn - Package ManagerFast, reliable, and secure dependency management.https://yarnpkg.com Fast, disk space efficient package manager | pnpmFast, disk space efficient package managerhttps://pnpm.io 使用场景 npm&#x…

数据库系统概论--第五章课后习题

1.什么是数据库的完整性&#xff1f; 答&#xff1a;数据库的完整性是指数据的正确性和相容性。 2. 数据库的完整性概念与数据库的安全性概念有什么区别和联系&#xff1f; 答&#xff1a; 数据的完整性和安全性是两个不同的概念,但是有一定的联系。前者是为了防止数据库中存…

将本地Python项目打包成docker镜像,上传到服务器,在docker中运行

文章目录 Docker环境创建虚拟环境pycharm使用虚拟环境准备打包保存为镜像文件加载镜像文件 参考文献 Docker环境 windows11Docker下载地址&#xff1a;https://docs.docker.com/desktop/install/windows-install/ 创建虚拟环境 虚拟环境可以搭建独立的Python运行环境&#x…

ORACLE_OCM.MGMT_CONFIG_JOB_2_2

今天巡检一套AIX上11g rac&#xff0c;发现有个报错 Errors in file /opt/app/oracle/diag/rdbms/orcl/orcl2/trace/orcl2_j000_16777270.trc: ORA-12012: error on auto execute of job "ORACLE_OCM"."MGMT_CONFIG_JOB_2_2" ORA-29280: invalid director…

〖Python网络爬虫实战⑱〗- 数据存储之TXT纯文本

订阅&#xff1a;新手可以订阅我的其他专栏。免费阶段订阅量1000 python项目实战 Python编程基础教程系列&#xff08;零基础小白搬砖逆袭) 说明&#xff1a;本专栏持续更新中&#xff0c;目前专栏免费订阅&#xff0c;在转为付费专栏前订阅本专栏的&#xff0c;可以免费订阅付…

WebSocket+Vue+SpringBoot实现语音通话

参考文章 整体思路 前端点击开始对话按钮后&#xff0c;将监听麦克风&#xff0c;获取到当前的音频&#xff0c;将其装化为二进制数据&#xff0c;通过websocket发送到webscoket服务端&#xff0c;服务端在接收后&#xff0c;将消息写入给指定客户端&#xff0c;客户端拿到发送…

Automa自动化爬取文本(一)

目录 介绍 下载地址 安装教程 爬取百度热搜 介绍 Automa 是一个免费、开源的 Chrome 扩展&#xff0c;它通过目前流行的 No Code 无代码方式&#xff0c;只需要拖拽模块就实现了浏览器自动化&#xff0c;比如自动填写表格、执行重复性任务。 在工作中&#xff0c;如果我们…

Docker安装Nginx(图文详解版)

目录 1.下载Nginx镜像 2.创建Nginx配置文件 3.创建Nginx容器并运行 4.查看效果 1.下载Nginx镜像 命令描述docker pull nginx下载最新版Nginx镜像 (此命令等同于 : docker pull nginx:latest )docker pull nginx:xxx下载指定版本的Nginx镜像 &#xff08;xxx指具体版本号&a…

有趣的 Kotlin 0x14:Base64编码

前言 Concise. Cross‑platform. Fun. Kotlin 来到 1.8.20 版本, 又给开发者带来了很多更新, 今天关注下标准库中新增的 Base64 相关内容. 原理 Base64编码是一种将二进制数据转换为可打印ASCII字符的编码方式。它使用64个不同的字符&#xff08;通常是A-Z、a-z、0-9和两个额…

学会SpringBoot的第一天(超详细)

&#x1f648;作者简介&#xff1a;练习时长两年半的Java up主 &#x1f649;个人主页&#xff1a;老茶icon &#x1f64a; ps:点赞&#x1f44d;是免费的&#xff0c;却可以让写博客的作者开兴好久好久&#x1f60e; &#x1f4da;系列专栏&#xff1a;Java全栈&#xff0c;计…

d2l Transformer

终于到变形金刚了&#xff0c;他的主要特征在于多头自注意力的使用&#xff0c;以及摒弃了rnn的操作。 目录 1.原理 2.多头注意力 3.逐位前馈网络FFN 4.层归一化 5.残差连接 6.Encoder 7.Decoder 8.训练 9.预测 1.原理 主要贡献&#xff1a;1.纯使用attention的Enco…

JavaFX与Liberica JDK,搭建,运行,打包,放弃Eclipse

1、官网 JavaFX中文官方网站、Oracle官方文档 2、教程 JavaFX中文基础教程视频合集 JavaFX实战教程 3、VSCode/Eclipse VSCode(写HelloWorld用)、VSCode的Java扩展 Eclipse&#xff0c;跳至第9段 4、Liberica JDK安装 Liberica JDK官网下载 依次选择&#xff0c;All ve…

压力测试防踩坑指南,压测中要注意的那些事儿

对于一些高频访问接口&#xff0c;压力测试必不可少&#xff0c;本文主要叙述了自己在压测过程中遇到的问题&#xff0c;在此分享&#xff0c;希望能帮助大家避免踩坑&#xff0c;提高效率。 1.pod数量 现象&#xff1a;服务器资源充足&#xff0c;tps上不去&#xff0c;检查发…

OneData 共享同一套数据技术和资产

一、什么是 OneData 体系? 官方&#xff1a;阿里云OneData数据中台解决方案基于大数据存储和计算平台为载体&#xff0c;以OneModel统一数据构建及管理方法论为主干&#xff0c;OneID核心商业要素资产化为核心&#xff0c;实现全域链接、标签萃取、立体画像&#xff0c;以数据…

ASEMI代理ADI亚德诺ADAU1701JSTZ-RL车规级芯片

编辑-Z ADAU1701JSTZ-RL芯片参数&#xff1a; 型号&#xff1a;ADAU1701JSTZ-RL 模拟电源电压&#xff1a;3.3 V 数字电源电压&#xff1a;1.8 V 输入/输出电压&#xff1a;3.3 V 环境温度&#xff1a;25 C 主时钟输入&#xff1a;12.288 MHz 满刻度模拟输入&#xff1…

彻底掌握FreeRTOS中的务通知(Task Notifications)

​在之前的文章中已经讲解了很多种用于任务件通信的机制&#xff0c;包括队列、事件组和各种不同类型的信号量。使用这些机制都需要创建一个通信对象。 事件和数据不会直接发送到接收任务或接收ISR&#xff0c;而是发送到通信对象&#xff08;也就是发送到队列、事件组、信号量…