ClassLoader

ClassLoader

0x00 ByteCode

​ Java字节码(ByteCode)其实仅仅指的是Java虚拟机执行使用的一类指令,通常被存储 在.class文件中。字节码的本质就是一个字节数组 ,它有特定的复杂的内部格式,Java类初始化的时候会调用java.lang.ClassLoader加载字节码,.class文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的.class文件,并创建对应的class对象,将class文件加载到虚拟机的内存,而在JVM中类的查找与装载就是由ClassLoader完成的。

ByteCode2Class脚本: https://github.com/hengyunabc/dumpclass

0x01 显式与隐式加载

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

  • 显式:利用反射来加载一个类,Class.forName()等。
  • 隐式:通过ClassLoader来动态加载,new 一个类或者 类名.方法名返回一个类。
1
2
3
4
5
6
7
8
9
10
11
@Test
public void loadClassTest() throws Exception {
//1、反射加载,显式加载。
Class<?> aClass = Class.forName("java.lang.Runtime");
System.out.println(aClass.getName());

//2、ClassLoader加载,隐式加载。
Class<?> aClass1 = ClassLoader.getSystemClassLoader().loadClass("java.lang.ProcessBuilder");
System.out.println(aClass1.getName());

}

0x02 ClassLoader

ClassLoader(类加载器)主要作用就是将class文件读入内存,并为之生成对应的java.lang.Class对象。

ClassLoader中的一些核心方法有:

  1. loadClass(加载指定的Java类)
  2. findClass(查找指定的Java类)
  3. findLoadedClass(查找JVM已经加载过的类)
  4. defineClass(定义一个Java类)
  5. resolveClass(链接指定的Java类)

当我们正常加载一个Class的时候,方法的执行顺序也如上所示。

内置ClassLoader

JVM中存在3个内置ClassLoader:

  1. BootstrapClassLoader 启动类加载器 负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.xxx.* 都在里面,比如 java.util.*、java.io.*、java.nio.*、java.lang.* 等等。
  2. ExtensionClassLoader 扩展类加载器 负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中
  3. AppClassLoader 系统类加载器 才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的

自定义ClassLoader

除了内置的这三个ClassLoader,我们还可以自定义ClassLoader,自定义ClassLoader必须继承java.lang.ClassLoader类。一个简单的demo为例,定义一个类并获取其字节码再利用自定义ClassLoader加载。

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
//Person.java
public class Person {
public String id;
private int age;

public Person(String id,int age){
this.id=id;
this.age=age;
}
public void hello(){
System.out.println("func hello called.");
}

}

//Util.java
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

public class Util {
public static String getByteArray(String className) throws Exception{
URI uri = Util.class.getClassLoader().getResource(className+".class").toURI();
byte[] bytes = Files.readAllBytes(Paths.get(uri));
String base64 = Base64.getEncoder().encodeToString(bytes);
return base64;
}
}

编写自定义ClassLoader,并重写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
//MyClassLoader.java
import java.lang.reflect.Method;

public class MyClassLoader extends ClassLoader{
private static String className = "Person";
private static byte[] byteArray;

static {
try {
byteArray = Util.getByteArray(className);
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//只处理Person类
if (name.equals(className)){
return defineClass(className, byteArray, 0, byteArray.length );
}
return super.findClass(name);
}

public static void main(String[] args) throws Exception{
MyClassLoader myClassLoader = new MyClassLoader();
Class<?> aClass = myClassLoader.loadClass(className);
Object o = aClass.newInstance();
//调用Person类的hello()
Method m = o.getClass().getMethod("hello");
m.invoke(o);
}
}
//输出: func hello called.

双亲委派模型

双亲委派模型展示了类的实际加载过程以及ClassLoader的使用顺序。

先不考虑自定义ClassLoader,加入我们编写一个普通类,那么它的加载过程就可以用上图来表示。

双亲委派可以简单理解为:向上委派,向下加载

当一个.class文件要被加载时首先会在AppClassLoader检查是否已被加载,如果加载过就不再加载,如果没有被加载则向上,也就是父加载器委派,父加载器重复这个过程直到BootstrapClassLoader,如果BootstrapClassLoader也没有加载过此类则开始向下加载;首先询问BootstrapClassLoader是否可以加载,如果可以就自己加载,如果不可以则向下加载;子加载器重复这个过程直到AppClassloader,如果AppClassloader也无法加载则抛出ClassNotFoundException异常。

如果我们自定义了ClassLoader并加载类,那么就会先从自定义ClassLoader开始加载,顺序上先于AppClassLoader。

补充

从双亲委派模型来理解上文中几个方法(loadClass、findClass….):

  • 如果我们要自定义加载一个类,那么首先调用loadClass去检查这个类是否被加载过,同样是向上委派的过程。如果没有发现类被记载则调用findClass。
  • findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass 。
  • defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类。

值得注意的是,defineClass方法并不会调用类的静态代码块或者构造方法,相关的调用如下:

  • 初始化:静态代码块
  • 实例化:构造代码块\无参构造函数

而使用Class.forName进行动态类加载有两种模式,初始化与不初始化,默认是初始化

指定初始化会调用静态代码块,禁止初始化不会调用静态代码块。

1
2
3
4
5
//默认开启初始化
Class.forName("Person");
//关闭初始化
Class.forName("Person",false,getSystemClassLoader());

0x03 加载字节码的几种方式

1.利用defineClass()

上面已经提到defineClass()会将Class转换成类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.reflect.Method; 
import java.util.Base64;
public class HelloDefineClass {

public static void main(String[] args) throws Exception {
Method defineClass =
ClassLoader.class.getDeclaredMethod("defineClass", String.class,
byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] code =
Base64.getDecoder().decode("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code, 0, code.length);
hello.newInstance();
}
}

2.利用URLClassLoader

URLClassLoader 实际上是我们平时默认使用的 AppClassLoader 的父类,所以,我们解释URLClassLoader 的工作过程实际上就是在解释默认的Java类加载器的工作流程。

正常情况下,Java会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类)来寻找.class文件来加载,而这个基础路径有分为三种情况:

