参考:
https://www.bilibili.com/video/BV1go4y197cL/
https://www.baeldung.com/java-classloaders
https://mp.weixin.qq.com/s/lX4IrOuCaSwYDtGQQFqseA
以 java 8 为例
什么是类加载
Java 是一种混合语言,它既有编译型语言的特性,又有解释型语言的特性。编译特性指所有的 Java 代码都必须经过编译才能运行。解释型指编译好的 .class 字节码需要经过 JVM 解释才能运行。.class
文件中存放着编译后的 JVM 指令的二进制信息。
当程序中用到某个类时,JVM 就会寻找加载对应的 .class 文件,并在内存中创建对应的 Class 对象。这个过程就称为类加载。
类的加载步骤
理论模型
从一个类的生命周期这个角度来看,一个类(.class) 必须经过加载、链接、初始化三个步骤才能在 JVM 中运行。
当 java 程序需要使用某个类时,JVM 会进行加载、链接、初始化这个类。
加载 Loading
通过类的完全限定名查找类的字节码文件,将类的 .class
文件字节码数据从不同的数据源读取到 JVM 中,并映射成 JVM 认可的数据结构。
这个阶段是用户可以参与的阶段,自定义的类加载器就是在这个过程。
连接 Linking
-
验证:检查 JVM 加载的字节信息是否符合 java 虚拟机规范。
确保被加载类的正确性,
.class
文件的字节流中包含的信息符合当前虚拟机要求,不会危害虚拟机自身安全。 -
准备:这一阶段主要是分配内存。创建类或接口的静态变量,并给这些变量赋默认值。
只对 static 变量进行处理。而 final static 修饰的变量在编译的时候就会分配。
-
例如:
static int num = 5
,此步骤会将 num 赋默认值 0,而 5 的赋值会在初始化阶段完成。 -
解析:把类中的符号引用转换成直接引用。
符号引用就是一组符号来描述目标,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化 Initialization
执行类初始化的代码逻辑。包括执行 static 静态代码块,给静态变量赋值。
具体实现
java.lang.ClassLoader
是所有的类加载器的父类,java.lang.ClassLoader
有非常多的子类加载器,比如我们用于加载 jar 包的 java.net.URLClassLoader
,后者通过继承 java.lang.ClassLoader
类,重写了findClass
方法从而实现了加载目录 class 文件甚至是远程资源文件。
三种内置的类加载器
-
Bootstrap ClassLoader
引导类加载器Java 类被
java.lang.ClassLoader
的实例加载,而 后者本身就是一个 java 类,谁加载后者呢?其实就是
bootstrap ClassLoader
,它是最底层的加载器,是 JVM 的一部分,使用 C++ 编写,故没有父加载器,也没有继承java.lang.ClassLodaer
类,在代码中获取为 null。它主要加载 java 基础类。位于
JAVA_HOME/jre/lib/rt.jar
以及sun.boot.class.path
系统属性目录下的类。出于安全考虑,此加载器只加载 java、javax、sun 开头的类。
-
Extension ClassLoader
扩展类加载器负责加载 java 扩展类。位于是
JAVA_HOME/jre/lib/ext
目录下,以及java.ext.dirs
系统属性的目录下的类。sun.misc.Launcher$ExtClassLoader // jdk 9 及之后 jdk.internal.loader.ClassLoaders$PlatformClassLoader
-
App ClassLoader
系统类加载器又称
System ClassLoader
,主要加载应用层的类。位于CLASS_PATH
目录下以及系统属性java.class.path
目录下的类。它是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用它来加载类。
sun.misc.Launcher$AppClassLoader // jdk 9 及之后 jdk.internal.loader.ClassLoaders$AppClassLOader
父子关系
AppClassLoader 父加载器为 ExtClassLoader,ExtClassLoader 父加载器为 null 。
很多资料和文章里说,
ExtClassLoader
的父类加载器是BootStrapClassLoader
,严格来说,ExtClassLoader
的父类加载器是 null,只不过在其的loadClass
方法中,当 parent 为 null 时,是交给BootStrap ClassLoader
来处理的。
双亲委派机制
试想几个问题:
-
有三种类加载器,如何保证一个类加载器已加载的类不会被另一个类加载器重复加载?
势必在加载某个类之前,都要检查一下是否已加载过。如果三个内置的类加载器都没加载,则加载。
-
某些基础核心类,是可以让所有的加载器加载吗?
比如 String 类,如果给它加上后门,放到 classpath 下,是让 appclassloader 加载吗?如果是被 appclassloader 加载,那么它需要做什么验证?如何进行验证?
为了解决上面的问题,java 采取的是双亲委派机制来协调三个类加载器。
每个类加载器对它加载的类都有一个缓存。
向上委托查找,向下委托加载。
-
类的唯一性
可以避免类的重复加载,当父类加载器已经加载了该类时,就没有必要子 ClassLoader 再加载一次,保证加载的 Class 在内存中只有一份。
子加载器可以看见父加载器加载的类。而父加载器没办法得知子加载器加载的类。如果 A 类是通过 AppClassLoader 加载,而 B 类通过ExtClassLoader 加载,那么对于 AppClassLoader 加载的类,它可以看见两个类。而对于 ExtClassLoader ,它只能看见 B 类。
-
安全性
考虑到安全因素,Java 核心 Api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Object 的类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心 JavaAPI 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递过来的 java.lang.Object,而直接返回已加载过的 Object.class,这样可以防止核心API库被随意窜改。
加载步骤及代码细节
public Class> loadClass(String name, boolean resolve) throws ClassNotFoundException
此函数是类加载的入口函数。resolve 这个参数就是表示需不需要进行 连接阶段。
下面是截取的部分代码片段,从这个片段中可以深刻体会双亲委派机制。
Class> c = findLoadedClass(name);
在类加载缓存中寻找是否已经加载该类。它最终调用的是 native 方法。
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
如果父加载器不为空,则让递归让父加载器去加载此类。
如果父加载器为空,则调用 Bootstrap 加载器去加载此类。此处也即为何说 ExtClassLoader 的父加载器为 null,而非 Bootstrap 。
c = findClass(name);
如果查询完所有父亲仍未找到,说明此类并未加载,则调用 findClass 方法来寻找并加载此类。我们自定义类加载器,主要重写的就是 findClass 。
总结
ClassLoader
类有如下核心方法:
loadClass
(加载指定的Java类)findLoadedClass
(查找JVM已经加载过的类)findClass
(查找指定的Java类)defineClass
(定义一个Java类)resolveClass
(链接指定的Java类)
理解Java类加载机制并非易事,这里我们以一个 Java 的 HelloWorld 来学习 ClassLoader
。
ClassLoader
加载 com.example.HelloWorld
类重要流程如下:
ClassLoader
调用loadClass
方法加载com.example.HelloWorld
类。- 调用
findLoadedClass
方法检查TestHelloWorld
类是否已经加载,如果 JVM 已加载过该类则直接返回类对象。 - 如果创建当前
ClassLoader
时传入了父类加载器(new ClassLoader(父类加载器)
)就使用父类加载器加载TestHelloWorld
类,否则使用 JVM 的Bootstrap ClassLoader
加载。 - 如果上一步无法加载
TestHelloWorld
类,那么调用自身的findClass
方法尝试加载TestHelloWorld
类。 - 如果当前的
ClassLoader
没有重写了findClass
方法,那么直接返回类加载失败异常。如果当前类重写了findClass
方法并通过传入的com.example.HelloWorld
类名找到了对应的类字节码,那么应该调用defineClass
方法去JVM中注册该类。 - 如果调用
loadClass
的时候传入的resolve
参数为 true,那么还需要调用resolveClass
方法链接类,默认为 false。 - 返回一个被 JVM 加载后的
java.lang.Class
类对象。
自定义类加载器
用途
大多数情况下,内置的类加载器够用了,但是当加载位于磁盘上其它位置,或者位于网络上的类时,或者需要对类做加密等,就需要自定义类加载器。
一些使用场景:通过动态加载不同实现的驱动的 jdbc。以及编织代理可以更改已知的字节码。以及类名相同的多版本共存机制。
具体实现
我们通常实现自定义类加载器,主要就是重写 findClass 方法。
protected Class> findClass(String name) throws ClassNotFoundException
从网络或磁盘文件(.class, jar, 等任意后缀文件) 上读取类的字节码。然后将获取的类字节码传给 defineClass 函数来定义一个类。
protected final Class> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
它最终调用也是 native 方法。
示例代码
使用类字节码中加载类
@Test
public void test3(){
Double salary = 2000.0;
Double money;
{
byte[] b = new byte[]{-54, -2, -70, -66, 0, 0, 0, 52, 0, 32, 10, 0, 7, 0, 21, 10, 0, 22, 0, 23, 6, 63, -15, -103, -103, -103, -103, -103, -102, 10, 0, 22, 0, 24, 7, 0, 25, 7, 0, 26, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 18, 76, 111, 99, 97, 108, 86, 97, 114, 105, 97, 98, 108, 101, 84, 97, 98, 108, 101, 1, 0, 4, 116, 104, 105, 115, 1, 0, 26, 76, 67, 108, 97, 115, 115, 76, 111, 97, 100, 101, 114, 47, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 59, 1, 0, 3, 99, 97, 108, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 1, 0, 6, 115, 97, 108, 97, 114, 121, 1, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 17, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 46, 106, 97, 118, 97, 12, 0, 8, 0, 9, 7, 0, 27, 12, 0, 28, 0, 29, 12, 0, 30, 0, 31, 1, 0, 24, 67, 108, 97, 115, 115, 76, 111, 97, 100, 101, 114, 47, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 1, 0, 11, 100, 111, 117, 98, 108, 101, 86, 97, 108, 117, 101, 1, 0, 3, 40, 41, 68, 1, 0, 7, 118, 97, 108, 117, 101, 79, 102, 1, 0, 21, 40, 68, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 0, 33, 0, 6, 0, 7, 0, 0, 0, 0, 0, 2, 0, 1, 0, 8, 0, 9, 0, 1, 0, 10, 0, 0, 0, 47, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 2, 0, 11, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 12, 0, 0, 0, 12, 0, 1, 0, 0, 0, 5, 0, 13, 0, 14, 0, 0, 0, 1, 0, 15, 0, 16, 0, 1, 0, 10, 0, 0, 0, 64, 0, 4, 0, 2, 0, 0, 0, 12, 43, -74, 0, 2, 20, 0, 3, 107, -72, 0, 5, -80, 0, 0, 0, 2, 0, 11, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 12, 0, 0, 0, 22, 0, 2, 0, 0, 0, 12, 0, 13, 0, 14, 0, 0, 0, 0, 0, 12, 0, 17, 0, 18, 0, 1, 0, 1, 0, 19, 0, 0, 0, 2, 0, 20};
money = calSalary(salary,b);
System.out.println("money: " + money);
}
}
private Double calSalary(Double salary,byte[] bytes) {
Double ret = 0.0;
try {
Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
method.setAccessible(true);
Class> clazz = (Class>) method.invoke(this.getClass().getClassLoader(), "ClassLoader.SalaryCaler1", bytes, 0, bytes.length);
System.out.println(clazz.getClassLoader());
Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class);
ret = (Double)cal.invoke(object,salary);
} catch (Exception e) {
e.printStackTrace();
}
return ret;
}
从文件中读取类字节码加载类
@Test
// 自定义类加载器,从 .myclass 文件中中加载类。
public void test4(){
// 将其它方法全注释,并且 ClassLoader.SalaryCaler 文件更名。
try {
Double salary = 2000.0;
Double money;
SalaryClassLoader classLoader = new SalaryClassLoader("C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\underlying\\target\\classes\\");
money = calSalary(salary, classLoader);
System.out.println("money: " + money);
} catch (Exception e) {
e.printStackTrace();
}
}
private Double calSalary(Double salary, SalaryClassLoader classLoader) throws Exception {
Class> clazz = classLoader.loadClass("ClassLoader.SalaryCaler1");
System.out.println(clazz.getClassLoader());
Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class);
return (Double)cal.invoke(object,salary);
}
package ClassLoader;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.SecureClassLoader;
public class SalaryClassLoader extends SecureClassLoader {
private String classPath;
public SalaryClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class> findClass(String name)throws ClassNotFoundException {
String filePath = this.classPath + name.replace(".", "/").concat(".myclass");
byte[] b = null;
Class> aClass = null;
try (FileInputStream fis = new FileInputStream(new File(filePath))) {
b = IOUtils.toByteArray(fis);
aClass = this.defineClass(name, b, 0, b.length);
} catch (Exception e) {
e.printStackTrace();
}
return aClass;
}
}
从 jar 包中读取类字节码加载类
@Test
//自定义类加载器,从 jar 包中加载 .myclass
public void test5(){
try {
Double salary = 2000.0;
Double money;
SalaryJarLoader classLoader = new SalaryJarLoader("C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\out\\artifacts\\SalaryCaler\\SalaryCaler.jar");
money = calSalary(salary, classLoader);
System.out.println("money: " + money);
} catch (Exception e) {
e.printStackTrace();
}
}
private Double calSalary(Double salary, SalaryJarLoader classLoader) throws Exception {
Class> clazz = classLoader.loadClass("ClassLoader.SalaryCaler1");
System.out.println(clazz.getClassLoader());
Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class);
return (Double)cal.invoke(object,salary);
}
package ClassLoader;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.SecureClassLoader;
public class SalaryJarLoader extends SecureClassLoader {
private String jarPath;
public SalaryJarLoader(String jarPath) {
this.jarPath = jarPath;
}
@Override
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class> c = null;
synchronized (getClassLoadingLock(name)){
c = findLoadedClass(name);
if(c == null){
c = this.findClass(name);
// System.out.println(c);
if( c == null){
c = super.loadClass(name,resolve);
}
}
}
return c;
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
Class> ret = null;
try {
URL jarUrl = new URL("jar:file:\\"+jarPath+"!/"+name.replace(".","/").concat(".myclass"));
InputStream is = jarUrl.openStream();
byte[] b = IOUtils.toByteArray(is);
ret = this.defineClass(name,b,0,b.length);
} catch (Exception e) {
// e.printStackTrace();
}
return ret;
}
}
打破双亲委派机制
重写继承而来的 loadClass 方法。
使其优先从本地加载,本地加载不到再走双亲委派机制。
@Override
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class> c = null;
synchronized (getClassLoadingLock(name)){
c = findLoadedClass(name);
if(c == null){
c = this.findClass(name);
if( c == null){
c = super.loadClass(name,resolve);
}
}
}
return c;
}
其它
URLClassLoader
URLClassLoader
提供了加载远程资源的能力,在写漏洞利用的 payload 或者 webshell 的时候我们可以使用它来加载远程的 jar 来实现远程的类方法调用。
在 java.net 包中,JDK提供了一个易用的类加载器 URLClassLoader,它继承了 ClassLoader。
public URLClassLoader(URL[] urls)
//指定要加载的类所在的URL地址,父类加载器默认为 AppClassLoader。
public URLClassLoader(URL[] urls, ClassLoader parent)
//指定要加载的类所在的URL地址,并指定父类加载器。
从本地 jar 包中加载类
@Test
// 从 jar 包中加载类
public void test3() {
try {
Double salary = 2000.0;
Double money;
URL jarUrl = new URL("file:C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\out\\artifacts\\SalaryCaler\\SalaryCaler.jar");
try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{jarUrl})) {
money = calSalary(salary, urlClassLoader);
System.out.println("money: " + money);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private Double calSalary(Double salary, URLClassLoader classLoader) throws Exception {
Class> clazz = classLoader.loadClass("ClassLoader.SalaryCaler");
Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class);
return (Double)cal.invoke(object,salary);
}
从网络 jar 包中加载类
package com.anbai.sec.classloader;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
/**
* Creator: yz
* Date: 2019/12/18
*/
public class TestURLClassLoader {
public static void main(String[] args) {
try {
// 定义远程加载的jar路径
URL url = new URL("https://anbai.io/tools/cmd.jar");
// 创建URLClassLoader对象,并加载远程jar包
URLClassLoader ucl = new URLClassLoader(new URL[]{url});
// 定义需要执行的系统命令
String cmd = "ls";
// 通过URLClassLoader加载远程jar包中的CMD类
Class cmdClass = ucl.loadClass("CMD");
// 调用CMD类中的exec方法,等价于: Process process = CMD.exec("whoami");
Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);
// 获取命令执行结果的输入流
InputStream in = process.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int a = -1;
// 读取命令执行结果
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}
// 输出命令执行结果
System.out.println(baos.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
import java.io.IOException;
/**
* Creator: yz
* Date: 2019/12/18
*/
public class CMD {
public static Process exec(String cmd) throws IOException {
return Runtime.getRuntime().exec(cmd);
}
}
jsp webshell
为什么上传的 jsp webshell 能立即访问,按道理来说 jsp 要经过 servlet 容器处理转化为 servlet 才能执行。而通常开发过程需要主动进行更新资源、或者重新部署、重启 tomcat 服务器。
这是因为 tomcat 的 热加载机制 。而之所以 JSP 具备热更新的能力,实际上借助的就是自定义类加载行为,当 Servlet 容器发现 JSP 文件发生了修改后就会创建一个新的类加载器来替代原类加载器,而被替代后的类加载器所加载的文件并不会立即释放,而是需要等待 GC。