Java类加载

本文最后更新于:1 个月前

静态代码块的执行

在上文JDK动态代理的代码中,在Person类定义中添加以下几点:

一个静态属性id,一个静态方法,一个静态代码块,一个构造代码块

1
2
3
4
5
6
7
8
9
10
public static int id;
public static void staticAction(){
System.out.println("静态方法调用");
}
static {
System.out.println("静态代码块调用");
}
{
System.out.println("构造代码块利用");
}

对静态属性调用,会触发静态代码块

1
Person.id = 1;

对静态方法调用,也会触发静态代码块

1
Person.staticAction();

对类进行初始化,两种代码块都被调用:

1
new Person();

class的获取

在java中,获取一个类的class,有下面几种方式:

这样只进行了加载,没进行初始化,因此没有任何输出

1
Class<?> c = Person.class;

直接通过loadClass方法也没进行初始化,无输出

1
2
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<?> c = classLoader.loadClass("com.potato.Entity.Person");

这样就能调用了静态代码块,也就是进行了初始化的操作

1
Class<?> c = Class.forName("Person");

跟进forName,能够看到下面调用了个forName0,其中第二个参数为true,意味是否初始化,默认forName传入一个参数就为true

在Class类中找到一个重载的forName方法

上面的代码第三个参数是一个ClassLoader对象,跟进后发现是一个抽象类,无法被实例化,

但是其有一个静态方法getSystemClassLoader()能获取到一个ClassLoader对象

构造以下代码(forName0的第四个参数caller是内部自己定义的,不是传入的,因此重载后的forName方法实际上只有三个参数)

第三个参数传入false,意为不进行初始化,也就不调用静态块,结果无输出

1
2
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<?> c = Class.forName("com.potato.Entity.Person",false,classLoader);

loadClass类加载过程分析

上文提到了一种类加载的方式,通过ClassLoader对象的loadClass()方法来进行类加载,对loadClass()类加载的底层原理进行探究也许可以挖掘出其进行类加载的核心方法

对以下代码进行调试,断点打在第6行

1
2
3
4
5
6
7
8
package com.potato.ClassLoader;

public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<?> c = classLoader.loadClass("com.potato.Entity.Person");
}
}

先是来到了ClassLoader类的loadClass()方法,跟进进去

来到了AppClassLoader类下的loadClass()方法,在做了一系列的安全检查之后,走到了调用父类的loadClass()方法,留意到此处有一个findLoadedClass()方法,适用于检测类是否已经被加载过了,该方法在下面的loadClass()方法中被大量调用,

这里暂停一下,详细理解一下loadClass()方法,它的作用就是一层一层地向上委托,检测该类是否已经被加载过(对应下文的findLoadedClass()),如果加载过则直接返回,未加载则委托给父加载器来加载,递归到最顶层的加载器BootStrap ClassLoader之后,若无法加载此类,再一层一层向下委派给子类加载器来加载

继续跟进

又回到了ClassLoader重载的loadClass()方法,稍微步过跟进一下分析,410行这里显示此时parent还不是null,也就是双亲委派过程中Application ClassLoader还得向上寻找Extension ClassLoader,跟进此处的loadClass(),

跟进后还是来到了ClassLoader类的loadClass()方法下,但是此时this已经变成了ExtClassLoader了,

parent无法再找到了,直接调bootstrapClassLoader,步入跟进到最后是个native底层方法,直接步过,也是看到c是null,未在\lib下加载出Person类,此时bootstrap类加载器已经无法加载Person类了,需要将其委派给子类加载器来加载了,代码继续向下执行

来到了此处的一个findClass()方法,再对findClass()方法做个详细点的理解,findClass()的主要功能就是找到对应的class文件,然后将class文件读入内存中转换为字节码传递给后面将出现的defineClass(),得到Class对象

继续跟进一下会发现来到了URLClassLoader类下的findClass()方法,为什么会跑到这里来?

