之前,我们完成了一个简单的整体加固 Demo 以及自动化加壳工具,也通过 Android 源码分析了 mClassLoader的重要性。但回过头来看,这两个话题之间其实存在一段逻辑上的跳跃:我们知道了要这么做,也知道了必须这么做,却还没有系统地理解为什么这么做能生效。
仔细想想其实跳过了几个基础但是比较重要的问题:
- mClassLoader 到底是什么?
它是 PathClassLoader还是DexClassLoader?它的内部结构长什么样? - 为什么系统要通过它来找类?
它的查找规则是什么?为什么我们自己 new 一个 DexClassLoader加载dex,系统却不认? - 除了替换
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这个类的成员。
这几个成员的包含关系如下:
BaseDexClassLoader-> PathList : DexPathList-> dexElements : Element[]-> dexFile : DexFile验证:通过反射获取Dex类名列表
了解成员结构之后,我们可以写一段代码来验证一下——将某个类加载器当前搜索路径下所有的类打印出来。这样能更直观地看到 ClassLoader 内部到底"管着"哪些类。
首先看一下 Android 源码中是怎么获取类名列表的:
在Android源码当中使用的是一个getClassNameList的方法来获取类名列表,那么我们也可以通过反射来调用这个方法来获取类名列表。
在得知几个成员关系的情况下,这个大致的方案如下:
首先需要通过反射来获取某个类加载器的 pathList使用反射获取 pathList中的dexElements遍历 dexElements数组,获取每个项中的dexFile,然后通过反射调用dexFile中的getClassNameList方法来获取类名列表
代码实现如下:
publicstaticvoidprintClassLoaderClasses(ClassLoader classLoader){ myLogD("classLoader -> " + classLoader);try{// 获取pathlistClassbasedexclassLoader= classLoader.loadClass("dalvik.system.BaseDexClassLoader");Fieldpathlistfield= basedexclassLoader.getDeclaredField("pathList"); pathlistfield.setAccessible(true);Objectpathlistobj= pathlistfield.get(classLoader);// 获取dexElementsClassdexpathlistClass= classLoader.loadClass("dalvik.system.DexPathList");FielddexElementsfield= dexpathlistClass.getDeclaredField("dexElements"); dexElementsfield.setAccessible(true); Object[] dexElementsobj = (Object[]) dexElementsfield.get(pathlistobj);// 获取dexFileClasselementClass= classLoader.loadClass("dalvik.system.DexPathList$Element");FielddexfileField= elementClass.getDeclaredField("dexFile"); dexfileField.setAccessible(true);// mCookie fieldClassdexfileClass= classLoader.loadClass("dalvik.system.DexFile");FieldmCookiefield= dexfileClass.getDeclaredField("mCookie"); mCookiefield.setAccessible(true);// 获取method Method[] methods = dexfileClass.getDeclaredMethods();MethodgetClassNameListmethod=null;for(Method method : methods){if(method.getName().equals("getClassNameList")){ getClassNameListmethod = method; getClassNameListmethod.setAccessible(true);break; } }for(Object element : dexElementsobj){Objectdexfileobj= dexfileField.get(element);ObjectmCookieobj= mCookiefield.get(dexfileobj); String[] classlist = (String[]) getClassNameListmethod.invoke(dexfileobj, mCookieobj);for(String className : classlist){ myLogI("classname : " + className); } } } catch (NoSuchFieldException e) {thrownewRuntimeException(e); } catch (ClassNotFoundException e) {thrownewRuntimeException(e); } catch (IllegalAccessException e) {thrownewRuntimeException(e); } catch (InvocationTargetException e) {thrownewRuntimeException(e); }}效果演示:
通过这种方法就能够将类加载器搜索路径中所有的dex/apk/jar文件包含的类打印出来。也就是说通过这个方式能够找到某个ClassLoader当前能够搜索到的类。
类加载器的类型
JVM 中类加载器分为 Bootstrap → Extension → Application 三层,Android 在此基础上做了简化和适配,形成了自己的一套类加载器体系:
Android 类加载器
ClassLoader是一个抽象类,定义了 ClassLoader的主要功能BootClassLoader继承自 ClassLoader,用于Android系统启动时预加载常用类.SecureClassLoader继承了 ClassLoader,但并不是实现类,而是拓展并加入了权限管理功能,增强安全性URLClassLoader通过 URL路径从jar文件和文件夹中加载类和资源BaseDexClassLoader继承自 ClassLoader,是抽象类ClassLoader的具体实现类.InMemoryDexClassLoaderAndroid8.0新增,继承自 BaseDexClassLoader,用于加载内存中的dex文件。PathClassLoader继承自 BaseDexClassLoader,用于加载已安装的apk的dex文件.DexClassLoader继承自 BaseDexClassLoader,用于加载已安装的apk的dex文件,以及从SD卡中加载未安装的apk的dex文件.
搞清楚了类加载器是什么、内部长什么样,下一个问题就是:当系统要找某个类的时候,到底按照什么规则在多个 ClassLoader 之间查找? 这就涉及到双亲委派机制了。

二、双亲委派机制
什么是双亲委派
双亲委派是类加载器(classLoader)加载类时的一种查找规则。这里使用JVM当中的双亲委派图进行解析,这个查找规则大致如下:
先判断这个 Class是否已经被加载,如果没有则先委派给父类加载器查找,并依次递归到顶层的类加载器如果顶层类加载器找到了,则返回该 Class,否则依次往下查找如果所有父加载器都没找到Class则自己进行查找,如果仍查找失败则抛出 classNotFound异常
这里配合Android源码来阅读能更快理解这个过程,这里找到loadClass的源码:
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {// 1. 先检查当前 ClassLoader 是否已经加载过这个类 Class<?> c = findLoadedClass(name);if (c == null) {// 2. 如果没有加载过,先交给父类加载器加载try {if (parent != null) { c = parent.loadClass(name, false); } else {// 3. 如果没有父加载器,就交给 BootstrapClassLoader c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) {// 父加载器找不到时,不直接抛出// 而是继续让当前 ClassLoader 自己找 }// 4. 父加载器也找不到,当前 ClassLoader 才调用 findClass 自己查找if (c == null) { c = findClass(name); } }// 5. 返回最终找到的 Class 对象return c;}双亲委派机制的优点:
避免重复加载,如果已经加载过一次 Class,可以直接读取已经加载的Class提高安全性,无法使用自定义类来替换系统类,可以防止核心API库被随意篡改。
通过代码打印ClassLoader委派链
java当中也提供了API来获取ClassLoader委派的父ClassLoader,这里可以通过代码来打印一下委派的关系链:
public static void printClassLoaderHierarchy(ClassLoader currentClassloader){int level = 0; StringBuffer sb = new StringBuffer(); while(currentClassloader != null){for(int i = 0;i < level; i++){ sb.append(" "); } sb.append("[") .append(level) .append("] ") .append(currentClassloader.getClass().getName()) .append(" -> ") .append(currentClassloader) .append("\n"); currentClassloader = currentClassloader.getParent(); level++; } myLogD(sb.toString());}效果展示:
通过这个方法能够很清楚的看到委派链是:PathClassLoader->BootClassLoader。
这个位于委派链最前端的 PathClassLoader,其实就是前两篇文章中反复出现的 mClassLoader——App 进程中默认的类加载器。系统所有 "找类" 的操作都从它开始,然后沿着委派链逐级向上委托。理解了这一点,就能明白为什么加固必须对它下手:我们自己创建的 DexClassLoader 不在这个委派链上,系统根本不会去问它。 接下来我们通过实战来验证这个问题。

三、动态加载Dex
mClassLoader 在源码中的位置
简单回顾一下:mClassLoader 这个默认类加载器存储在 LoadedApk 中,而 LoadedApk 又存放在 ActivityThread 的 mPackages 这个 ArrayMap 成员里:
mPackages 以包名为键,对应的值是一个 WeakReference,引用的就是 LoadedApk——它包含了 mClassLoader 这个默认类加载器。
也就是说,要修改 mClassLoader,需要先通过反射拿到 ActivityThread,再获取 LoadedApk,最后修改其中的 mClassLoader 字段。ActivityThread 可以通过反射调用其静态方法 currentActivityThread 直接获取。
测试环境准备
这里重新创建一个新的项目,包括普通类,以及一个继承activity的组件类:
TestClass:publicclassTestClass {publicstaticvoidtestfunc(){ Log.i("[testLoaded]","[com.example.test01.TestClass] testfunc is calling..."); }}TestActivity:publicclassTestActivityextendsActivity {@OverrideprotectedvoidonCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState); Log.i("[testLoaded]","[com.example.test01.TestActivity] onCreate is calling..."); }}
由于高版本的Android无法读sdcard的内容,这里为了方便加载期间,build之后将相关的dex文件丢到测试项目的assets目录当中:
并且还需要封装一个从assets目录当中,使用dexClassLoader加载dex的方法:
privatestatic ClassLoader loadDexFromAssest(Context context, String dexName, ClassLoader parentClassLoader){FiledexFile=newFile(context.getFilesDir(),dexName);if(!dexFile.exists()){try(InputStreamis= context.getAssets().open(dexName); FileOutputStreamfos=newFileOutputStream(dexFile)){byte[] buffer = newbyte[4096];int len;while((len = is.read(buffer)) != -1){ fos.write(buffer, 0, len); } } catch (FileNotFoundException e) {thrownewRuntimeException(e); } catch (IOException e) {thrownewRuntimeException(e); } }FileoptimizedDir= context.getCodeCacheDir();returnnewDexClassLoader(dexFile.getAbsolutePath(), optimizedDir.getAbsolutePath(),null, parentClassLoader);}此外为了让当前项目能够启动这个TestActivity,还需要在Manifest清单文件当中的<activity>标签当中加上这个:
<activityandroid:name="com.example.test01.TestActivity"></activity>动态加载普通类
首先动态加载一下TestClass这个普通的类,然后通过反射调用一下这个类当中的testfunc函数,代码如下:
publicstaticvoidloadedNomalClass(Context context){ ClassLoader pathClassloader = MainActivity.class.getClassLoader(); dexclassloader = loadDexFromAssest(context, "test.dex", pathClassloader);try{ Class testClass = dexclassloader.loadClass("com.example.test01.TestClass"); Method testfunc = testClass.getDeclaredMethod("testfunc"); testfunc.setAccessible(true); testfunc.invoke(null); } catch (ClassNotFoundException e) {thrownewRuntimeException(e); } catch (InvocationTargetException e) {thrownewRuntimeException(e); } catch (NoSuchMethodException e) {thrownewRuntimeException(e); } catch (IllegalAccessException e) {thrownewRuntimeException(e); }}可以看到这里成功执行了这个testfunc,没有任何异常。
动态加载组件
动态加载组件类的问题
了解了动态加载dex,并且使用反射调用内部方法的手法后。接着来看看如果仅仅使用上面的步骤来动态加载组件,并启动组件的话会出现什么问题。
publicstaticvoidstartActivityFirstMethod(Context context){ClassLoaderpathClassloader= MainActivity.class.getClassLoader(); dexclassloader = loadDexFromAssest(context, "test.dex", pathClassloader);try{ClassTestActivity= dexclassloader.loadClass("com.example.test01.TestActivity"); myLogD("Class : " + TestActivity); context.startActivity(newIntent(context, TestActivity)); } catch (ClassNotFoundException e) {thrownewRuntimeException(e); }}这里需要使用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 中的类:
- 替换默认类加载器
— 将 mClassLoader直接设置为DexClassLoader(这正是第一篇文章中壳代码的做法) - 插入委派链
— 将 DexClassLoader插入到PathClassLoader与BootClassLoader之间 - 合并 dexElements
— 将 DexClassLoader的dexElements合并到PathClassLoader的dexElements中(也就是常说的"热修复"方案)
方法一
获取到LoadedApk,然后使用反射修改mClassLoader,改成DexClassLoader即可,大致过程可以参考下图:
代码示例:
publicstaticvoidstartActivityFirstMethod(Context context){ClassLoaderpathClassloader= MainActivity.class.getClassLoader(); dexclassloader = loadDexFromAssest(context, "test.dex", pathClassloader);try{// 通过currentActivityThread获取 ActivityThreaObjClassactivityThreadclass= pathClassloader.loadClass("android.app.ActivityThread");MethodcurrentActivityThreadmethod= activityThreadclass.getDeclaredMethod("currentActivityThread"); currentActivityThreadmethod.setAccessible(true);ObjectcurrentactivityThreadobj= currentActivityThreadmethod.invoke(null);// 获取mPackagesFieldmPackagesfield= activityThreadclass.getDeclaredField("mPackages"); mPackagesfield.setAccessible(true);ArrayMapmPackagesobj= (ArrayMap) mPackagesfield.get(currentactivityThreadobj);if(appcontext != null){Stringpackagename= appcontext.getPackageName();WeakReferencewr= (WeakReference) mPackagesobj.get(packagename);ObjectloadedapkObj= (Object) wr.get();ClassloadedApkClass= pathClassloader.loadClass("android.app.LoadedApk");FieldmClassLoaderField= loadedApkClass.getDeclaredField("mClassLoader"); mClassLoaderField.setAccessible(true);// 打印mClassLoader变化ClassLoaderprevClassloader= (ClassLoader) mClassLoaderField.get(loadedapkObj); myLogD("prev classloader -> " + prevClassloader); mClassLoaderField.set(loadedapkObj,dexclassloader); myLogD("curr classloader -> " + dexclassloader); } } catch (ClassNotFoundException e) {thrownewRuntimeException(e); } catch (InvocationTargetException e) {thrownewRuntimeException(e); } catch (NoSuchMethodException e) {thrownewRuntimeException(e); } catch (IllegalAccessException e) {thrownewRuntimeException(e); } catch (NoSuchFieldException e) {thrownewRuntimeException(e); }try{ClassTestActivity= dexclassloader.loadClass("com.example.test01.TestActivity"); myLogD("Class : " + TestActivity);// printClassLoaderHierarchy(dexclassloader); context.startActivity(newIntent(context, TestActivity)); } catch (ClassNotFoundException e) {thrownewRuntimeException(e); }}可以看到成功执行了onCreate了。
这就是第一篇文章中壳代码的核心逻辑——在 attachBaseContext 中替换 mClassLoader,让系统后续找类时沿着新的 ClassLoader 去加载我们解密出来的 dex。
方法二
将dexClassLoader插入到pathClassLoader和bootClassLoader当中
publicstaticvoidstartActivitySecondMethod(Context context){ClassLoaderpathClassloader= MainActivity.class.getClassLoader();ClassLoaderbootClassloader= pathClassloader.getParent(); dexclassloader = loadDexFromAssest(context, "test.dex", bootClassloader);try{Fieldparentfield= ClassLoader.class.getDeclaredField("parent"); parentfield.setAccessible(true); parentfield.set(pathClassloader, dexclassloader); } catch (NoSuchFieldException e) {thrownewRuntimeException(e); } catch (IllegalAccessException e) {thrownewRuntimeException(e); }// 打印委派链 printClassLoaderHierarchy(pathClassloader);try{ClassTestActivityClass= dexclassloader.loadClass("com.example.test01.TestActivity"); myLogE(TestActivityClass.toString()); context.startActivity(newIntent(context, TestActivityClass)); } catch (ClassNotFoundException e) {thrownewRuntimeException(e); }}可以看到此时的委派链是pathClassLoader -> DexClassLoader -> BootClassLoader,而且能够正常启动Activity,并执行onCreate函数。
相比方法一直接替换 mClassLoader,这种方法保留了原来的 PathClassLoader 在委派链顶端,改动更小、更隐蔽,是另一种可行的加固思路。
方法三:合并 dexElements(热修复方案)
上面两种方法都是从修改委派链路出发,那么还有没有别的方法呢?回想一下 0x01 中介绍的 dexElements——这个数组决定了类加载器要到哪里找类以及查找顺序。如果我们将 DexClassLoader 的 dexElements 合并到 PathClassLoader 的 dexElements 中,PathClassLoader 就能直接搜到我们外部加载的 dex 了。
而这正是常说的热修复原理:将补丁 dex 加载后插入到 dexElements 头部,加载类时就会优先命中补丁中的类。
这里通过源码看看为什么会优先加载补丁dex:
可以看到这里是通过遍历dexElements当中的每一项来findClass的,只要找到了就直接返回class。这就是为什么能够优先加载补丁后的dex。
代码示例
那么知道了原理就来写一下代码。
首先要来封装几个工具方法:
从 ClassLoader中获取dexElements:
publicstatic Object getDexElementsInClassLoader(ClassLoader classLoader){try{ClassBaseDexClassLoaderClass= classLoader.loadClass("dalvik.system.BaseDexClassLoader");FieldpathListField= BaseDexClassLoaderClass.getDeclaredField("pathList"); pathListField.setAccessible(true);ObjectpathListObj= pathListField.get(classLoader);ClassDexPathListClass= classLoader.loadClass("dalvik.system.DexPathList");FielddexElementsField= DexPathListClass.getDeclaredField("dexElements"); dexElementsField.setAccessible(true);ObjectdexElementsObj= dexElementsField.get(pathListObj);return dexElementsObj; } catch (ClassNotFoundException e) {thrownewRuntimeException(e); } catch (NoSuchFieldException e) {thrownewRuntimeException(e); } catch (IllegalAccessException e) {thrownewRuntimeException(e); }}为 ClassLoader设置dexElements:
publicstaticvoidsetDexElementsInCloassLoader(ClassLoader classLoader, Object dexElements){try{ClassBaseDexClassLoaderClass= classLoader.loadClass("dalvik.system.BaseDexClassLoader");FieldpathListField= BaseDexClassLoaderClass.getDeclaredField("pathList"); pathListField.setAccessible(true);ObjectpathlistObj= pathListField.get(classLoader);ClassDexPathListClass= classLoader.loadClass("dalvik.system.DexPathList");FielddexElementsField= DexPathListClass.getDeclaredField("dexElements"); dexElementsField.setAccessible(true); dexElementsField.set(pathlistObj, dexElements); } catch (NoSuchFieldException e) {thrownewRuntimeException(e); } catch (ClassNotFoundException e) {thrownewRuntimeException(e); } catch (IllegalAccessException e) {thrownewRuntimeException(e); }}合并 dexElements:
publicstaticObjectcombineDexElements(Object element1, Object element2){Object result = null; int len1 = Array.getLength(element1); int len2 = Array.getLength(element2); int newlen = len1+len2; result = Array.newInstance(element1.getClass().getComponentType(), newlen);for(int i = 0; i < newlen; i++){if(i < len2){Array.set(result,i,Array.get(element2, i)); }else{Array.set(result, i, Array.get(element1, i - len2)); } }myLogD("newDexElements's len : "+newlen);return result;}- 启动函数:
publicstaticvoidstartActivityThirdMethod(Context context){ClassLoaderpathClassLoader= MainActivity.class.getClassLoader(); dexclassloader = loadDexFromAssest(context, "test.dex", pathClassLoader);ObjectdexElements1= getDexElementsInClassLoader(pathClassLoader);ObjectdexElements2= getDexElementsInClassLoader(dexclassloader);ObjectnewdexElements= combineDexElements(dexElements1, dexElements2); setDexElementsInCloassLoader(pathClassLoader, newdexElements); printClassLoaderHierarchy(pathClassLoader);try{ClassTestActivityClass= pathClassLoader.loadClass("com.example.test01.TestActivity"); myLogE(TestActivityClass.toString()); context.startActivity(newIntent(context, TestActivityClass)); } catch (ClassNotFoundException e) {thrownewRuntimeException(e); }}合并之后可以看到pathClassLoader当中有test.dex,并且onCreate也可以正常执行了。
这种方法不需要修改委派链,只需要操作 dexElements 数组。它既是热修复的底层原理,也可以作为加固的一种实现方式——将被保护的 dex 合并进 mClassLoader 的搜索路径中,系统自然就能找到它了。

四、总结
回看前言的三个问题,到了这里,前言中抛出的三个问题已经有了明确的答案:
1. mClassLoader 到底是什么?
它是 PathClassLoader,存储在 LoadedApk 中,是 App 进程的默认类加载器。它的内部结构是 PathList → dexElements[] → DexFile,dexElements 数组决定了它能搜索哪些 dex 文件。
2. 为什么系统要通过它来找类?
因为双亲委派机制。系统找类时从 mClassLoader(即 PathClassLoader)出发,沿着 PathClassLoader → BootClassLoader 这条委派链逐级向上查找。我们自己 new 的 DexClassLoader 不在这个链上,所以 startActivity 时系统压根不会去问它——这就是 ClassNotFound 的根因。
3. 除了替换 mClassLoader,还有没有别的方案?
有。本文展示了三种方案,它们的共同目标是——让系统能从默认的查找路径上找到外部 dex 中的类:

整体加固的原理
整体加固的原理,说到底就是类加载器的接管。壳代码在 attachBaseContext 这个最早的执行时机,解密原始 dex,然后通过操纵类加载器(替换、插入或合并),让系统在后续启动 Application、Activity 等组件时,能够找到隐藏在外部 dex 中的真正代码。三种方案路径不同,但终点一致——把解密后的 dex "挂"到系统能看见的地方。

看雪ID:x0rrrrr
https://bbs.kanxue.com/user-home-999665.htm

# 往期推荐

球分享

球点赞

球在看

点击阅读原文查看更多
夜雨聆风