Featured image of post 动态加载字节码和和javassist

动态加载字节码和和javassist

恶意类加载的原理和利用以及javassist的使用

参考文章:https://blog.csdn.net/mole_exp/article/details/122768814

Java中动态加载字节码的方法

利用 URLClassLoader 加载远程class文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static void main(String[] args) {
    try {
        //使用file协议在本地寻找指定.class文件
        //URL[] urls = new URL[]{new URL("file:///Users/fa1c0n/codeprojects/IdeaProjects/misc-classes/src/main/java/")};
        //使用http协议到远程地址寻找指定.class文件
        URL[] urls = new URL[]{new URL("http://127.0.0.1:8000/")};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        Class clazz = urlClassLoader.loadClass("Exploit");
        clazz.newInstance();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

利用 ClassLoader#defineClass 直接加载字节码

类加载 - 双亲委派模型

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。

双亲委派模型的代码实现

如上图,实现双亲委派的代码都集中在 java.lang.ClassLoader#loadClass()方法中,其逻辑如下:

先检查是否已被加载过;

若没有加载过则调用父加载器的loadClass()方法;

若父加载器为null则默认使用启动类加载器(Bootstrap ClassLoader)作为父加载器;

如果父加载器加载类失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。(findClass()最终会调用defineClass()加载字节码)

注意:

这里的“双亲”,指的并不是有两个父加载器,可能仅仅是英文“parent”的翻译。每个ClassLoader最多有一个父加载器,也就是parent变量。“双亲委派机制”指的就是优先让父加载器去加载类,如果父加载器没有成功加载到类,才由本ClassLoader加载。

这样可以保证安全性,防止系统类被伪造(比如自定义java.lang.Object类,肯定是无法运行的)。

对于Java程序来讲,一般的类是由AppClassLoader来加载的,而系统类则是由BootStrapClassLoader加载的。由于BootStrapClassLoader是在native层实现的,所以调用系统类的getClassLoader()方法会返回null。

通俗理解

双亲委派机制是 Java 类加载器的重要设计原则,用于确保 Java 应用的安全性和稳定性。为了让这个机制更形象,以下是详细解释和具体例子。


双亲委派机制的定义

双亲委派机制规定,当一个类加载器接收到类加载请求时,它会首先将这个请求委托给它的“父加载器”处理。只有当父加载器无法加载时,子加载器才会尝试自己加载。

这种机制确保:

  1. 类加载的唯一性:避免重复加载同一个类。
  2. 核心类的安全性:例如 java.lang.String 等核心类只能由根类加载器加载,防止被篡改。

形象的比喻

可以把双亲委派机制比作一个问题的求解过程,假设有一个学生、小组长、班主任和校长的层级关系:

  1. 学生有个问题想解决(例如“加载某个类”)。
  2. 学生先问小组长(子加载器)。
  3. 小组长说:“先等等,我得先问班主任(父加载器)。”
  4. 班主任又说:“我得先请示校长(根类加载器)。”
  5. 如果校长知道答案,就会直接回答。
  6. 如果校长说:“我也不知道这个问题的答案”,班主任才会尝试自己解决,依次递归到学生。

这个过程保证了所有请求都从最高层级开始尝试解决,避免了低层级重复回答。

而如果最高层都无法解决,就会让学生自行解决

如果我们传入一个恶意类的字节码,这个系统里当然没有这个恶意类的加载器,所以学生自行解决会生成和加载这个恶意类,从而RCE(我是这么认为的,不对的话请指正)

javassist

Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口。Javaassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。也就是当需要字节码的时候不需要手动另外新建类然后编译为class文件

依赖

1
2
3
4
5
<dependency>
  <groupId>org.javassist</groupId>
  <artifactId>javassist</artifactId>
  <version>3.23.1-GA</version>
</dependency>

演示

通过不新建类来构建一个不存在的对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//创建类,这是一个单例对象
ClassPool pool = ClassPool.getDefault();

//需要构建的类
CtClass ctClass = pool.makeClass("test.Person");

//新增字段
CtField field$name = new CtField(pool.get("java.lang.String"),"name",ctClass);

//设置访问级别
field$name.setModifiers(Modifier.PRIVATE);

//设置继承
ctClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));

//也可以给个初始值
ctClass.addField(field$name,CtField.Initializer.constant("ygsr"));

//生成get/set方法
ctClass.addMethod(CtNewMethod.setter("setName",field$name));
ctClass.addMethod(CtNewMethod.getter("getName",field$name));

//新增构造函数
//无参构造函数
CtConstructor cons$no = new CtConstructor(new CtClass[]{},ctClass);
cons$no.setBody("{name = \"ygsr\";}");
ctClass.addConstructor(cons$no);
//有参构造方法
CtConstructor cons$one  = new CtConstructor(new CtClass[]{pool.get("java.lang.String")},ctClass);
// $0=this  $1,$2,$3... 代表方法参数
cons$one.setBody("{$0.name = $1;}");
ctClass.addConstructor(cons$one);

//或者CtNewConstructor.make
CtConstructor constructor = CtNewConstructor.make("public Person(){  System.out.println(\"abc\");}",ctClass);
ctClass.addConstructor(constructor);
//两者只能使用一者

//创建一个名为print的方法,无参,无回显,输出name值
CtMethod ctMethod = new CtMethod(CtClass.voidType,"print",new CtClass[]{},ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("{System.out.println(name);}");
ctClass.addMethod(ctMethod);

//当前目录
final String Path = "E:\javawork\timu\src\main\java\";

//生成.class文件
ctClass.writeFile(Path);

//将ctfClass对象转换为字节码数组
byte[] bytes = ctClass.toBytecode();

最后更新于 Mar 03, 2025 07:35 UTC
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计