类加载
Yu9

0x01 前言

大多人学习Java安全是从反序列化漏洞开始说起,提到反序列化漏洞不可避免的要提一提反射。反射通俗易懂的说就是操作class,通过得到类的class然后做出一些操作

例如执行命令:

1
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")),"calc.exe");

拆开写:

1
2
3
4
5
6
7
Class clazz = Class.forName("java.lang.Runtime");

Method exec = clazz.getMethod("exec", String.class);

Object getRuntime = clazz.getMethod("getRuntime").invoke(clazz);

exec.invoke(getRuntime,"calc.exe");

可以看到要进行反射,那获取这个类是不可缺少的一步;通常来说我们有如下三种⽅式获取⼀个“类”,也就 是 java.lang.Class 对象:

  • obj.getClass(): 当已经存在某个类的实例对象 obj 时,可以通过调用 obj.getClass() 方法来获取该对象所属类的 Class 对象。

    1
    2
    CodeSomeClass obj = new SomeClass();
    Class<?> clazz = obj.getClass();
  • 类的 .class 属性:如果已经加载了某个类,可以直接使用该类的 .class 属性来获取对应的 Class 对象。

    1
    CodeClass<?> clazz = SomeClass.class;
  • Class.forName(): 如果已知某个类的名字,可以使用 Class.forName() 来获取对应的 Class 对象。

    1
    javaCopy CodeClass<?> clazz = Class.forName("com.example.SomeClass");

今天我们要探究的主题就是Class.forName() 是如何通过类名(全限定类名)来获取对应的 Class 对象

在此之前我们需要先了解一下类加载相关的知识

0x02 类加载

类加载是一个很大的话题,我们重点关注的还得是跟漏洞相关的!

在反序列化过程中,如果要将一个序列化的对象转换回原始的对象,首先需要读取字节流中的类信息(即对象的类描述),然后根据这个类信息来动态加载类,并创建对象实例。如果类信息对应的类尚未被加载,系统会触发类加载机制来加载该类,确保能够正确地实例化对象。因此,类加载在反序列化中也扮演着重要的角色。

2.1、类的生命周期

类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:

  1. 加载(Loading):当程序中使用某个类时,Java虚拟机(JVM)会使用类加载器将该类的.class文件加载到内存中。类加载的过程包括加载、链接和初始化等步骤。
  2. 链接(Linking):在链接阶段,会验证类中的字节码以确保其符合语言规范,并且会将类或接口的二进制表示合并到虚拟机的运行时状态中。链接又可细分为三个步骤:
    • 验证(Verification):确保加载的类符合Java语言规范,不会造成安全问题。
    • 准备(Preparation):为类的静态变量分配内存空间,并设置默认初始值。
    • 解析(Resolution):将类、方法、字段等符号引用解析为直接引用,为后续的调用做准备。
  3. 初始化(Initialization):在初始化阶段,如果类具有父类,JVM会先初始化父类。然后按顺序执行静态变量的赋值操作和静态初始化块,从而完成类的初始化工作。
  4. 使用(Usage):在类初始化完成后,程序可以通过创建对象、调用静态方法等方式使用这个类。
  5. 卸载(Unloading):当一个类不再被程序所引用时,且满足一定条件(比如类加载器被回收、类型的引用已经不存在等),JVM会卸载该类,释放相关的内存空间。

image-20240213160816125

2.2、类加载过程

Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?

系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析

image-20240219215149105

在类加载阶段重点需要关注的就是类加载器 。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 双亲委派模型 决定(不过,我们也能打破由双亲委派模型)。

每个 Java 类都有一个引用指向加载它的 ClassLoader。不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的。

2.3、类加载器

作用:加载.class 文件到 JVM 中(在内存中生成一个代表该类的 Class 对象)

字节码的本质就是一个字节数组 []byte

除了加载类之外,类加载器还可以加载 Java 应用所需的资源如文本、图像、配置文件、视频等等文件资源。

JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。

对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

1)JVM 中内置了三个重要的 ClassLoader

  1. **BootstrapClassLoader(启动类加载器)**:最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jarresources.jarcharsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。
  2. **ExtensionClassLoader(扩展类加载器)**:主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
  3. **AppClassLoader(应用程序类加载器)**:面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

BootstrapClassLoader 由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。

2)那些位于网络上静态文件服务器提供的 jar 包和 class文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。

2.4、双亲委派

image-20240220011840715

class_loader_process

每个类加载器加载过的类都有一个缓存

向上委托查找,向下委托加载

2.5、类加载器的关系

Bootstrap 类加载器、Extension 类加载器和 System/Application 类加载器之间的关系如下图所示:

class_loader_relation

ExtClassLoader 和 AppClassLoder 继承 URLClassLoader,而 URLClassLoader 继承 ClassLoader,BoopStrap ClassLoder 是用 C/C++ 代码来实现的,并不继承自 java.lang.ClassLoader,它本身是虚拟机的一部分,并不是一个 Java 类。

JVM 加载的顺序:BoopStrap ClassLoder -> ExtClassLoader -> AppClassLoder

2.6、自定义类加载器

类的加载过程会使用到 findLoadedClass()、loadClass()、findClass() 等方法,ClassLoader 的 loadClass() 方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先,检查类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 父加载器不为空,调用父加载器的loadClass()方法
c = parent.loadClass(name, false);
} else {
// 父加载器为空则,调用 Bootstrap ClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// 如果仍然没有找到该类,则调用findClass()方法以找到该类
c = findClass(name);
}
}
return c;
}

**loadClass():**JVM 在加载类的时候,都是通过 ClassLoader 的 loadClass() 方法来加载 class 的,loadClass() 方法使用双亲委派模式。如果要改变双亲委派模式,可以修改 loadClass() 方法来改变 class 的加载方式。

**findClass():**ClassLoader 通过 findClass() 方法来加载类。自定义类加载器实现这个方法来加载需要的类,比如指定路径下的文件、字节流等。

**definedClass():**将定义的字节码文件经过字节数组流解密之后,将该字节流数组生成字节码文件,也就是该类文件的类名 .class。通常用在重写 findClass() 方法中,返回一个 Class 对象。

0x03 案例

案例入手,先准备一个Person类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.huashu338.servlet.loderClass;

public class Person {
public String name;

public static int id;

static {
System.out.println("静态代码块");
}

public static void qwe(){
System.out.println("静态方法");
}

{
System.out.println("构造代码块");
}

public Person() {
System.out.println("无参构造");
}

public Person(String name) {
this.name = name;
System.out.println("有参构造");
}

public void eat(String food){
System.out.println(food);
}


public String toString() {
return "Person{name = " + name + "}";
}
}

无参构造

1
new Person();

image-20231019205401740

有参构造

1
new Person("zahnsan");

image-20231019205434987

静态方法

1
Person.qwe();

image-20231019211358420

静态参数

1
Person.id=1;

image-20231019211519342

获取他的类

1
Class c = Person.class;

image-20231019212806943

0x04 参考

https://javaguide.cn/java/jvm/class-loading-process.html

https://anye3210.github.io/2021/08/02/详解Java类加载过程/

https://henleylee.github.io/posts/2019/b7c8c167.html#toc-heading-3

https://zhuanlan.zhihu.com/p/51374915

由 Hexo 驱动 & 主题 Keep
总字数 50.7k 访客数 访问量