在前两篇文章中,我们完成了一个简单的整体加固 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,这个数组决定了类加载器要到哪里找类,以及查找类的顺序
PathList:PathList是Android类加载器内部用来保存搜索路径的成员,其中又包含了dexElements这个成员,是BaseDexClassLoader这个类的成员。
这几个成员的包含关系如下:
了解成员结构之后,我们可以写一段代码来验证一下——将某个类加载器当前搜索路径下所有的类打印出来。这样能更直观地看到 ClassLoader 内部到底"管着"哪些类。首先看一下 Android 源码中是怎么获取类名列表的:

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

通过这种方法就能够将类加载器搜索路径中所有的dex/apk/jar文件包含的类打印出来。也就是说通过这个方式能够找到某个ClassLoader当前能够搜索到的类。
JVM 中类加载器分为 Bootstrap → Extension → Application 三层,Android 在此基础上做了简化和适配,形成了自己的一套类加载器体系:

搞清楚了类加载器是什么、内部长什么样,下一个问题就是:当系统要找某个类的时候,到底按照什么规则在多个 ClassLoader 之间查找? 这就涉及到双亲委派机制了。
双亲委派是类加载器(classLoader)加载类时的一种查找规则。这里使用JVM当中的双亲委派图进行解析,这个查找规则大致如下:

这里配合Android源码来阅读能更快理解这个过程,这里找到loadClass的源码:
双亲委派机制的优点:
java当中也提供了API来获取ClassLoader委派的父ClassLoader,这里可以通过代码来打印一下委派的关系链:
效果展示:

通过这个方法能够很清楚的看到委派链是:PathClassLoader->BootClassLoader。
这个位于委派链最前端的 PathClassLoader,其实就是前两篇文章中反复出现的 mClassLoader——App 进程中默认的类加载器。系统所有 "找类" 的操作都从它开始,然后沿着委派链逐级向上委托。理解了这一点,就能明白为什么加固必须对它下手:我们自己创建的 DexClassLoader 不在这个委派链上,系统根本不会去问它。 接下来我们通过实战来验证这个问题。
简单回顾一下:mClassLoader 这个默认类加载器存储在 LoadedApk 中,而 LoadedApk 又存放在 ActivityThread 的 mPackages 这个 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默认用来查找类的classloader是pathClassloader。
通过这个大概能够知道出现这个问题的其实是:动态加载的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——这个数组决定了类加载器要到哪里找类以及查找顺序。如果我们将 DexClassLoader 的 dexElements 合并到 PathClassLoader 的 dexElements 中,PathClassLoader 就能直接搜到我们外部加载的 dex 了。
而这正是常说的热修复原理:将补丁 dex 加载后插入到 dexElements 头部,加载类时就会优先命中补丁中的类。
这里通过源码看看为什么会优先加载补丁dex:

可以看到这里是通过遍历dexElements当中的每一项来findClass的,只要找到了就直接返回class。这就是为什么能够优先加载补丁后的dex。
那么知道了原理就来写一下代码
首先要来封装几个工具方法:
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2天前
被x0rrrrr编辑
,原因: