Android Apk加固原理解析

article/2025/10/5 20:14:52

前言

为什么要加固

对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;@Overrideprotected 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);// 获取所有的dexFile[] dexFiles = app.listFiles(new FilenameFilter() {@Overridepublic 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;}}}}}@Overridepublic void onCreate() {super.onCreate();// 替换真实的Application,不然壳的入侵性太强,而且原apk的Application不能运行。restoreRealApp();}private void restoreRealApp() {if (isRestoreRealApp) {return;}if (TextUtils.isEmpty(applicationName)) {return;}try {// 得到 attachBaseContext(context) 传入的上下文 ContextImplContext baseContext = getBaseContext();// 拿到真实 APK Application 的 classClass<?> 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 classClass<?> 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);// 删除 ProxyApplicationmAllApplications.remove(this);//  添加真实的 ApplicationmAllApplications.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 替换为 真实的 ApplicationmApplicationField.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();}}@Overridepublic String getPackageName() {if (!TextUtils.isEmpty(applicationName)) {return "";}return super.getPackageName();}/*** 这个函数是如果在 AndroidManifest.xml 中定义了 ContentProvider 那么就会执行此处 : installProvider,简介调用该函数** @param packageName* @param flags* @return* @throws PackageManager.NameNotFoundException*/@Overridepublic 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"@JvmStaticfun 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添加签名,生成签名apkval 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://chatgpt.dhexx.cn/article/oLxTg4FK.shtml

相关文章

apk加固后再签名

目录 前言v1签名v1v2签名 前言 apk更新之前需要做安全检测&#xff0c;检测之前一版会做加固处理&#xff0c;加固后还需要重新进行签名。本文介绍一下v1签名和v1v2签名两种方式。 有文章说需要把apk原来的签名文件&#xff0c;即META-INF文件夹删除&#xff0c;实测不删好像也…

Android apk 加固混淆的作用之解决apk报毒

现在市面上对apk的安全合规管控越来越严格了&#xff0c;也就要求了apk在上架之前一定要做合规检测和加固处理。对apk就是加固的好处&#xff0c;可以提高apk的安全性&#xff0c;提高apk被逆向分析破解的门槛&#xff0c;同时通过加固保护可以提高过安全合规的检测。由于APP加…

简书 android 加固,Android apk加固(加壳)整理

一、Dex加壳由来 最近在学习apk加密&#xff0c;在网上看了一篇《Android中的Apk的加固(加壳)原理解析和实现》&#xff0c;我发现原文把整个apk都写入到dex文件中&#xff0c;如果apk小还好&#xff0c;当原APK大于200M&#xff0c;客户端解壳很费劲&#xff0c;打开后应用就卡…

019 Android加固之APK加固的原理和实现

文章目录 前言加载Activity遇到的问题APK的启动过程替换ClassLoader流程获取ActivityThread类对象获取AppBindData类对象mBoundApplication获取LoadedApk类对象info获取info对象中的ClassLoader 设计傀儡dex文件手工加固APK代码实现APK加固实现步骤 总结 前言 动态加载dex之后…

【Android 安全】Android 应用 APK 加固总结 ( 加固原理 | 应用加固完整的实现方案 | 源码资源 )

文章目录 一、 APK 加固原理1、 Android 应用反编译2、 ProGuard 混淆3、 多 dex 加载原理4、 代理 Application 开发5、Java 工具开发6、Application 替换 二、 应用加固完整的实现方案1、 代理 Application( 1 ) ProxyApplication( 2 ) OpenSSL 解码 Kotlin 类( 3 ) 反射工具…

android资源加固,Android apk加固实现原理

apk加固是每一个app发布之前必须要做的事情;如果一个apk没有加固那么别人就很容易被别人反编译&#xff0c;看到这其中的原码&#xff0c;虽然现在有代码混淆、把业务写到native层&#xff0c;但是这都是治标不治本。反编译的技术在更新&#xff0c;那么保护Apk的技术就不能停止…

Android中Apk加固代码实现

前言&#xff1a;上一篇博客已经把Apk加固的思路详细的介绍过了&#xff0c;也开始创建了一个空的demo进行&#xff0c;然后在项目中添加一个代理module&#xff08;解密&#xff0c;和系统源码交互功能&#xff09;和tools工具加密Java library 的module &#xff0c;这里开始…

Android APK加固原理

一、前言 Android作为开源框架&#xff0c;开放之余&#xff0c;所要面临的就是安全问题&#xff0c;世间之事&#xff0c;有正就有邪&#xff0c;有攻就有守&#xff0c;作为开发者虽然不需要进入专业安全领域&#xff0c;但还是需要掌握基本的安全常识和原理。 二、加壳 加…

