ClassLoader
Java类加载器在Java安全中有着非常重要的地位,通过学习 ClassLoader 能够掌握 Java 语言执行命令的底层逻辑。
简介

在 Java 漫谈中我们利用过 CLassload#defineClass 方法动态加载字节码后传入 JVM 虚拟机进行调用,这里字节码的获取我们也提到过可以有多样的获取方式。
常见的类加载方式
两种常见的类加载方式(获取到 java.lang.class 对象):
Class.fornameClassloader.loadclass
这两个方法通过传入 类的名字 后通过 ClassLoader 实现类加载,前者是 native 方法,后者是 Java 方法。不难想到第一个方法在加载的时候如果不想用默认的 通过类名全名加载类 的 ClassLoader,或者第二个方法需要指定 ClassLoader 获取 Class ,都需要获取 CLassloader。
ClassLoader获取方式:
1 | ClassLoader loader = null; |
loadClass、findClass、defineClass
loadClass:类加载的方法,默认的双亲委派机制就实现在这个方法中。findClass:根据名称或位置加载.class字节码definclass:把字节码转化为Class
ClassLoader 类关系以及结构
URLClassLoader 是类加载机制双亲委托机制中最靠近子类的父类。ExtClassLoader、AppClassLoader、WebappClassLoaderBase等都继承自 URLClassLoader。
这个图非常好的说明了加载字节码的时候流程:

简单来说 Classloader 是一个抽象类,离它最近的 URLClassLoader ,它被 ExtClassLoad、AppClassLoader、UserClassLoader(这里是 WebappClassLoaderBase 加载器)继承着。
看一下 ClassLoader 的 loadClass 方法:
1 | protected Class<?> loadClass(String name, boolean resolve) |
Bootstrap ClassLoader,主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。Extention ClassLoader,主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。Application ClassLoader,主要负责加载当前应用的classpath下的所有类User ClassLoader,用户自定义的类加载器,可加载指定路径的class文件
其中 Bootstrap 加载器没被写入图中的原因是 Bootstrap 是一个 native 方法,不继承于 URLClassLoader。
双亲委派机制
本质
双亲委派的本质机制就是:在父类能加载情况下要用父类进行加载,只有父类无法加载的时候才会由自己的加载器进行加载。
实现
双亲委派机制类加载器之间不是用继承来实现,
具体地说,实现双亲委派机制的方法:
- 检查类是否已经被加载过
- 如果没有加载就调用父加载器(
parent)的loadClass方法进行加载 - 如果父加载器为空(已经委派到
Bootstrap)就用Bootstrap加载器加载 - 如果父加载器加载失败在调用自己的
findClass方法进行加载
意义
双亲委派的意义:
- 避免类的重复加载
- 保证类加载的安全性(防止 jar 包中的类被随意替换)
破坏
破坏方式:
- 因为双亲委派机制是在
loadClass方法中实现的,那么想要破坏这种机制就要自定义一个类加载器重写loadClass方法,彻底破坏双亲委派机制。 - JDK 1.2 以后就不提倡用户直接重写
loadClass方法,因为父类加载器加载失败就会调用自己的findClass方法完成加载,所以说不妨继承ClassLoader并且在findClass中实现自己的加载逻辑。
注意继承于 ClassLoader 的类会默认调用父类的无参方法设置父类加载器为 Apploader 并且继承于父类的原始 loadClass 方法进行双亲委派。
破坏例子:
JNDI、JDBC 这类需要加载 SPI 接口实现类:
我们日常开发中,大多数时候会通过API的方式调用Java提供的那些基础类,这些基础类时被Bootstrap加载的。但是,调用方式除了API之外,还有一种SPI的方式。
如典型的JDBC服务,我们通常通过以下方式创建数据库连接:
1 | Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "1234"); |
在以上代码执行之前,DriverManager会先被类加载器加载,因为java.sql.DriverManager类是位于rt.jar下面的 ,所以他会被根加载器加载。
类加载时,会执行该类的静态方法。其中有一段关键的代码是:
1 | ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); |
这段代码,会尝试加载 classpath 下面的所有实现了 Driver 接口的实现类。
那么,问题就来了。
DriverManager 是被根加载器加载的,那么在加载时遇到以上代码,会尝试加载所有 Driver 的实现类,但是这些实现类基本都是第三方提供的,根据双亲委派原则,第三方的类不能被根加载器加载。
于是,就在**JDBC中引入ThreadContextClassLoader(线程上下文加载器,默认情况下是AppClassLoader)**破坏了双亲委派原则。
我们深入到ServiceLoader.load方法就可以看到:
1 | public static <S> ServiceLoader<S> load(Class<S> service) { |
它获取当前线程的线程上下文类加载器 AppClassLoader,用于加载 classpath 中的具体实现类。
Tomcat 破坏双亲委派获取不同版本类:
当需要同名不同版本的类的时候双亲委派机制就显得太死板了,因此 Tomcat 破坏了双亲委派机制,提供隔离后为每个 web 单独部署了一个 WebAppClassloader,它重写了 loadClass 方法颠倒了查找顺序:即先找负责的目录下的 class 文件,加载不到的时候再交给 super.loadClass 方法走正常的双亲委派。
加载加密字节码
直接通过重写 findClass 中的逻辑先解密再调用 defineClass 把解密后的字节码传递给 JVM 虚拟机,这样加密后的恶意类能进行免杀。
1 | public class SecureClassLoader extends ClassLoader { |
模块化设计
在 JDK 9 后整个 JDK 都根据模块化设计,编译的时候只编译需要用的模块避免臃肿,同样的加载器也只负责自己的模块。
1 | Class<?> c = findLoadedClass(cn); |
在木马中的利用
类加载器中最后一步 defineClass 直接会通过字节码在 JVM 中执行类的初始化,这部分在 Java 安全漫谈中提到过,我们可以直接直截了当的传入字节码后调用 defineClass 方法进行 RCE。
1 | <%@ page import="sun.misc.BASE64Decoder" %> |