Java类加载器-ClassLoader
什么是classloader
Java是一个依赖于JVM(Java虚拟机)实现的跨平台的开发语言。Java程序在运行前需要先编译成class文件。classloader
顾名思义,即是类加载。java类初始化的时候会调用java.lang.ClassLoader
加载字节码,class文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的class文件,并创建对应的class对象,将class文件加载到虚拟机的内存,而在JVM中类的查找与装载就是由ClassLoader
完成的,而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader
就是用来动态加载class文件到内存当中用的,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载机制
类文件编译流程图
上图中,我们创建一个测试文件ClassLoaderTest.java
运行,经过javac的编译会在本地磁盘生成ClassLoaderTest.class
文件,如果我们要磁盘中的class文件在java虚拟机中运行,需要经过一系列的生命周期(加载、连接(验证->准备->解析))以及初始化的操作,最后就是我们java虚拟机内存使用自设方法区中字节码二进制数据去引用堆区的Class对象。
类加载的三大步骤
第一步 加载
加载指的是把class字节码文件从各个来源通过类加载器装载入内存中,并为之创建一个java.lang.Class
对象,即程序中使用任何类时,系统都会为之建立一个java.lang.Class
对象,系统中所有的类都是java.lang.Class
的实例。类的加载由类加载器完成,JVM提供的类加载器叫做系统类加载器,此外还可以通过继承ClassLoader基类来自定义类加载器。
- 字节码来源,一般的加载来源包括从本地路径下编译生成的.class文件,jar包
中的.class文件,从远程网络,以及动态代理实时编译 - 类加载器即classloader
第二步 连接
连接阶段负责把类的二进制数据合并到JRE中
连接阶段又分为三个阶段
验证:保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
- 1、文件格式的验证,文件中是否有不规范的或者附加的其他信息。例如比如常量中
是否有不被支持的常量。 - 2、元数据的验证,保证其描述的信息符合Java语言规范的要求。例如类是否有父
类,是否继承了不被允许的final类等 - 3、字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
- 4、符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类,校
验符号引用中的访问性(private, public等) 是否可被当前类访问等
- 1、文件格式的验证,文件中是否有不规范的或者附加的其他信息。例如比如常量中
- 准备:主要是为类变量(注意, 不是实例变量)分配内存,并且赋予初值。
特别需要注意,初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。例如int类型初值为0,reference为nll等 解析:将常量池内的符号引用替换为直接引用的过程。
- 举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是
符号引用,1234567就是直接引用。 - 在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的
内存地址或偏移量,也就是直接引用。
- 举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是
第三步 初始化
这个阶段主要是对类变量初始化,是执行类构造器的过程。
- 1.只对static修饰的变量或语句进行初始化。
- 2.如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
- 3.如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
双亲委托机制
classloader
的双亲委托机制是指多个类加载器之间存在父子关系的时候,某个class类具体由哪个加载器进行加载的问题。当一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先会判断这个class是不是已经加载成功,如果没有加载的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到顶层的父类Bootstrap ClassLoader
,如果Bootstrap classloader
找到了,直接返回,如果没有找到,则一级一级返回,最后是由自身去查找这些对象。使用双亲委派模型,Java类随着它的加载器一起具备了一种带有优先级的层次关系,通过这种层次模型,可以避免类的重复加载,也可以避免核心类被不同的类加载器加载到内存中造成冲突和混乱,从而保证了Java核心库的安全,比如网络上传输了一个java.lang.Object
类,通过双亲模式传递到启动类当中,然后发现其Object 类早已被加载过,所以就不会加载这个网络传输过来的java.lang.Object
类,保证我们的java核心API库不被篡改,出现类似用户自
定义java.lang.Object类的情况。
使用其他方式可以加载类的二进制数据(在加载中有做简单介绍):
- 1.从本地文件系统加载class文件。
- 2.从JAR包中加载class文件,如JAR包的数据库启驱动类。
- 3.通过网络加载class文件。
- 4.把一个Java源文件动态编译并执行加载。
ClassLoader类加载器
一切的Java类都必须经过JVM加载后才能运行,而ClassLoader
的主要作用就是Java类文件的加载。类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)
、Extension ClassLoader(扩展类加载器)
、App ClassLoader(系统类加载器)
,AppClassLoader
是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader
加载类,ClassLoader.getSystemClassLoader()
返回的系统类加载器也是AppClassLoader
。
当我们获取类加载器时返回了一个null值,例如java.io.File.class.getClassLoader()
将返回一个null对象,因为java.io.File
类在JVM初始化的时候会被Bootstrap ClassLoader(引导类加载器)
加载(该类加载器实现于JVM层,采用C++编写),我们在尝试获取被Bootstrap ClassLoader
类加载器所加载的类的ClassLoader
时候都会返回null
。
ClassLoader
类有如下核心方法:
- 1.loadClass(String className),根据名字加载一个类。作用是从已加载的类、父加载器位置寻找类(即双亲委派机制),在前面没有找到的情况下,调用当前ClassLoader的
findClass()
方法; - 2.defineClass(String name, byte[] b, int off, int len),将一个字节流定义为一个类。处理传入的字节码,返回一个Class类型的对象。
- 3.findClass(String name),查找一个类。根据URL指定的方式来加载类的字节码,其中会调用
defineClass()
- 4.findLoadedClass(String name),在已加载的类中,查找一个类。
- 5.resolveClass(链接指定的Java类)
ClassLoader类加载流程
这里我们借用javasec的流程来进行学习
类动态加载方式
Java类加载方式为显示、隐式两种,其中使用JAVA反射方式或ClassLoader来来动态加载一个类对象是显示的方式,使用类名.方法名()或new的方式为隐式,显示类加载方式可以理解为类动态加载,我们可以定义类加载器去加载任意的类。
// 反射加载TestHello示例
Class.forName("com.alexsel.TestHello");
// ClassLoader加载TestHello示例
this.getClass().getClassLoader().loadClass("com.alexsel.TestHello");
自定义类加载器过程
我们可以使用java.lang.ClassLoader
类加载器的loadClass()、findLoadedClass()、findClass()、defineClass()方法来创建属于自己的类加载方式,自定义加载分为三个步骤
- 1.编写一个类继承ClassLoader抽象类;
- 2.重写findClass方法;
- 3.在findClass方法中调用defineClass()方法即可实现自定义ClassLoader;
接下来我们就尝试使用写一个自己的类加载器来实现加载自定义字节码(这里以加载com.alexsel.test.TestHello
)并调用hello方法。
如果com.alexsel.test.TestHello
类存在的情况下,可以使用如下方法实现hello调用
TestHello th = new TestHello();
String str = th.hello();
System.out.println(str);
如果classPath
中不存在com.alexsel.test.TestHello
类,那我们可以使用自定义类加载器的方式重写findClass方法,然后调用defineClass
方法时传入TestHello
类的字节码的方式来向JVM中定义一个TestHello类,最后通过反射机制就可以调用TestHello
类的hello
方法。
既然要加载TestHello
类的字节码,我们就需要先创建TestHello
类并获取其字节码
创建TestHello
public class TestHello {
public String Hello(){
String Hello = "Hello!";
System.out.println(Hello);
return Hello;
}
}
接着将这个类文件编译为class文件
javac .\TestHello.java
接着输出文件的字节码
public class MyUlits {
public static void main(String[] args) throws IOException {
InputStream file = new FileInputStream("src/com/alexsel/test/TestHello.class");
// System.out.println(file.available());
byte[] bytes = IOUtils.readFully(file,file.available());
System.out.println(Arrays.toString(bytes));
}
}
然后编写自定义加载类
package com.alexsel.test;
import java.lang.reflect.Method;
public class MyClassLoader extends ClassLoader {
//所要加载的类名
private static String testClassName = "com.alexsel.test.TestHello";
//TestHello类字节码
private static byte[] testClassBytes = {
-54, -2, -70, -66, 0, 0, 0, 61, 0, 29, 10, 0, 2, 0, 3, 7, 0, 4, 12, 0, 5, 0, 6, 1, 0, 16, 106,
97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86,
8, 0, 8, 1, 0, 6, 72, 101, 108, 108, 111, 33, 9, 0, 10, 0, 11, 7, 0, 12, 12, 0, 13, 0, 14, 1, 0, 16, 106, 97, 118, 97, 47, 108,
97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 3, 111, 117, 116, 1, 0, 21, 76, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114,
105, 110, 116, 83, 116, 114, 101, 97, 109, 59, 10, 0, 16, 0, 17, 7, 0, 18, 12, 0, 19, 0, 20, 1, 0, 19, 106, 97, 118, 97, 47, 105,
111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 1, 0, 7, 112, 114, 105, 110, 116, 108, 110, 1, 0, 21, 40, 76,
106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 7, 0, 22, 1, 0, 31, 99, 111, 109, 47, 97,
108, 101, 120, 115, 101, 108, 47, 116, 101, 115, 116, 99, 108, 97, 115, 115, 47, 84, 101, 115, 116, 72, 101, 108, 108, 111, 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, 72, 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, 14, 84, 101, 115, 116, 72, 101, 108, 108, 111, 46, 106, 97, 118, 97, 0, 33, 0, 21, 0, 2, 0, 0, 0, 0,
0, 2, 0, 1, 0, 5, 0, 6, 0, 1, 0, 23, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 24, 0, 0, 0, 6, 0, 1, 0, 0,
0, 3, 0, 1, 0, 25, 0, 26, 0, 1, 0, 23, 0, 0, 0, 44, 0, 2, 0, 2, 0, 0, 0, 12, 18, 7, 76, -78, 0, 9, 43, -74, 0, 15, 43, -80, 0, 0, 0, 1,
0, 24, 0, 0, 0, 14, 0, 3, 0, 0, 0, 5, 0, 3, 0, 6, 0, 10, 0, 7, 0, 1, 0, 27, 0, 0, 0, 2, 0, 28
};
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//只处理TestHello类
if(name.equals(testClassName)){
//将指定的字节流定义为类
return defineClass(testClassName,testClassBytes,0,testClassBytes.length);
}
return super.findClass(name);
}
public static void main(String[] args) {
//创建自定义类加载器
MyClassLoader loader = new MyClassLoader();
try{
//使用自定义的类加载器加载TestHello
Class testClass = loader.loadClass(testClassName);
//反射创建TestHello类,等价于 TestHello t = new TestHello();
Object testInstance = testClass.newInstance();
//反射获取hello方法
Method method = testInstance.getClass().getMethod("Hello");
//反射调用hello方法,,等价于 String str = obj.Hello();
String str = (String)method.invoke(testInstance);
System.out.println(str);
} catch (Exception e) {
e.printStackTrace();
}
}
}
加载类触发代码的方式
我们上面提到过显示加载和隐式加载是有区别的,显示加载时(反射)可以触发该类的初始化从而调用静态代码块执行,因为使用java.lang.reflect
对类进行反射调用时,如果该类没有初始化会先进行类的初始化。隐式加载如new,ClassLoader.getSystemClassLoader.loadClass()
,不会初始化类也就不执行静态代码块中的内容。
几种可以加载恶意类的方法:
- 利用静态代码块,在类的加载环节之一【初始化】时触发(重点)
---触发方法Class.forName()
- 利用构造函数,在类实例化时触发(当然,实例化前必然先初始化)
---触发方法newInstance(), new Evil()
- 利用自定义函数,在函数被调用时触发
--- 触发方法 xxx.fun() m.invoke()
- 利用接口的重写方法,在函数调用时触发
另外,通过类的加载流程可知:凡是能触发构造函数中代码的方法,都能触发静态代码块中的代码;凡是能触发自定义动态函数中代码的方法,都能触发静态代码块中的方法。
测试代码:
EvilClassTest类
public class EvilClassTest {
public static void main(String[] args) {
//触发方式1
try {
//这里只会执行静态代码块中的内容
Class<?> test1 = Class.forName("com.alexsel.testEvilClass.evilClazz");
//这里触发构造函数中的命令
evilClazz cs = (evilClazz) test1.newInstance();
//只会执行静态代码块中的命令,但它可以执行指定类加载器,更为灵活
Class<?> test2 = Class.forName("com.alexsel.testEvilClass.evilClazz",true,ClassLoader.getSystemClassLoader());
//这里触发构造函数中的命令
evilClazz cs1 = (evilClazz) test2.newInstance();
//触发方式2 xxxClassLoader().loadClass()
//这里不会触发静态代码,因为是隐式加载的方式
Class<?> c1 = ClassLoader.getSystemClassLoader().loadClass("com.alexsel.testEvilClass.evilClazz");
c1.newInstance();//这里触发静态代码块后,触发构造函数
//触发方式3,本质上和2是一样的
new evilClazz();//会执行静态代码块和构造函数中的代码
//触发方式4
new evilClazz().fun();//静态代码,构造函数,自定义函数都会触发。
//凡是能触发构造函数中代码的方法,都能触发静态代码块中的代码;凡是能触发自定义动态函数中代码的方法,都能触发静态代码块中的方法。
} catch (Exception e) {
e.printStackTrace();
}
}
}
evilClazz类
public class evilClazz implements ObjectFactory {
//静态代码块命令执行
static
{
try {
Runtime.getRuntime().exec("explorer.exe");
} catch (IOException e) {
e.printStackTrace();
}
}
//构造函数命令执行
evilClazz(){
try{
Runtime.getRuntime().exec("calc");
}catch(Exception e){
e.printStackTrace();
}
}
//自定义函数
public void fun() {
try{
Runtime.getRuntime().exec("notepad.exe");
}catch(Exception e){
e.printStackTrace();
}
}
//实现了ObjectFactory接口,重写该方法
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
System.out.println("123123");
try {
Runtime.getRuntime().exec("mstsc.exe");
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
参考文章
https://javasec.org/javase/ClassLoader/