Java中动态加载字节码的方法
1、利用 URLClassLoader 加载远程class文件
1 | public static void main(String[] args) { |
2、利用 ClassLoader#defineClass 直接加载字节码
2.1 双亲委派模型
BootstrapClassLoader:启动类加载器/根加载器,负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.*.*都在里面。这个 ClassLoader 比较特殊,它其实不是一个ClassLoader实例对象,而是由C代码实现。用户在实现自定义类加载器时,如果需要把加载请求委派给启动类加载器,那可以直接传入null作为 BootstrapClassLoader。
ExtClassLoader:扩展类加载器,负责加载 JVM 扩展类,扩展 jar 包位于 JAVA_HOME/lib/ext/*.jar 中,库名通常以 javax 开头。
AppClassLoader,应用类加载器/系统类加载器,直接提供给用户使用的ClassLoader,它会加载 ClASSPATH 环境变量或者 java.class.path 属性里定义的路径中的 jar 包和目录,负责加载包括开发者代码中、第三方库中的类。AppClassLoader 可以由 ClassLoader 类提供的静态方法 getSystemClassLoader() 得到。
ClassLoader.getParent() 可以获取用于委派的父级class loader,通常会返回null来表示bootstrap class loader。
2.2 双亲委派模型的代码实现
如上图,实现双亲委派的代码都集中在 java.lang.ClassLoader#loadClass()方法中,其逻辑如下:
先检查是否已被加载过;
若没有加载过则调用父加载器的loadClass()方法;
若父加载器为null则默认使用启动类加载器(Bootstrap ClassLoader)作为父加载器;
如果父加载器加载类失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。(findClass()最终会调用defineClass()加载字节码)
:::tips
注意:
这里的“双亲”,指的并不是有两个父加载器,可能仅仅是英文“parent”的翻译。每个ClassLoader最多有一个父加载器,也就是parent变量。“双亲委派机制”指的就是优先让父加载器去加载类,如果父加载器没有成功加载到类,才由本ClassLoader加载。
这样可以保证安全性,防止系统类被伪造(比如自定义java.lang.Object类,肯定是无法运行的)。
对于Java程序来讲,一般的类是由AppClassLoader来加载的,而系统类则是由BootStrapClassLoader加载的。由于BootStrapClassLoader是在native层实现的,所以调用系统类的getClassLoader()方法会返回null。
:::
2.3 自定义ClassLoader
java.lang.ClassLoader是一个抽象类。创建一个继承自ClassLoader的类,并重写findClass()方法实现类的加载,即可完成自定义ClassLoader。示例如下:
1 | public class MyClassLoader extends ClassLoader { |
3、URLClassLoader (远程加载 Class 文件)
这个加载方式最大的特点就是可以远程进行加载,我们在 VPS 上启一个 http 服务,把恶意类放在 http 服务下,可以实现远程加载。
1 | import java.net.URL; |
4、defineClass() 加载字节码
不管是加载远程class文件,还是本地class文件,Java都经历了下面三个方法的调用:
:::tips
其中:
loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机 制),在前面没有找到的情况下,执行 findClass
findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在 本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass
defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类
所以可见,真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java 默认的 ClassLoader#defineClass 是一个native方法,逻辑在JVM的C语言代码中。
:::
使用ClassLoader#defineClass()直接加载类字节码的示例:
1 | package com.govuln; |
ClassLoader#defineClass()被调用时,Class对象并不会被初始化,只有显示调用其构造方法,初始化代码才能被执行。即使将初始化代码放在类的static块中,在defineClass时也无法被直接调用到。因此,如果要使用defineClass()在目标机器上执行任意代码,需要想办法调用构造方法。
5、利用 TemplatesImpl 加载字节码
这里还是记录下主要是从下面这两篇文章抄的
这个漏洞的触发点在 TemplatesImpl.newTransformer()
方法中,我们来看具体的代码。
跟到getTransletInstance里去看看
这里要求_name不能为null否则就直接返回null了_class必须为null否则进不去defineTransletClasses方法里
这里主要在这个for循环里有defineClass方法里跟进去看看
继续跟进去
最终还是调用java原生的defineclass加载类,记录下调用链
1 | TemplatesImpl.newTransformer() |
参考:Java安全漫谈 - 13.Java中动态加载字节码的那些方法.pdf
https://www.lianqing.xyz/?p=561
6、Javassit库:
Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。
能够在运行时定义新的Java类,在JVM加载类文件时修改类的定义。
Javassist类库提供了两个层次的API,源代码层次和字节码层次。源代码层次的API能够以Java源代码的形式修改Java字节码。字节码层次的API能够直接编辑Java类文件。
向Maven的Pom.xml文件中,添加以下字段,以导入依赖:
1 | <!-- https://mvnrepository.com/artifact/javassist/javassist --> |
在这个包中,主要调用到的方法是:
ClassPool:
ClassPool是CtClass对象的容器,它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便以后使用,其中键名是类名称,值是表示该类的CtClass对象。
常用方法:
- static ClassPool getDefault():返回默认的ClassPool,一般通过该方法创建我们的ClassPool;
- ClassPath insertClassPath(ClassPath cp):将一个ClassPath对象插入到类搜索路径的起始位置;
- ClassPath appendClassPath:将一个ClassPath对象加到类搜索路径的末尾位置;
- CtClass makeClass:根据类名创建新的CtClass对象;
- CtClass get(java.lang.String classname):从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用;
CtClass:
CtClass类表示一个class文件,每个CtClass对象都必须从ClassPool中获取。
常用方法:
- void setSuperclass(CtClass clazz):更改超类,除非此对象表示接口;
- byte[] toBytecode():将该类转换为类文件;
- CtConstructor makeClassInitializer():制作一个空的类初始化程序(静态构造函数);
示例:
获取字节码:
1 | ClassPool pool = ClassPool.getDefault(); |
7、BCEL加载字节码
这里因为学习fastjson要用到bcel字节码所以这里学习下如何利用bcel加载字节码
这里我们先准备一个恶意类
1 | package com.ocean; |
来跟一下调用流程
跟到loadclass里会调用这个creatClass方法跟进去
在createClass()
中,通过subString()
截取$$BCEL$$
后的字符串,并调用Utility.decode
进行相应的解码并最终返回改字节码的bytes数组(decode方法参数uncompress用来标识是否为zip流,当为true时走zip流解码)。之后生成Parser
解析器并调用parse()
方法进行解析,并生成JavaClass
对象。
之后在调用defineclass方法进行加载,后面就是调用newinstance进行初始化加载静态代码块了