还是大部分放在学习开发上面,但是也是引入了Java安全的知识,二者同步学习吧也是。
前言
JVM(Java虚拟机)中的动态类加载是一种在运行时加载类的机制,它允许应用程序在不重新启动的情况下加载新的类。它对Java应用程序的灵活性和扩展性至关重要。
什么是Java的“字节码”
严格来说,Java字节码(ByteCode)其实仅仅指的是Java虚拟机执行使用的一类指令,通常被存储 在.class文件中。 众所周知,不同平台、不同CPU的计算机指令有差异,但因为Java是一门跨平台的编译型语言,所以这 些差异对于上层开发者来说是透明的,上层开发者只需要将自己的代码编译一次,即可运行在不同平台 的JVM虚拟机中。 甚至,开发者可以用类似Scala、Kotlin这样的语言编写代码,只要你的编译器能够将代码编译成.class文件,都可以在JVM虚拟机中运行:
类加载机制
JVM(Java虚拟机)的类加载机制是指JVM在运行Java程序时,如何加载、连接和初始化类的过程。这个机制保证了Java程序在运行时能够正确地加载所需的类。
JVM的类加载机制主要分为以下三个阶段:
加载(Loading)
加载是类加载的第一个阶段。在加载阶段,类加载器将.class文件(字节码文件)读入内存,并转化成运行时数据结构(也称为方法区)。加载阶段不会执行类的初始化,仅仅是将类的二进制数据读取到内存中。
Java类加载器
类加载器,顾名思义,就是用来实现类的加载操作。每个类加载器都有一个独立的类名称空间,就是说每个由该类加载器加载的类,都在自己的类名称空间,如果要比较两个类是否“相等”,首先这两个类必须在相同的类命名空间,即由相同的类加载器加载(即对于任何一个类,都必须由该类本身和加载它的类加载器一起确定其在JVM中的唯一性),不是同一个类加载器加载的类,不会相等。
- 启动类加载器(Bootstrap Class Loader):这个类使用C++开发(所有的类加载器中,唯一使用C++开发的类加载器),用来加载
/lib目录中jar和tools.jar或者使用 -Xbootclasspath 参数指定的类。 - 扩展类加载器(Extension Class Loader):定义为misc.Launcher$ExtClassLoader,用来加载
/lib/ext目录或者使用java.ext.dir指定的类。 - 应用程序类加载器(Application Class Loader):定义为misc.Launcher$AppClassLoader,用来加载用户类路径下面(classpath)下面所有的类,一般情况下,该类是应用程序默认的类加载器。
- 用户自定义类加载器(User Class Loader):用户自定义类加载器,一般没有必要,后面我们会专门来一部分介绍该类型的类加载器。
双亲委派模型
双亲委派模型,是从 Java1.2 开始引入的一种类加载器模式,在Java中,类的加载操作通过java.lang.ClassLoader中的loadClass()方法完成,咱们首先看看该方法的实现
1 | protected Class<?> loadClass(String name, boolean resolve) |
接收到一个类加载请求后,首先判断该类是否有加载,如果已经加载,则直接返回;
如果尚未加载,首先获取父类加载器,如果可以获取父类加载器,则调用父类的loadClass()方法来加载该类,如果无法获取父类加载器,则调用启动器加载器来加载该类;
判断该类是否被父类加载器或者启动类加载器加载,如果已经加载完成则返回,如果未成功加载,则自己尝试来加载该类。
上面的描述,说明了loadClass()方法的实现,我们进一步对上面的步骤进行解释:
- 因为类加载器首先调父类加载器来进行加载,从loadClass()方法的实现,我们知道父类加载器会尝试调自己的父类加载器,直到启动类加载器,所以,任何一个类的加载,都会最终委托到启动类加载器来首先加载;
- 在前面有进行介绍,启动类加载器、扩展类加载器、应用程序类加载器,都有自己加载的类的范围,例如启动类加载器只加载JDK核心库,因此并不是父类加载器就可以都加载成功,父类加载器无法加载(一般如上面代码,抛出来ClassNotFoundException),此时会由自己加载。
总结:
双亲委派模型:如果一个类加载器收到类加载请求,会首先把加载请求委派给父类加载器完成,每个层次的类加载器都是这样,最终所有的加载请求都传动到最根的启动类加载器来完成,如果父类加载器无法完成该加载请求(即自己加载的范围内找不到该类),子类加载器才会尝试自己加载。
这样的双亲委派模型有个好处:就是所有的类都尽可能由顶层的类加载器加载,保证了加载的类的唯一性,如果每个类都随机由不同的类加载器加载,则类的实现关系无法保证,对于保证Java程序的稳定运行意义重大。
链接(Linking)
链接阶段进一步分为三个阶段:验证、准备和解析。a. 验证(Verification):在这个阶段,JVM会对加载的字节码进行校验,以确保其符合JVM规范。包括类型检查、字节码验证、符号引用验证等。b. 准备(Preparation):在准备阶段,JVM会为类的静态变量分配内存,并设置默认初始值。这里不包括使用final修饰的静态变量,因为它们已在编译期间分配了初始值。c. 解析(Resolution):解析阶段是将类中的符号引用解析成直接引用的过程,例如将类、方法、字段的引用转化为内存地址。
初始化(Initialization)
初始化阶段是类加载的最后一个阶段。在这个阶段,JVM会执行类的初始化代码,包括静态变量的赋值和静态代码块的执行。类初始化时机:当对类的静态成员进行赋值时(如静态变量的赋值)。当调用类的静态方法时。当使用new关键字实例化对象时(但实际对象的初始化在构造函数中)。当访问或者初始化一个类的子类时,父类也会被初始化。当启动Java应用程序的主类时。
这个类加载机制保证了类在首次使用时才会被加载,同时保证了类的初始化在多线程环境下的安全性。
动态类加载
动态类加载是指在程序运行时,根据需要动态地加载类或接口的过程。Java中的动态类加载主要通过Java的反射API和类加载器来实现。
它主要应用在一些需要动态扩展的应用场景,例如插件系统,或者在应用启动时需要动态决定需要加载哪些类的情况。使用动态类加载需要注意的是,最好明确需要加载的类的路径和名称,避免在运行时出现找不到类的错误。同时,对于动态加载的类,其安全性也是需要特别关注的。
工作原理
JVM(Java虚拟机)中的动态类加载是一种在运行时加载类的机制,它允许应用程序在不重新启动的情况下加载新的类。以下是JVM中动态类加载的工作原理:
类加载器层次结构:JVM中的类加载机制是分层的,通常包括以下三个主要层次:启动类加载器、扩展类加载器和应用程序类加载器。这些加载器形成了父子关系,构成了双亲委派模型。
双亲委派模型:在动态类加载中,JVM首先检查是否已经加载了所请求的类。它通过遵循双亲委派模型来执行这一检查。按照这一模型,JVM首先将类加载请求委派给父类加载器(启动类加载器、扩展类加载器),如果父类加载器无法找到该类,才会由当前类加载器(应用程序类加载器)尝试加载。
自定义类加载器:动态类加载通常涉及自定义类加载器。应用程序可以编写自定义类加载器,这些加载器可以加载不在类路径中的类。自定义类加载器必须继承自java.lang.ClassLoader类,并覆盖其中的loadClass方法来实现类加载逻辑。
类字节码的获取:在动态类加载中,通常需要获取类的字节码。这可以通过多种方式实现,例如从文件系统、网络或其他外部资源中获取。获取类字节码的方式取决于应用程序的具体需求。
类加载过程:当自定义类加载器的loadClass方法被调用时,它首先会检查是否已经加载了该类。如果已加载,它会返回已加载的类。否则,它将尝试委派给父类加载器加载。
类加载成功:如果父类加载器无法加载该类(双亲委派模型),自定义类加载器会尝试加载类字节码。一旦类字节码被加载,它可以通过defineClass方法将类定义为JVM可识别的类。
类初始化:一旦类被成功加载,JVM会执行类的初始化过程,包括执行静态初始化块和静态变量的赋值。这确保了类在使用之前已经准备好。
动态类加载允许应用程序在运行时引入新的类,这对于插件系统、热部署和动态扩展非常有用。但需要小心使用,因为错误的类加载和卸载可能导致内存泄漏或不稳定的应用程序行为。因此,在实施动态类加载时,应仔细考虑安全性和性能方面的问题。
实现方式
动态类加载是指在Java应用程序运行时,根据需要加载类的能力。这可以通过多种方式实现,以下是一些常见的动态类加载实现方式:
Java反射
Java的反射机制允许在运行时加载和操作类,方法和字段。
使用Class.forName()
方法或类的Class对象的newInstance()
方法可以动态加载和实例化类。
可以通过反射来访问和调用类的成员和方法。
优点:灵活性高,适用于各种情况。
缺点:性能开销较大,容易导致运行时错误。
实例:
DynamicClass.java
1 | package DynamicLoadingOfClasses; |
DynamicClassLoadingExample.java
1 | package DynamicLoadingOfClasses; |
上述代码中的关键步骤包括:
- 使用
Class.forName("DynamicClass")
通过类名加载DynamicClass
类。 - 使用反射创建
DynamicClass
类的实例。 - 使用反射获取并调用
hello()
方法。
自定义类加载器
前面介绍到,类加载的双亲委派模型,是推荐模型,在loadClass中实现的,并不是必须使用的模型。我们可以通过自定义类加载器,直接加载我们需要的Java类,而不委托给父类加载器。
如上图所示,我们有自定义的类加载器MyClassLoader,用来加载类MyClass,则在JVM中,会存在上面三类引用(上图忽略这三种类型对象对其他的对象的引用)。如果我们将左边的三个引用变量,均设置为null,那么此时,已经加载的MyClass将会被卸载。
自定义类加载器允许您编写自己的类加载逻辑,从外部源加载类字节码。
继承java.lang.ClassLoader类,覆盖loadClass()方法实现类的加载。
适用于特定的应用程序需求,例如插件系统。
优点:灵活性高,可以实现自定义加载逻辑。
缺点:需要小心处理类加载的双亲委派模型,容易引入类加载冲突。
例子:
1 | package DynamicLoadingOfClasses; |
在上述示例中:
CustomClassLoader
是自定义的类加载器,它继承自ClassLoader
类,并实现了findClass
方法,用于加载类字节码。loadClassData
方法用于读取类文件的字节码。- main方法演示了如何使用自定义类加载器加载
DynamicClass
类,创建实例并调用其中的方法。
URLClassLoader
利用 URLClassLoader 加载远程 class 文件
URLClassLoader
实际上是我们平时默认使用的 AppClassLoader
的父类,所以,我们解释 URLClassLoader
的工作过程实际上就是在解释默认的 Java
类加载器的工作流程。
正常情况下,Java会根据配置项 sun.boot.class.path
和 java.class.path
中列举到的基础路径(这些路径是经过处理后的 java.net.URL
类)来寻找.class文件来加载,而这个基础路径有分为三种情况:
- URL未以斜杠 / 结尾,则认为是一个JAR文件,使用
JarLoader
来寻找类,即为在Jar包中寻找.class文件 - URL以斜杠 / 结尾,且协议名是
file
,则使用FileLoader
来寻找类,即为在本地文件系统中寻找.class文件 - URL以斜杠 / 结尾,且协议名不是
file
,则使用最基础的Loader
来寻找类。
我们正常开发的时候通常遇到的是前两者,那什么时候才会出现使用 Loader 寻找类的情况呢?当然是 非 file 协议的情况下,最常见的就是 http 协议。
先简单来看看前两种情况
file 协议
我们在目录下新建一个 Calc.java 的文件。
1 | import java.io.IOException; |
接着,我们编写 URLClassLoader
的启动类
1 | package DynamicLoadingOfClasses; |
file+jar 协议
先将我们之前的 class 文件打包一下,打包为 jar 文件。
去到源 .class 文件下,别去复制的地方,运行命令
1 | jar -cvf Calc.jar Clac.class |
1 | package DynamicLoadingOfClasses; |
HTTP 协议
在 Calc.class
文件目录下执行 python3 -m http.server 9999
,起一个 http 服务。
1 | package DynamicLoadingOfClasses; |
HTTP协议也可以加载jar包
ClassLoader#defineClass
利用ClassLoader#defineClass
直接加载字节码
我们认识到了如何利用URLClassLoader
加载远程class文件,也就是字节码。其实,不管是加 载远程class文件,还是本地的class或jar文件,Java都经历的是下面这三个方法调用
其中:
loadClass
的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机 制),在前面没有找到的情况下,执行findClass
findClass
的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在 本地文件系统、jar包或远程http服务器上读取字节码,然后交给defineClass
defineClass
的作用是处理前面传入的字节码,将其处理成真正的Java类 所以可见,真正核心的部分其实是defineClass
,他决定了如何将一段字节流转变成一个Java类,Java 默认的ClassLoader#defineClass
是一个native方法,逻辑在JVM的C语言代码中。
进 ClassLoader
类当中,去看一看 DefineClass
是怎么被调用的。
如果类名被找到了则
解释一下 defineClass
name
为类名,b
为字节码数组,off
为偏移量,len
为字节码数组的长度。
因为系统的 ClassLoader#defineClass 是一个保护属性,所以我们无法直接在外部访问。因此可以反射调用 defineClass()
方法进行字节码的加载,然后实例化之后即可弹 shell
演示如何让系统的 defineClass
来直接加载字节码:
1 | package DynamicLoadingOfClasses; |
反射设置:
- 从
ClassLoader
类中获取defineClass
方法,使用反射实现。 setAccessible(true)
允许访问defineClass
方法,该方法通常是不可访问的。
Base64解码:
- 使用
Base64.getDecoder()
解码提供的base64编码的字符串。
动态类定义:
- 调用
defineClass
方法以使用解码后的字节码定义类”Hello”。 - 将结果的
Class
对象赋给变量hello
注意一点,在 defineClass
被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造 函数,初始化代码才能被执行。而且,即使我们将初始化代码放在类的static块中,在 defineClass
时也无法被直接调用到。所以,如果我们要使用 defineClass
在目 标机器上执行任意代码,需要想办法调用构造函数。
这里,因为系统的 ClassLoader#defineClass
是一个保护属性,所以我们无法直接在外部访问,不得 不使用反射的形式来调用。 在实际场景中,因为defineClass
方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我 们常用的一个攻击链 TemplatesImpl
的基石。
TemplatesImpl
虽然大部分上层开发者不会直接使用到defineClass
方法,但是Java底层还是有一些类用到了它,这就是 TemplatesImpl
。
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
这个类中定义了一个内部类 TransletClassLoader
1 | static final class TransletClassLoader extends ClassLoader { |
可以看到在TransletClassLoader
中的defineClass
调用了此方法
再往上TemplatesImpl
的defineTransletClasses
方法
再往上TemplatesImpl
的getTransletInstance
方法
再往上TemplatesImp
l的newTransformer
方法,此时已经是public方法了外面可以直接调用
调用链如下
1 | TemplatesImpl#newTransformer() ->TemplatesImpl#getTransletInstance() -> |
看个例子:
HelloTemplateImpl.java
1 | package DynamicLoadingOfClasses; |
其中, setFieldValue
方法用来设置私有属性,可见,这里我设置了三个属性: _bytecodes
、 _name
和 _tfactory
。 _bytecodes
是由字节码组成的数组; _name
可以是任意字符串,只要不为null即可; _tfactory
需要是一个 TransformerFactoryImpl
对象,因为 TemplatesImpl#defineTransletClasses()
方法里有调用到 _tfactory.getExternalExtensionsMap()
,如果是null会出错。 另外,值得注意的是, TemplatesImpl
中对加载的字节码是有一定要求的:这个字节码对应的类必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
的子类。所以我们需要构造一个特殊的类HelloTemplatesImpl.java
1 | package DynamicLoadingOfClasses; |
它继承了 AbstractTranslet
类,并在构造函数里插入Hello的输出。将其编译成字节码,即可被 TemplatesImpl
执行了:
在HelloTemplatesImpl.java里添加方法然后在setFieldValue(obj, "_name", "方法名");
修改方法名即可执行恶意命令。
在多个Java反序列化利用链,以及fastjson、jackson的漏洞中,都曾出现过 TemplatesImpl
的身影,以后的学习还要多多注意噢。
利用BCEL ClassLoader加载字节码
BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为 被Apache Xalan所使用,而Apache Xalan又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的原生库中。BCEL ClassLoader在Fastjson等漏洞的利用链构造时都有被用到,其实这个类和前面的 TemplatesImpl 都出自于同一个第三方库。但是在Java 8u251的更新中,这个ClassLoader被移除了,所以就先不学习了,如果以后遇到再学习,毕竟太久远了。
Java类的卸载
在Java中,每个类都有相应的Class Loader,同样的,每个实例对象也会有相应的类,当满足如下三个条件时,JVM就会卸载这个类:
- 该类所有实例对象不可达
- 该类的Class对象不可达
- 该类的Class Loader不可达
那么,上面示例对象、Class对象和类的Class Loader直接是什么关系呢?
在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。而一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()
方法,就能获得它的类加载器。所以,Class实例和加载它的加载器之间为双向引用关系。
一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()
方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。
Java虚拟机自带的类加载器(前面介绍的三种类加载器)在JVM运行过程中,会始终存在,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。因此,由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。
那么,我们是不是就完全不能在Java程序运行过程中,动态修改我们使用的类了吗?答案是否定的!根据上面的分析,通过Java虚拟机自带的类加载器加载的类无法卸载,我们可以自定义类加载器来加载Java程序,通过自定义类加载器加载的Java类,是可以被卸载的。
Java类的热替换
类的热替换,是指程序在运行的时候,对内存方法区中类定义进行替换。因为堆中的 Class 对象是对方法区对象的封装,所以可以理解为对Class对象的替换,当一个Class被替换后,系统无需重启,替换的类会立即生效。
说明:在类的加载过程中,类的结构信息会存在在JVM的方法区中,类的具体对象会在堆中分配内存空间。
参考:
Java类动态加载和热替换-云社区-华为云 (huaweicloud.com)
Java反序列化基础篇-05-类的动态加载 | Drunkbaby’s Blog (drun1baby.top)
JVM动态类加载深度剖析:Java程序员的高效编程秘诀_java动态加载类的框架了解哪些-CSDN博客
Java单向代码执行链配合的动态代码上下文执行 - Ruilin (rui0.cn)
知识星球 | 深度连接铁杆粉丝,运营高品质社群,知识变现的工具 (zsxq.com)