  1. URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻找.class文件

  2. URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找.class文件

  3. URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类

注意第三种情况,如果我们使用的是http协议,那么就会使用到Loader来寻找类,也就是说URLClassLoader允许远程加载类

1
2
3
4
5
6
7
8
9
10
11
12
import java.net.URL; 
import java.net.URLClassLoader;
public class HelloClassLoader
{
public static void main( String[] args ) throws Exception
{
URL[] urls = {new URL("http://localhost:8000/")};
URLClassLoader loader = URLClassLoader.newInstance(urls);
Class c = loader.loadClass("Hello");
c.newInstance();
}
}

3.利用xlan

TemplatesImplcom.sun.org.apache.xalan包中一个非常有利用价值的类,它是默认包含在JDK中的。

通过调用其newTransformer()方法我们最终可以调用defineClass(),具体的细节并不复杂不再赘述,大致过程是

1
newTransformer() -> getTransletInstance() -> defineTransletClasses() -> defineClass()

当然有几个属性要满足条件才能打通利用链:

  • _name不为空
  • _bytecodes存放字节码
  • _tfactory必须是TransformerFactoryImpl类
  • 加载的类必须是AbstractTranslet的子类
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
//Test.java
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;

public class Test extends AbstractTranslet {
public Test(){
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}
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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Client {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field namefield = templatesClass.getDeclaredField("_name");
namefield.setAccessible(true);
namefield.set(templates,"aaa");
//_class属性为null不赋值
byte[] code = Files.readAllBytes(Paths.get("c:\\tmp\\classes\\Test.class"));
byte [][] codes = {code};
Field bytecodesfield = templatesClass.getDeclaredField("_bytecodes");
bytecodesfield.setAccessible(true);
bytecodesfield.set(templates,codes); //载入字节码
Field tfactoryfield = templatesClass.getDeclaredField("_tfactory");
tfactoryfield.setAccessible(true);
tfactoryfield.set(templates, new TransformerFactoryImpl());
templates.newTransformer();
//运行弹出计算器
}
}

4.利用BCELClassLoader

注意: BCELclassloader在jdk8u251之后的版本就无法使用

BCEL字节码是字节码的一种,与上文中提到的字节码在本质上并无不同,仅仅是形式上的变化。

  • BCEL类存储在com.sun.org.apache.bcel.internal.util包中。
  • BECL ClassLoader也是一种恢复成一个类并在JVM虚拟机中进行加载的字节序列。
  • BCEL也是在JDK库中,在com.sun.org.apache.bcel.internal.util的包中有一个ClassLoader类,它是一个ClassLoader类,和默认的java.lang包下的ClassLoader类不同,loadClass实现不同而已。
  • 欲让BCELclassloader识别加载BCEL字节码,需要在开头添加$$BCEL$$
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
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import evil.Exploit;
import java.util.Base64;
public class BCELClassLoaderDemo {
/**
* 生成BCEL格式的字节码,方法一:
* 通过 Repository.lookupClass()将Class对象转化为表示Java字节码的对象JavaClass
* 然后通过Utility.encode() 将Java字节码对象JavaClass转化为BCEL格式的字节码
*/
public static String generateBcelCode1(Class clazz) throws Exception {
JavaClass evilJavaClazz = Repository.lookupClass(clazz);
String code = Utility.encode(evilJavaClazz.getBytes(), true);
String bcelCode = "$$BCEL$$" + code;
System.out.println("bcelcode=" + bcelCode);
return bcelCode;
}
/**
* 生成BCEL格式的字节码,方法二:
* 将Java字节码直接传入Utility.encode() ,从而得到BCEL格式的字节码
*/
public static String generateBcelCode2(String classBase64) throws Exception {
byte[] codes = Base64.getDecoder().decode(classBase64);
String code = Utility.encode(codes, true);
String bcelCode = "$$BCEL$$" + code;
System.out.println("bcelcode=" + bcelCode);
return bcelCode;
}
public static void main(String[] args) {
try {
ClassLoader bcelClassLoader = new ClassLoader();
// String bcelCode = generateBcelCode1(Exploit.class);
String bcelCode = generateBcelCode2("yv66v...(class字节码的base64编码)...");
bcelClassLoader.loadClass(bcelCode).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Exploit.java
package evil;

import java.io.IOException;

public class Exploit {
static{
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
}
}

5.利用unsafe

sun.misc.Unsafe类中有defineClass()

1
2
3
4
5
6
sun.misc.Unsafe public Class<?> defineClass(String s,
byte[] bytes,
int i,
int i1,
ClassLoader classLoader,
java.security.ProtectionDomain protectionDomain)

我们可以直接调用

1
2
3
4
5
6
7
8
9
10

// 获取Unsafe无参构造方法
Constructor constructor = Unsafe.class.getDeclaredConstructor();

// 修改构造方法访问权限
constructor.setAccessible(true);

// 反射创建Unsafe类实例,等价于 Unsafe unsafe = new Unsafe();
Unsafe unsafe = (Unsafe) constructor.newInstance();
unsafe.defineClass(<className>,<classBytes>,<offset=0>,<length>);

测试用class自行编写。

unsafe的补充: https://javasec.org/javase/Unsafe


ClassLoader
http://example.com/2022/12/28/ClassLoader/
Author
springtime
Posted on
December 28, 2022
Licensed under