ClassLoader 机制
一、前言
几乎所有主流的 Android 加固方案都离不开 ClassLoader 机制,那么想进一步学习理解 Android 加固自然就得较为系统的学习一下这块知识了。刚好看雪上有位大佬系统的讲解了 Android 加固各代壳从入门到入土,在入门就讲解了这部分内容,记录一下。
二、开始
热修复和插件化技术依赖于 ClassLoader,JVM 虚拟机运行class 文件,而 Dalvik/ART 运行 dex 文件,所以它们的ClassLoader 有部分区别。
Java 中的 ClassLoader
Java的ClassLoader分为两种:
- 系统类加载器
BootstrapClassLoader, ExtensionsClassLoader, ApplicationClassLoader - 自定义类加载器
Custom ClassLoader, 通过继承 java.lang.ClassLoader 实现
具体分析如下:
- Bootstrap ClassLoader (引导类加载器)
是使用 C/C++ 实现的加载器(不能被 Java 代码访问),用于加载 JDK 的核心类库,例如 java.lang 和 java.util 等系统类;会加载 JAVA_HOME/jre/lib 和 -Xbootclasspath 参数指定的目录;JVM 虚拟机的启动就是通过 Bootstrap ClassLoader 创建的初始类完成的。
可以通过如下代码得出其加载的目录(java8)
public class Test0 {
public static void main(String[] args) {
System.out.println(System.getProperty("sun.boot.class.path"));
}
}
效果如下
- Extensions ClassLoader (拓展类加载器)
该类在 Java 中的实现类为 ExtClassLoader,用于加载 java的拓展类,提供除系统类之外的额外功能;用于加载JAVA_HOME/jre/lib/ext 和 java.ext.dir 指定的目录。
以下代码可以打印ExtClassLoader的加载目录:
public class JavaClassLoaderTest {
public static void main(String[] args) {
System.out.println(System.getProperty(java.ext.dirs));
}
}
Application ClassLoader (应用程序类加载器)
对应的实现类为 AppClassLoader,又可以称作 System ClassLoader(系统类加载器),因为它可以通过ClassLoader.getSystemClassLoader() 方法获取;用于加载程序的 Classpath 目录和系统属性 java.class.path 指定的目录。Custom ClassLoader (自定义加载器)
除了以上3个系统提供的类加载器之外,还可以通过继承java.lang.ClassLoader 实现自定义类加载器;Extensions 和 Application ClassLoader 也继承了该类。
ClassLoader 的继承关系
运行一个Java程序需要几种类型的类加载器呢?可以使用如下代码验证
public class JavaClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader=JavaClassLoaderTest.class.getClassLoader();
while(loader!=null){
System.out.println(loader);
loader=loader.getParent();
}
}
}
打印出了 AppClassLoader 和 ExtClassLoader,由于BootstrapClassLoader 由 C/C++ 编写,并非Java类,所以无法在 Java 中获取它的引用。
注意:
1. 系统提供的类加载器有 3 种类型,但是系统提供的 ClassLoader 不止 3 个;
1. **AppClassLoader** 的**父类加载器**为 **ExtClassLoader**,不代表 AppClassLoader 继承自ExtClassLoader(因为这里所说的 **父类加载器** 指的是 **委派关系**,而并非 **继承关系**)。
ClassLoader 的 继承关系 如下图所示:
1. ClassLoader 是一个**抽象类**,定义了ClassLoader 的主要功能;
2. **SecureClassLoader** 继承自 ClassLoader ,但并不是 ClassLoader 的**实现类**,而是拓展并加入了权限管理方面的功能,增强了安全性;
3. **URLClassLoader** **继承**自 SecureClassLoader 可通过 URL 路径从 jar 文件和文件夹中加载类和资源;
4. **ExtClassLoader** 和 **AppClassLoader** 都**继承**自 URLClassLoader
它们都是 Launcher 的内部类,而 Launcher 是 JVM 虚拟机的入口应用,所以ExtClassLoader 和 AppClassLoader 都在 Launcher 中初始化。
ClassLoader的双亲委托机制
类加载器查找 Class 采用了双亲委托模式:
1. 先判断该 Class 是否加载,如果没有则先**委托父类加载器查找**,并依次递归直到顶层的Bootstrap ClassLoader;
1. 如果 Bootstrap ClassLoader 找到了则返回该 Class,否则依次向下查找;
1. 如果所有父类都没找到 Class 则调用自身 findClass 进行查找。
双亲委托机制的优点:
1. **避免重复加载**。如果已经加载过Class则无需重复加载,只需要读取加载的Class即可;
1. **更加安全**。保证无法使用自定义的类替代系统类,并且只有两个类名一致且被同一个加载器加载的类才会被认为是同一个类。
ClassLoader.loadClass方法源码如下(Java17)
1. 注释 1 处检查传入的类是否被加载, 如果已经加载则不执行后续代码;
2. 注释 2 处若父类加载器**不为 null** 则调用父类 loadClass 方法加载 Class;
3. 注释 3 处如果父类加载器**为 null** 则调用 **findBootstrapClassOrNull** 方法;
该方法内部调用了 **Native 方法 findBootstrapClass**,最终用Bootstrap ClassLoader检查该类是否被加载。
注意: 在 Android中 ,该方法直接返回 null,因为 Android 中没有 BootstrapClassLoader
4. 注释 4 处调用自身的 findClass 查找类。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);//1
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);//2.
} else {
c = findBootstrapClassOrNull(name);//3
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);//4
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
以上流程示意图如下
Android 中的 ClassLoader
Java 中的 ClassLoader 可以加载 jar 和 class 文件(本质都是加载class文件);而在 Android 中,无论 DVM 还是 ART 加载的文件都是 dex 文件,所以需要重新设计 ClassLoader 的相关类。
Android 中的 ClassLoader 分为系统类加载器和自定义加载器:
- 系统类加载器
包括 BootClassLoader, PathClassLoader, DexClassLoader等 - 自定义加载器
通过继承 BaseDexClassLoader 实现,它们的继承关系如图所示:
各个ClassLoader的作用:
1. ClassLoader:**抽象类**,定义了 ClassLoader 的主要功能;
2. BootClassLoader:继承自 ClassLoader,用于 Android 系统启动时**预加载常用类**;
3. SecureClassLoader:继承自ClassLoader,**扩展了类权限方面的功能**,加强了安全性;
4. URLClassLoader:继承自 SecureClassLoader,用于**通过 URL 路径加载类和资源**;
5. BaseDexClassLoader:继承自 ClassLoader,**是抽象类 ClassLoader 的具体实现类**;
6. InMemoryDexClassLoader(Android8.0新增):继承自 BaseDexClassLoader,用于**加载内存中的dex文件**;
7. PathClassLoader:继承自 BaseDexClassLoader,用于**加载已安装的 APK 的 dex 文件;**
8. DexClassLoader:继承自 BaseDexClassLoader,**用于加载已安装的 APK 的 dex 文件,以及从 SD 卡中加载未安装的 APK 的 dex 文件**。
实现 Android 加固时,壳程序动态加载被保护程序的 dex 文件主要使用以下 3 个类加载器:
1. **DexClassLoader** 可以加载未安装apk的dex文件
它是一代加固——整体加固(落地加载)的核心之一;
2. **InMemoryDexClassLoader** 可以加载内存中的dex文件
它是二代加固——整体加固(不落地加载)的核心之一;
3. BaseDexClassLoader 是 ClassLoader 的具体实现类
实际上 **DexClassLoader**,**PathClassLoader** 以及 **InMemoryDexClassLoader** 加载类时,**均通过委托 BaseDexClassLoader 实现**。
ClassLoader 加载 Dex 流程简介
Dex 文件的加载依赖于前文提到的 PathClassLoader,DexClassLoader 和InMemoryDexClassLoader。
加载 Dex 文件的功能均通过委托父加载器 BaseDexClassLoader 实现,其中PathClassLoader 和 DexClassLoader 调用相同的 BaseDexClassLoader 构造函数,InMemoryDexClassLoader 调用另一个构造函数。
最终通过 ArtDexFileLoader::OpenCommon 方法在 ART 虚拟机中创建 DexFile::DexFile对象,该对象是 Dex 文件在内存中的表示,用于安卓程序运行时加载类以及执行方法代码,也是后续第三代加固——代码抽取加固,进行指令回填时的关键对象。
三种 ClassLoader 加载 Dex 文件的流程如下(基于Android10.0):
- Java层
PathClassLoader 和 DexClassLoader 委托 BaseDexClassLoader 最终执行 JNI 方法DexFile.openDexFileNative 进入 Native 层;而 InMemoryDexClassLoader 委托BaseDexClassLoader 后则执行 DexFile.openInMemoryDexFiles 进入 Native 层。
- Native 层
PathClassLoader 和 DexClassLoader 这条委托链会根据不同情况,调用ArtDexFileLoader::Open 的不同重载,或者调用 OpenOneDexFileFromZip;InMemoryDexClassLoader 调用 ArtDexFileLoader::Open 的第 3 种重载。
无论是调用哪个函数,最终都会调用 ArtDexFileLoader::OpenCommon。
经过以上调用流程后进入 ArtDexFileLoader::OpenCommon,经过 DexFile 的初始化和验证操作后便成功创建了 DexFile 对象:
创建 DexFile 对象后,Class 对应的文件便被加载到 ART 虚拟机中。
ClassLoader 加载 Class 流程简介
前文通过 ClassLoader.loadClass 讲解了双亲委托机制,那么一个 Class 具体是如何被加载到 JVM 中的呢?
首先,继承自 BaseDexClassLoader 的 3 种 ClassLoader 调用自身 loadClass 方法时。委托父类查找,委托到 ClassLoader.loadClass 时返回;BaseDexClassLoader.findClass 调用DexPathList.findClass,其内部调用 Element.findClass,最终调用DexFile.loadClassBinaryName 进入 DexFile 中,该流程如图所示:
进入 DexFile 后,主要执行以下操作:
- DexFile
通过JNI函数defineClassNative进入Native层;
- DexFile_defineClassNative
通过 FindClassDef 枚举 DexFile 的所有 DexClassDef 结构并使用 ClassLinker::DefineClass 创建对应的 Class 对象;
之后调用 ClassLinker::InsertDexFileInToClassLoader 将对应的 DexFile 对象添加到 ClassLoader 的 ClassTable 中。
- ClassLinker::DefineClass
调用 LoadField 加载类的相关字段,之后调用 LoadMethod 加载方法,再调用 LinkCode 执行方法代码的链接。
该流程如图所示:
综上所述,ClassLoader 最终通过 ClassLinker::DefineClass 创建 Class 对象,并完成Field 和 Method 的加载以及 Code 的链接。
调用链中有一个核心函数——ClassLinker::LoadMethod,通过该函数可以获取方法字节码在 DexFile 中的偏移值 code_off,它是实现指令回填的核心之一。
LoadDexDemo
经过前文的介绍,我们知道 Android 中可使用 ClassLoader 加载 dex 文件,并调用其保存的类的方法。
创建空项目,编写一个测试类用于打印信息,编译后提取 APK 文件和该类所在的 dex 文件并推送至手机的 tmp 目录。
package com.example.emptydemo;
import android.util.Log;
public class TestClass {
public void print() {
Log.d("NshIdE", "TestClass.print() is called!");
}
}
创建另一个项目,通过 DexClassLoader 加载 APK 和提取的 dex 文件并反射执行print方法:
- 创建私有目录用于创建 DexClassLoader,分别是 odex 和 lib 目录;
- 创建 DexClassLoader;
- 加载指定类;
- 反射加载并执行类的方法。
package com.example.testdemo;
import android.content.Context;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Context appContext = getApplicationContext();
//com.example.emptydemo
loadDexClassAndExecuteMethod(appContext,
"data/local/tmp/classes.dex"); //直接加载 dex 文件
loadDexClassAndExecuteMethod(appContext,
"data/local/tmp/app-release.apk");//加载apk文件,本质还是加载dex
}
public void loadDexClassAndExecuteMethod(Context context, String
strDexFilePath) {
// 1.先创建优化私有目录app_opt_dex和app_lib_dex,用于ClassLoader
// /data/user/0/com.example.testdemo/app_opt_dex
File optFile = context.getDir("opt_dex", 0);
// /data/user/0/com.example.testdemo/app_lib_dex
File libFile = context.getDir("lib_dex", 0);
// 2.创建ClassLoader用于加载Dex文件 依次为指定dex文件路径 Odex目录 lib库目录 父类加载器
DexClassLoader dexClassLoader = new DexClassLoader(
strDexFilePath,
optFile.getAbsolutePath(),
libFile.getAbsolutePath(),
MainActivity.class.getClassLoader());
Class<?> clazz = null;
try {
// 3.通过创建的DexClassLoader加载dex文件的指定类
clazz = dexClassLoader.loadClass("com.example.emptydemo.TestClass");
if (clazz != null) {
try {
// 4.反射获取并调用类的方法
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("print");
method.invoke(obj);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
效果如下
DexClassLoader 构造函数如下
/*
参数一: String dexPath, Dex文件路径
参数二: String optimizedDirectory, 优化后的dex即Odex目录
Android中内存中不会出现上述参数一的Dex文件, 会先优化,然后运行,优化后为.odex文件
参数三: String librarySearchPath, lib库搜索路径
参数四: ClassLoader parent, 父类加载器
*/
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}
三、尾声
通过对 ClassLoader 较为系统的学习之后,并且再加上反射调用的实践,对这方面的知识有了进一步理解和学习,有利于后续学习 Android 加固的实现原理。
参考:[原创]Android从整体加固到抽取加固的实现及原理-Android安全-看雪-安全社区|安全招聘|kanxue.com
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1621925986@qq.com