让我们回到上面的findClass()稍微跟进一下,是ClassLoader中一个需要被重写的方法,本身都没有做定义,在ExtClassLoader中调用的时候会向父类寻找findClass(),这几个类的继承关系是ClassLoader->SecureClassLoader->URLClassLoader->AppClassLoader/ExtClassLoader,因此最终也是调用了URLClassLoader的方法

第一次ExtClassLoader进行findClass未能获取到class文件,继续向下委派(进入这个doPrivileged()方法需要”强制步入”->”步过”->”强制步入”->”步入”)

到AppClassLoader,再次步入findClass()方法,此时能够看到ucp已经从路径中读取到class文件,res将传入下面的defineClass()方法进行类加载,继续跟进

URLClassLoader对defineClass()方法进行了一个重写,前面的大部分代码都是做了一些安全判断,以及从res获取字节等的功能,最重要的在于最后一行中调用的另一个格式重写的defineClass()方法,跟进一下

又来到了URLClassLoader的父类SecureClassLoader,调用其中重写的defineClass(),

继续跟进该defineClass()方法,重新回到ClassLoader中,此时各种资源,包括class文件的路径,字节码等均准备就绪,下方调用了一个defineClass1()方法,

在类中找到defineClass1()的定义,是一个native方法,无法继续跟进了,实际上最后就是在这个地方完成了类的动态加载,分析一下参数,传入一个类名,一个类文件的字节码,然后从字节码中获取到这个类

获取到class之后,自上向下的委派成功了,因此一步步地返回,返回到findClass处,已经成功获取到一个Class对象了,后续步过至结束,成功完成类加载

URLClassLoader动态类加载

先创建一个恶意类包含一个恶意静态代码块,并执行javac Test.java编译,编译后的Test.class我转储到D:\tmp下了

1
2
3
4
5
6
7
8
9
public class Test {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

实例化一下,成功弹出计算器

此处切记传入参数结尾需要是两条反斜杠,否则tmp将不被认为是目录的一部分而是被看做jar包

也可加载远程类:在目录下启动一个http服务,

依旧成功弹出计算器,远程类的加载大大增加了攻击面

更换高版本jdk运行依旧可行,

defineClass()触发动态类加载

留意到defineClass()方法在ClassLoader类中是受保护的方法,因此想调用须通过反射

几个参数,第一个是类名,第二个是字节码,第三个是字节码起始读取位,第四个是读取长度

最终代码:

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.potato.ClassLoader;

import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
// ClassLoader classLoader = ClassLoader.getSystemClassLoader();
// Class<?> c = classLoader.loadClass("com.potato.Entity.Person");
// System.out.println(classLoader);
ClassLoader classLoader = ClassLoader.getSystemClassLoader();

Class<?> c = ClassLoader.class;
Method defineClassMethod = c.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true);
byte[] bytes = Files.readAllBytes(Paths.get("D:\\tmp\\Test.class"));
Class<?> cl = (Class<?>) defineClassMethod.invoke(classLoader,"Test", bytes,0,bytes.length);
cl.newInstance();

}
}

成功召唤出计算器

用另一种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.potato.ClassLoader;

import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
// ClassLoader classLoader = ClassLoader.getSystemClassLoader();
// Class<?> c = classLoader.loadClass("com.potato.Entity.Person");
// System.out.println(classLoader);
ClassLoader classLoader = ClassLoader.getSystemClassLoader();

Class<?> c = ClassLoader.class;
Method defineClassMethod = c.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true);
byte[] bytes = Files.readAllBytes(Paths.get("D:\\tmp\\Test.class"));
Class<?> cl = (Class<?>) defineClassMethod.invoke(classLoader, bytes,0,bytes.length);
cl.newInstance();

}
}

相比URLClassLoader:

优点:无需出网

缺点:defineClass是受保护的,反序列化过程中比较少见一个方法中能够直接调用该方法的

Unsafe调用动态类加载

参考文章:

https://blog.csdn.net/weixin_36586120/article/details/117457014

https://segmentfault.com/a/1190000023876273