首页
社区
课程
招聘
[原创]APP整体加固原理
发表于: 2天前 1243

[原创]APP整体加固原理

2天前
1243

在前两篇文章中,我们完成了一个简单的整体加固 Demo 以及自动化加壳工具,也通过 Android 源码分析了 mClassLoader
的重要性。但回过头来看,这两个话题之间其实存在一段逻辑上的跳跃:我们知道了要这么做,也知道了必须这么做,却还没有系统地理解为什么这么做能生效

仔细想想其实跳过了几个基础但是比较重要的问题:

这些问题导致我对整个加固逻辑只是一知半解,偶然间发现了一些资源课程帮我解决了这一部分的问题,遂写下这篇文章记录下来,希望会有所帮助。

由于笔者水平有限,文中难免存在疏漏或理解不到位的地方,如有错误恳请各位大佬不吝指正,感激不尽。

​ **类加载器(ClassLoader)**是Java/Android虚拟机中负责加载类的组件。这里以Android为例,dex文件本身只是存放类字节码的文件。把一个dex文件复制到App私有目录之后,系统并不会自动识别里面有哪些类,也不会自动将这些类变成可以直接使用的Java对象。

ClassLoader的作用就是告诉虚拟机:应该到哪些dex / apk / jar文件里去找类。

​ 当使用DexClassLoader加载一个外部dex的时候,本质上就是给虚拟机新增了一条类的搜索路径。在加载类的时候,ClassLoader会沿着查找规则,在这些dex路径当中找到对应类的字节码,并且把它定义成运行时的Class对象。

Android类加载器当中有几个比较重要的成员,对于动态加载及整体加固非常重要,分别有下面几个:

DexFile:是Dex文件在运行时的封装,对于类加载器来说,类加载器最终会通过它从dex中查找并加载目标类;对于开发者来说,可以通过这个结构获取到类名列表,dex文件路径,native层的dex句柄

dexElements:是Element类的数组,其中Element这个类包含了DexFile,这个数组决定了类加载器要到哪里找类,以及查找类的顺序

PathListPathList是Android类加载器内部用来保存搜索路径的成员,其中又包含了dexElements这个成员,是BaseDexClassLoader这个类的成员。

这几个成员的包含关系如下:

​ 了解成员结构之后,我们可以写一段代码来验证一下——将某个类加载器当前搜索路径下所有的类打印出来。这样能更直观地看到 ClassLoader 内部到底"管着"哪些类。首先看一下 Android 源码中是怎么获取类名列表的:

在Android源码当中使用的是一个getClassNameList的方法来获取类名列表,那么我们也可以通过反射来调用这个方法来获取类名列表。在得知几个成员关系的情况下,这个大致的方案如下:

代码实现如下:

效果演示:

​ 通过这种方法就能够将类加载器搜索路径中所有的dex/apk/jar文件包含的类打印出来。也就是说通过这个方式能够找到某个ClassLoader当前能够搜索到的类。

JVM 中类加载器分为 BootstrapExtensionApplication 三层,Android 在此基础上做了简化和适配,形成了自己的一套类加载器体系:

搞清楚了类加载器是什么、内部长什么样,下一个问题就是:当系统要找某个类的时候,到底按照什么规则在多个 ClassLoader 之间查找? 这就涉及到双亲委派机制了。

​ 双亲委派是类加载器(classLoader)加载类时的一种查找规则。这里使用JVM当中的双亲委派图进行解析,这个查找规则大致如下:

这里配合Android源码来阅读能更快理解这个过程,这里找到loadClass的源码:

双亲委派机制的优点:

​ java当中也提供了API来获取ClassLoader委派的父ClassLoader,这里可以通过代码来打印一下委派的关系链:

效果展示:

通过这个方法能够很清楚的看到委派链是:PathClassLoader->BootClassLoader

这个位于委派链最前端的 PathClassLoader,其实就是前两篇文章中反复出现的 mClassLoader——App 进程中默认的类加载器。系统所有 "找类" 的操作都从它开始,然后沿着委派链逐级向上委托。理解了这一点,就能明白为什么加固必须对它下手:我们自己创建的 DexClassLoader 不在这个委派链上,系统根本不会去问它。 接下来我们通过实战来验证这个问题。

简单回顾一下:mClassLoader 这个默认类加载器存储在 LoadedApk 中,而 LoadedApk 又存放在 ActivityThreadmPackages 这个 ArrayMap 成员里:


mPackages包名为键,对应的值是一个 WeakReference,引用的就是 LoadedApk——它包含了 mClassLoader 这个默认类加载器。

也就是说,要修改 mClassLoader,需要先通过反射拿到 ActivityThread,再获取 LoadedApk,最后修改其中的 mClassLoader 字段。ActivityThread 可以通过反射调用其静态方法 currentActivityThread 直接获取。

这里重新创建一个新的项目,包括普通类,以及一个继承activity的组件类:

TestClass

