Java类加载机制
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
双亲委派机制
jvm对class文件采用的是按需加载的方式,当需要使用该类时,jvm才会将它的class文件加载到内存中产生class对象。
在加载类的时候,是采用的双亲委派机制,即把请求交给父类处理的一种任务委派模式。通俗讲的话就是,声明一个Application ClassLoader,我们在加载某个类时,他会先向上询问父类是否加载,一层一层向上查找,一旦找到某个父类加载该类时就停止询问并调用,在询问过程中最终走到Bootstrap ClassLoader,若都没有找到有父类加载该类时,则自个儿加载。
工作原理
- 如果一个类加载器接收到了类加载的请求,它自己不会先去加载,会把这个请求委托给父类加载器去执行。
- 如果父类还存在父类加载器,则继续向上委托,一直委托到启动类加载器:Bootstrap ClassLoader
- 如果父类加载器可以完成加载任务,就返回成功结果,如果父类加载失败,就由子类自己去尝试加载,如果子类加载失败就会抛出ClassNotFoundException异常,这就是双亲委派模式
ClassLoad类加载分析
这里以ClassLoader中的getSystemClassLoader为例,加载定义的Person类,下个断点看看在loadClass这个过程中到底发生了什么?
以loadClass为起点,加载Person类,可以发现直接从ClassLoad中的loadClass –> Launcher$Application.loadClass
一直走到最后,返回父类的loadClass
这里直接来到了ClassLoader.loadClass,继续跟进
这里将这段loadClass关键代码提出来讲讲:
1 | protected Class<?> loadClass(String name, boolean resolve) |
大致流程是先走到findLoadedClass(name),根据类名在JVM中查找是否已经被加载过,显然返回null,因为我们的Person类是第一次被加载,来到if判断,这里的parent父类初始为Launcher类中ExtClassLoader,然而在该类下是没有loadClass的,因此一个来回后parent变为了null。关于这里为什么会经过一个来回再回到这里?因为Launcher下的ExtClassLoader的最终父类就是ClassLoader,准确来说大致经过了这样一个流程:Launcher$ExtClassLoader -> URLClassLoader -> SecureClassLoader -> ClassLoader, 层层extends的关系,这其实也印证了上面提到的双亲委派机制。
上图为第一次进入if,下面为第二次进入if
随后来到findBootstrapClassOrNull(name),看名字就知道在启动类BootstrapClass中查看Person类是否被加载,否则返回null,而最终在寻找的这个过程实际上是调用C程序实现(native类型强调使用非Java语言实现)。显然这里同样是找不到的,c 依然 = null。
因为从父类中未找到的缘故,因此选择自定义函数查找Person类。向下进入到另一个关键性函数: findClass,该函数在URLClassLoader中具体实现:findClass的作用是根据name向URL path中查询该类
Person被replace为Person.class,调用getResource从文件系统中查找同名Person.class
拿到结果后,调用另一个关键性函数defineClass,关于defineClass,其实他才是JVM将字节码文件(Person.class)转换为Java类的核心。同样,试想若Person.class为一个危险类(后面演示执行危险代码),因为defineClass的特点,在Java安全中常被拿来反序列化调用为Java类时将导致任意代码执行漏洞,例如在Commons-Collections3中攻击链TemplatesImpl下的defineClass的调用等。
拿到结果后,调用另一个关键性函数defineClass,关于defineClass,其实他才是JVM将字节码文件(Person.class)转换为Java类的核心。同样,试想若Person.class为一个危险类(后面演示执行危险代码),因为defineClass的特点,在Java安全中常被拿来反序列化调用为Java类时将导致任意代码执行漏洞,例如在Commons-Collections3中攻击链TemplatesImpl下的defineClass的调用等。
走到这里大致流程执行完毕,回想一下整个过程其实就是三个步骤:
ClassLoader#loadClass -> URLClassLoader#findClass -> ClassLoader#defineClass
- loadClass()的作用是从已加载的类、父加载器位置寻找类(即双亲委派机制),在前面没有找到的情况下,调用findClass()方法;
- findClass()根据类名查找该类的位置(甚至是远程的方式),并调用defineClass()作真正的类加载;
- defineClass()的作用是获取该类,并将字节码(.class文件)转换为真正的 Java 类
动态远程加载恶意类
Http协议远程加载
定义危险类ClassLoad1,编译为ClassLoad1.class文件后放到E盘下,用于后面调用这里利用Http协议,在E盘使用python3 -m http.server 9999 开启服务用于远程访问:1
2
3
4
5
6
7
8
9
10
11
12import java.io.IOException;
public class ClassLoad1 {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
编写利用类:
1 | public class UrlLoader { |
弹出计算器,同样的用法还有利用file协议,jar协议访问。
defineClass直接调用字节码文件
下面演示defineClass调用恶意类导致的任意代码执行
利用代码:
1 | public class UrlLoader { |
小结
动态类加载在Java安全中具有及其重要的地位,在Java反序列化中CC链的分析利用将会使用到这部分知识。对于类加载,我觉得还是有必要理解整个过程的流程和原理,由于也是初步接触Java安全,对于更深层次的理解和利用还不太熟练,有讲得不对的地方希望帮忙指出。