java类加载

静态代码块的执行

在上文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

image-20241024212508541

我们跟进forName0

image-20241024213018654

第一个参数是类名,第二个参数是是否初始化,第三个参数是类加载器,第四个参数不是很重要

在Class.java中继续查看可以发现还有一个重载的forName(完整版)

image-20241024213356007

通过该函数我们可以控制类是否进行初始化,跟进第三个参数可以发现类ClassLoader是一个抽象类,不能够直接进行实例化

image-20241024214016468

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

image-20241024214511280

于是我们来测试,自己输入相关参数,不让类进行初始化,也就不会调用静态方法

image-20241025101510350

但是只要将其实例化之后,就都会正常调用

1
2
3
4
5
6
7
8
9
10
package com.sherlock;

public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
// Class.forName("com.sherlock.Person");
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class<?> c = Class.forName("com.sherlock.Person", false, cl);
c.newInstance();
}
}

loadClass类加载过程分析

上面有提到一种类加载的方式,通过ClassLoader对象的loadClass()方法来进行类加载,对loadClass()类加载的底层原理进行探寻,看看写在其他地方的类能不能够被加载,从而实现任意类加载,可以做更多的事

image-20241025103109087

让我们来调试一下,跟进loadClass方法(这里需要强制步入),来到了ClassLoader类的loadClass()方法,继续跟进进去

image-20241025103419064

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

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

image-20241025104539763

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

image-20241025105222567

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

image-20241025105444071

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

image-20241025105945269

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

image-20241025110151502

我们强制步入看看该函数,我们会发现跑到了URLClassLoader类下的findClass函数。为什么呢

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

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

image-20241025111953094

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

image-20241025112114018

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

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

image-20241025112213285

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

image-20241025112715823

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

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

image-20241025112812265

总结一下,其实就是

ClassLoader->SecureClassLoader->URLClassLoader->AppClassLoader

loadClass->findClass(重写的方法)->defineClass(从字节码加载类)

URLClassLoader动态类加载

其实就是可以通过输入一个URL实现类加载

首先我们新建一个类文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
import java.io.IOException;

public class Test {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

然后将文件编译后可以在target目录下面找到文件Test.class,将该文件剪贴至我自己创建的目录tmp下面

利用URLClassLoader进行类加载,并实例化后可以弹出计算机

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

但是呢file里面我们是本地调用,一般是没有什么用的,但是呢我们可以用http协议来

在tmp目录下面起一个http服务:python -m http.server 9999

然后通过该url加载该目录下面的Test类,成功弹出计算器

image-20241025161321035

这说明了我们同样可以远程加载类,大大增加了攻击面

defineClass()触发动态类加载

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

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

image-20241025162341677

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.sherlock;

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

public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader cl = ClassLoader.getSystemClassLoader();
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("E:\\mycode\\tmp\\Test.class"));
Class c = (Class)defineClass.invoke(cl, "Test", code, 0, code.length);
c.newInstance();
}
}

我们测试一下,成功弹窗

image-20241025164340596

用另一种defineClass()同样可行(无需类名)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.sherlock;

import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class c = Unsafe.class;
Field theUnsafeField = c.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
byte[] code = Files.readAllBytes(Paths.get("E:\\mycode\\tmp\\Test.class"));
Class c2 = (Class)unsafe.defineClass("Test", code, 0, code.length,cl,null);
c2.newInstance();
}
}

相比URLClassLoader:

优点:无需出网

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

引用

https://blog.potatowo.top/2024/09/19/Java%E7%B1%BB%E5%8A%A0%E8%BD%BD/