Android Apk加固原理解析 您所在的位置:网站首页 加固工具apk Android Apk加固原理解析

Android Apk加固原理解析

2024-07-11 22:40| 来源: 网络整理| 查看: 265

前言 为什么要加固

对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运行起来最先执行的方法)方法,主要做如下几件事:

dex文件解密;反射加载解密后的dex文件;反射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 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 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 mAllApplications = (ArrayList) 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) { 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_PATH和SHELL_APK_PATH对应目录,执行Main方法,最终产物如图: 最终产物 我们解压apk-signed.apk,点开dex文件,可以看到如图: 加密apk 证明我们dex加密成功!!

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

代码链接

代码链接

结语

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



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有