java类加载
java类加载
Sherlock静态代码块的执行
在上文JDK动态代理的代码中,在Person类定义中添加以下几点:
一个静态属性id,一个静态方法,一个静态代码块,一个构造代码块
1 | public static int id; |
对静态属性调用,会触发静态代码块
1 | Person.id = 1; |
对静态方法调用,也会触发静态代码块
1 | Person.staticAction(); |
对类进行初始化,两种代码块都被调用:
1 | new Person(); |
class的获取
在java中,获取一个类的class,有下面几种方式:
这样只进行了加载,没进行初始化,因此没有任何输出
1 | Class<?> c = Person.class; |
直接通过loadClass方法也没进行初始化,无输出
1 | ClassLoader classLoader = ClassLoader.getSystemClassLoader(); |
这样就能调用了静态代码块,也就是进行了初始化的操作
1 | Class<?> c = Class.forName("Person"); |
跟进forName,能够看到下面调用了个forName0(它被调用来执行真正的类加载操作),其中第二个参数为true,意味是否初始化,默认forName传入一个参数就为true
我们跟进forName0
第一个参数是类名,第二个参数是是否初始化,第三个参数是类加载器,第四个参数不是很重要
在Class.java中继续查看可以发现还有一个重载的forName(完整版)
通过该函数我们可以控制类是否进行初始化,跟进第三个参数可以发现类ClassLoader是一个抽象类,不能够直接进行实例化
但是其有一个静态方法getSystemClassLoader()能获取到一个ClassLoader对象
于是我们来测试,自己输入相关参数,不让类进行初始化,也就不会调用静态方法
但是只要将其实例化之后,就都会正常调用
1 | package com.sherlock; |
loadClass类加载过程分析
上面有提到一种类加载的方式,通过ClassLoader对象的loadClass()方法来进行类加载,对loadClass()类加载的底层原理进行探寻,看看写在其他地方的类能不能够被加载,从而实现任意类加载,可以做更多的事
让我们来调试一下,跟进loadClass方法(这里需要强制步入),来到了ClassLoader类的loadClass()方法,继续跟进进去
来到了AppClassLoader类下的loadClass()方法,在做了一系列的安全检查之后,走到了调用父类的loadClass()方法,留意到此处有一个findLoadedClass()方法,适用于检测类是否已经被加载过了,该方法在下面的loadClass()方法中被大量调用,
这里暂停一下,详细理解一下loadClass()方法,它的作用就是一层一层地向上委托,检测该类是否已经被加载过(对应下文的findLoadedClass()),如果加载过则直接返回,未加载则委托给父加载器来加载,递归到最顶层的加载器BootStrap 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对象了,后续步过至结束,成功完成类加载
总结一下,其实就是
ClassLoader->SecureClassLoader->URLClassLoader->AppClassLoader
loadClass->findClass(重写的方法)->defineClass(从字节码加载类)
URLClassLoader动态类加载
其实就是可以通过输入一个URL实现类加载
首先我们新建一个类文件,内容如下:
1 | import java.io.IOException; |
然后将文件编译后可以在target目录下面找到文件Test.class,将该文件剪贴至我自己创建的目录tmp下面
利用URLClassLoader进行类加载,并实例化后可以弹出计算机
(此处切记传入参数结尾需要是两条反斜杠,否则tmp将不被认为是目录的一部分而是被看做jar包)
但是呢file里面我们是本地调用,一般是没有什么用的,但是呢我们可以用http协议来
在tmp目录下面起一个http服务:python -m http.server 9999
然后通过该url加载该目录下面的Test类,成功弹出计算器
这说明了我们同样可以远程加载类,大大增加了攻击面
defineClass()触发动态类加载
留意到defineClass()方法在ClassLoader类中是受保护的方法,因此想调用须通过反射
几个参数,第一个是类名,第二个是字节码,第三个是字节码起始读取位,第四个是读取长度
完整代码如下:
1 | package com.sherlock; |
我们测试一下,成功弹窗
用另一种defineClass()同样可行(无需类名)
1 | package com.sherlock; |
相比URLClassLoader:
优点:无需出网
缺点:defineClass是受保护的,反序列化过程中比较少见一个方法中能够直接调用该方法的
引用
https://blog.potatowo.top/2024/09/19/Java%E7%B1%BB%E5%8A%A0%E8%BD%BD/