APK加固原理详解

一、前言 之前使用的360加固&#xff0c;挺好用的&#xff0c;从2021年底的时候限制每天每个账号仅上传2次apk&#xff08;免费的&#xff0c;不知道VIP的是不是这样&#xff09;。通过这个事情&#xff0c;感觉技术还是掌握在自己手里稳妥点&#xff0c;不用受制于人&#xf…

Android中的Apk的加固(加壳)原理解析和实现

本文转载自&#xff1a;Android中的Apk的加固(加壳)原理解析和实现 - roccheung - 博客园 一、前言 今天又到周末了&#xff0c;憋了好久又要出博客了&#xff0c;今天来介绍一下Android中的如何对Apk进行加固的原理。现阶段。我们知道Android中的反编译工作越来越让人操作熟…

浅谈安卓apk加固原理和实现

转载本文需注明出处&#xff1a;微信公众号EAWorld&#xff0c;违者必究。 引言&#xff1a; 在安卓开发中&#xff0c;打包发布是开发的最后一个环节&#xff0c;apk是整个项目的源码和资源的结合体&#xff1b;对于懂点反编译原理的人可以轻松编译出apk的源码资源&#xff0c…

安卓逆向笔记--apk加固

安卓逆向笔记–apk加固 资料来源: 浅谈安卓apk加固原理和实现 Android中的Apk的加固(加壳)原理解析和实现 前两个太老了所以具体代码借鉴下面的 Android Apk加壳技术实战详解 一、apk常见加固方法 (1)代码层级加密–代码混淆 代码混淆是一种常见的加密方式。本质是把工程中原…

imx6ull uboot移植

以下内容来自&#xff1a;正点原子Linux驱动文档 一、简介 uboot移植主要是根据原厂的uboot移植&#xff1a;芯片厂商通常会做一块自己的评估板并发布BSP&#xff0c;当我们需要定制自己的开发板时可以根据自己的需求&#xff08;硬件上的不同&#xff09;&#xff0c;对原厂…

全志V3S嵌入式驱动开发(uboot移植)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 安装了ubuntu操作系统,有了开发板,下面就可以进行我们的开发工作了。第一步,我们要面临的问题就是uboot移植。一般来说,嵌入式的基础环境就是uboot、linux和rootfs。uboot完成一…

2021-09-14 uboot移植开发

引言&#xff1a;最近要改动uboot&#xff0c;实现像微软PC上&#xff0c;u盘一键刷机或手机上安全模式下刷机的操作 专门去好好研究了点uboot的启动过程&#xff1b;以下为总结&#xff1a; 嵌入式系统 微软-PC ——…

iTOP4412 uboot移植教程

好多刚开始学习uboot移植的同学总是觉得uboot好难&#xff0c;但是再难的问题如果把它一步步拆开&#xff0c;一个个解决&#xff0c;问题也就将迎刃而解。做uboot移植&#xff0c;我们首先就得了解uboot的编译流程&#xff0c;这里以在iTOP4412精英版2G内存的板子上移植u-boot…

<Linux开发> -之-系统移植 uboot移植过程详细记录(第二部分)

&#xff1c;Linux开发&#xff1e; -之-系统移植 uboot移植过程详细记录&#xff08;第二部分&#xff09; 第一部分链接&#xff1a;系统移植-之-uboot移植第一部分 第一部分主要讲解了&#xff0c;uboot移植过程中使用的一些工具的安装&#xff0c;以及测试nxp远程uboot&a…

X210开发板(S5PV210芯片)uboot移植DM9000驱动移植

前言 本文是介绍在uboot中如何移植DM9000的驱动&#xff0c;并不深入去讲解DM9000芯片的操作时序和内部寄存器&#xff0c;想要读懂驱动代码要仔细阅读DM9000芯片的数据手册。移植的基础是手里有DM9000芯片可以用的驱动代码&#xff0c;只需要根据开发板中DM9000芯片的接线方式…

嵌入式linux UBoot移植篇

如何在U-boot添加自己的linux板卡并启动呢&#xff1f; uboot 的移植并不是说我们完完全全的从零开始将 uboot 移植到我们现在所使用的开发板或者开发平台上。这个对于我们来说基本是不可能的&#xff0c;这个工作一般是半导体厂商做的&#xff0c; 半导体厂商负责将 uboot 移…

Uboot移植流程

linux-Bootloader&#xff08;Uboot&#xff09;移植流程 前言 最近在做ZigBee的温室大棚项目&#xff0c;将自己学习的过程和经验分享给大家。本文基于linux3.4.39内核版本&#xff0c;s5p6818开发板实现。 1、uboot启动简介 uboot启动的过程比较复杂&#xff0c;这里就只…