TestActivity

由于高版本的Android无法读sdcard的内容,这里为了方便加载期间,build之后将相关的dex文件丢到测试项目的assets目录当中:

并且还需要封装一个从assets目录当中,使用dexClassLoader加载dex的方法:

此外为了让当前项目能够启动这个TestActivity,还需要在Manifest清单文件当中的<activity>标签当中加上这个:

首先动态加载一下TestClass这个普通的类,然后通过反射调用一下这个类当中的testfunc函数,代码如下:

可以看到这里成功执行了这个testfunc,没有任何异常。

了解了动态加载dex,并且使用反射调用内部方法的手法后。接着来看看如果仅仅使用上面的步骤来动态加载组件,并启动组件的话会出现什么问题。

这里需要使用context.startActivity来启动Activity,而不是反射。

通过logcat可以看到这个dexclassloader是能够成功loadClass,但是这里使用startActivity来启动Activity却抛出了ClassNotFound的异常。说明当前的类加载器无法找到这个com.example.test01.TestActivity

这是为什么呢?这里用printClassLoaderHierarchy来看看委派链,传入的参数是dexclassloader

发现只有在dexClassLoader这个类加载器当中才有test.dex这个访问路径。

​ 通过观察委派链输出的ClassLoader和报错信息的ClassLoader,可以发现这两个类加载器是一样的,那么说明这个app默认用来查找类的classloaderpathClassloader

​ 通过这个大概能够知道出现这个问题的其实是:动态加载的dex文件的类加载器是DexClassLoader,其委派的加载器是pathClassLoader。而默认类加载器是pathClassLoader,通过双亲委派查找类的时候,只能查找pathClassLoader ~ BootClassLoader这个范围,而无法通过dexClassLoader查找,所以会出现ClassNotFound这个异常。大概如下图所示:

那么现在已经知道了是大概是因为这个双亲委派机制无法通过dexClassLoader找到这个类。那么我们只需要将dexClassLoader添加到这个双亲委派能够查找到的范围就可以了,这里提供三个方法,它们的本质都是让系统能从默认的查找路径上找到外部 dex 中的类

​ 获取到LoadedApk,然后使用反射修改mClassLoader,改成DexClassLoader即可,大致过程可以参考下图:

可以看到成功执行了onCreate了。

这就是第一篇文章中壳代码的核心逻辑——在 attachBaseContext 中替换 mClassLoader,让系统后续找类时沿着新的 ClassLoader 去加载我们解密出来的 dex。

将dexClassLoader插入到pathClassLoader和bootClassLoader当中

可以看到此时的委派链是pathClassLoader -> DexClassLoader -> BootClassLoader,而且能够正常启动Activity,并执行onCreate函数。

相比方法一直接替换 mClassLoader,这种方法保留了原来的 PathClassLoader 在委派链顶端,改动更小、更隐蔽,是另一种可行的加固思路。

​ 上面两种方法都是从修改委派链路出发,那么还有没有别的方法呢?回想一下 0x01 中介绍的 dexElements——这个数组决定了类加载器要到哪里找类以及查找顺序。如果我们将 DexClassLoaderdexElements 合并到 PathClassLoaderdexElements 中,PathClassLoader 就能直接搜到我们外部加载的 dex 了。

​ 而这正是常说的热修复原理:将补丁 dex 加载后插入到 dexElements 头部,加载类时就会优先命中补丁中的类。

​ 这里通过源码看看为什么会优先加载补丁dex

可以看到这里是通过遍历dexElements当中的每一项来findClass的,只要找到了就直接返回class。这就是为什么能够优先加载补丁后的dex

那么知道了原理就来写一下代码

首先要来封装几个工具方法:


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2天前 被x0rrrrr编辑 ,原因:
上传的附件:
收藏
免费 35
打赏
分享
最新回复 (16)
雪    币: 2988
活跃值: (2490)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
2
1
2天前
0
雪    币: 158
活跃值: (4851)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
学习。
2天前
0
雪    币: 192
活跃值: (268)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
1
2天前
0
雪    币: 2448
活跃值: (8126)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
谢谢分享
2天前
0
雪    币: 732
活跃值: (3227)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
6
1
2天前
0
雪    币: 5457
活跃值: (3235)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
谢谢分享
2天前
0
雪    币: 3729
活跃值: (4884)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
过来瞧瞧
2天前
0
雪    币: 901
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
Tql
2天前
0
雪    币: 156
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
看看
2天前
0
雪    币: 139
活跃值: (370)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
看看原理
1天前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
看看
1天前
0
雪    币: 530
活跃值: (2423)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
666
1天前
0
雪    币: 202
活跃值: (363)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
1
1天前
0
雪    币: 375
活跃值: (3917)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
15
学习一下
1天前
0
雪    币: 6214
活跃值: (11172)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
可以复习
1天前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
17
666
1天前
0
游客
登录 | 注册 方可回帖
返回