Java RASP

Java RASP

什么是RASP

RASP(Runtime Application Self-Protection)是一种先进的应用安全技术,通过将安全防护能力直接嵌入到应用程序运行时环境中,实现对应用行为的实时监控和攻击防护。

核心功能

  • 应用内嵌:以库或模块的形式嵌入到应用程序,使RASP能够获取应用内部完整的执行上下文信息
  • 实时监控:监控应用程序的执行流程,跟踪输入输出、函数调用和数据流动等关键活动
  • 上下文感知:RASP了解应用程序的完整上下文,包括用户身份、数据处理逻辑、执行环境等信息
  • 行为分析:对应用行为进行分析,检测异常活动和潜在攻击模式1。分析技术包括基于规则和机器学习等多种方法
  • 实时响应:当检测到威胁时,RASP可以采取多种防护措施,如拒绝请求、阻断攻击、记录日志或生成警报

实现原理

java平台

在Java生态中,RASP主要利用Java Agent和Instrumentation API实现:

  • Java Agent:通过-javaagent参数或动态attach方式将RASP代理加载到目标JVM中

    Agent提供两个入口点:

    • premain:在JVM启动时加载
    • agentmain:在JVM运行时动态加载
  • Instrumentation API:允许RASP在类加载时修改字节码,插入安全检测逻辑

    关键接口包括:

    • ClassFileTransformer:定义类转换逻辑
    • addTransformer:注册字节码转换器
  • 字节码操作:使用ASM、Javassist等框架动态修改类文件,在关键API调用处插入Hook代码

其他语言

  • PHP:通过PHP扩展实现
  • .NET:利用HostingStartup机制
  • Golang:使用进程注入和Inline Hook技术
  • Node.js:通过修改模块加载机制实现

java字节码操作

class字节码

java文件被编译为class文件后才能被jvm使用。

使用javac对以下类进行编译:

package com.example.demo.entity;

public class School {
    private final String name;
    private final Integer age;

    public School(String name, Integer age) {
        this.name=name;
        this.age=age;
    }

    public String getName() {
        return name;
    }


    public Integer getAge() {
        return age;
    }
}

注意要使用-g来保留所有调试信息,包括局部变量表:

javac -g School.java

然后使用javap来显示具体的信息:

javap -v -p School

显示:

  Last modified 2025年7月13日; size 656 bytes
  SHA-256 checksum 94e4cf8fc6aa6ebf1ec1a28d5dda5496a7c64474d2505c6998c083676f10c169
  Compiled from "School.java"
public class com.example.demo.entity.School
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // com/example/demo/entity/School
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 2, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // com/example/demo/entity/School.name:Ljava/lang/String;
   #8 = Class              #10            // com/example/demo/entity/School
   #9 = NameAndType        #11:#12        // name:Ljava/lang/String;
  #10 = Utf8               com/example/demo/entity/School
  #11 = Utf8               name
  #12 = Utf8               Ljava/lang/String;
  #13 = Fieldref           #8.#14         // com/example/demo/entity/School.age:Ljava/lang/Integer;
  #14 = NameAndType        #15:#16        // age:Ljava/lang/Integer;
  #15 = Utf8               age
  #16 = Utf8               Ljava/lang/Integer;
  #17 = Utf8               (Ljava/lang/String;Ljava/lang/Integer;)V
  #18 = Utf8               Code
  #19 = Utf8               LineNumberTable
  #20 = Utf8               LocalVariableTable
  #21 = Utf8               this
  #22 = Utf8               Lcom/example/demo/entity/School;
  #23 = Utf8               getName
  #24 = Utf8               ()Ljava/lang/String;
  #25 = Utf8               getAge
  #26 = Utf8               ()Ljava/lang/Integer;
  #27 = Utf8               SourceFile
  #28 = Utf8               School.java
{
  private final java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: (0x0012) ACC_PRIVATE, ACC_FINAL

  private final java.lang.Integer age;
    descriptor: Ljava/lang/Integer;
    flags: (0x0012) ACC_PRIVATE, ACC_FINAL

  public com.example.demo.entity.School(java.lang.String, java.lang.Integer);
    descriptor: (Ljava/lang/String;Ljava/lang/Integer;)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: aload_1
         6: putfield      #7                  // Field name:Ljava/lang/String;
         9: aload_0
        10: aload_2
        11: putfield      #13                 // Field age:Ljava/lang/Integer;
        14: return
      LineNumberTable:
        line 7: 0
        line 8: 4
        line 9: 9
        line 10: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   Lcom/example/demo/entity/School;
            0      15     1  name   Ljava/lang/String;
            0      15     2   age   Ljava/lang/Integer;

  public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #7                  // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 13: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/example/demo/entity/School;

  public java.lang.Integer getAge();
    descriptor: ()Ljava/lang/Integer;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #13                 // Field age:Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 18: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/example/demo/entity/School;
}
SourceFile: "School.java"
jvm指令集

JVM(Java 虚拟机)指令集是 JVM 执行字节码时使用的一套指令集合,每个指令由一个字节的操作码(Opcode)和可选的操作数组成。以下是 JVM 指令集的核心分类和常见指令

JVM指令
1. 加载和存储指令

用于将数据在栈帧中的局部变量表和操作数栈之间传输。

  • iload_<n>:将局部变量表中索引为<n>的 int 值压入操作数栈(如iload_0)。
  • istore_<n>:将操作数栈顶的 int 值存入局部变量表索引<n>位置。
  • aload_<n>:加载引用类型(如aload_0加载this引用)。
  • ldc:将常量从常量池推送到操作数栈(如字符串、数值常量)。
2. 算术和逻辑指令

对操作数栈上的值进行运算,并将结果压回栈顶。

  • iadd/isub/imul/idiv:整数加减乘除。
  • iinc:局部变量自增(如iinc 1 2表示将局部变量 1 加 2)。
  • iand/ior/ixor:按位与、或、异或。
3. 类型转换指令

用于数值类型的显式转换。

  • i2l/i2f/i2d:int 转 long/float/double。
  • checkcast:类型检查(运行时验证对象类型)。
4. 对象创建与操作
  • new:创建对象(如new java/lang/String)。
  • getfield/putfield:访问对象字段(实例变量)。
  • getstatic/putstatic:访问类静态字段。
5. 方法调用与返回
  • invokevirtual:调用实例方法(基于对象实际类型)。
  • invokestatic:调用静态方法。
  • invokespecial:调用构造方法、私有方法或父类方法。
  • invokeinterface:调用接口方法。
  • ireturn/areturn:返回 int / 引用类型。
6. 控制转移指令
  • goto:无条件跳转。
  • if_icmp<cond>:整数比较跳转(如if_icmpeq比较相等)。
  • tableswitch/lookupswitch:分支跳转(用于switch语句)。
7. 异常处理指令
  • athrow:抛出异常。
  • try-catch:通过字节码中的Exception table实现。
8. 同步指令
  • monitorenter/monitorexit:实现synchronized块。

JVM的指令集是基于栈实现的,意味着不依赖硬件寄存器,便于在不同架构上实现,也意味着它的大部分操作都依赖于一个操作数栈。

image-20250713015221319

每个 Java 线程都会对应一个虚拟机栈,虚拟机栈中包含多个栈帧。每一个栈帧是为方法执行而创建的,用于管理 Java 程序的运行,并保存方法的局部变量、部分结果,还参与方法的调用与返回。

当方法调用另一种方法时,新的栈帧被推送到栈顶,成为当前栈帧,而当方法返回时,当前栈帧出栈,旧栈帧再次成为当前帧

局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量

LocalVariableTable:
   Start  Length  Slot  Name   Signature
      0      15     0  this   Lcom/example/demo/entity/School;
      0      15     1  name   Ljava/lang/String;
      0      15     2   age   Ljava/lang/Integer;

这是School构造函数的局部变量表,定义了三个变量及其类型,这是在编译时指定了-g得到的结果,假如没有指定,则不会显示局部变量表。

在写过的文章中有提到,fastjson 1.2.68的版本漏洞,在利用目标类时要求类在编译时保留构造方法的局部变量表。

操作数栈

当一个方法刚开始执行时,操作数栈是空的。在方法执行过程中,字节码指令会根据需要往栈中写入数据(入栈)或提取数据(出栈)。例如,进行算术运算时,相关操作数会先入栈,运算指令执行时从栈顶取出操作数进行运算,再将结果入栈;调用其他方法时,也通过操作数栈进行参数传递。

字节码操作类库

  • ASM:是一款读写 Java 字节码的工具,可跳过源码编写与编译,直接以字节码形式创建类,或修改已存在类的属性、方法等。通过其提供的 API,如ClassWriterMethodVisitorFieldVisitor等,可以构建新的类结构或修改已有类的字节码结构。
  • Javassist:是一个开源的分析、编辑和创建 Java 字节码的类库。它以 Java 代码的方式来操作字节码,不需要直接操作复杂的字节码指令,降低了操作门槛。可通过ClassPool获取类对象CtClass,再利用CtMethodCtField等类对类中的方法和属性进行操作,如插入代码、添加属性等。
  • CGLIB:是一个基于 ASM 实现的高性能代码生成类库,常用于在运行时生成字节码并创建代理类。它通过继承的方式创建代理类,可代理普通类,即使该类没有实现任何接口,常用于 AOP 编程等场景。
  • BCEL:即 Byte Code Engineering Library,是 Apache Jakarta 项目的一部分,它可以让开发者深入 JVM 汇编语言进行类操作的细节,在实际的 JVM 指令层次上进行操作,拥有丰富的 JVM 指令级支持。
ASM

ASM是一种通用Java字节码操作和分析框架。它可以用于修改现有的class文件或动态生成class文件。

ClassVisitor
  • 两个构造函数

    public ClassVisitor(int api) {
            this(api, (ClassVisitor)null);
        }
    
        public ClassVisitor(int api, ClassVisitor cv) {
            if (api != 262144 && api != 327680) {
                throw new IllegalArgumentException();
            } else {
                this.api = api;
                this.cv = cv;
            }
        }
    

    其中api入参为ASM API 版本号(如 Opcodes.ASM9),用于兼容性检查

  • 五个类结构访问方法

    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
    public void visitSource(String source, String debug)
    public void visitOuterClass(String owner, String name, String desc)
    public void visitInnerClass(String name, String outerName, String innerName, int access)
    public void visitAttribute(Attribute attr)
    

    在访问类的不同部分时被调用,默认实现将操作传递给下一个 ClassVisitor的对应方法

  • 两个注解处理方法

    public AnnotationVisitor visitAnnotation(String desc, boolean visible)
    public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible)
    

    用于处理类上的注解,默认实现将操作传递给下一个 ClassVisitor的对应方法

  • 字段和方法处理:

    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value)
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)
    

    用于处理字段和方法,默认实现将操作传递给下一个 ClassVisitor的对应方法

  • 结束

    public void visitEnd()
    

    标记类访问结束,必须调用以完成处理。

visitMethod

当访问类的一个方法时,该方法会被调用,通过重写visitMethod可以对特定类的特定方法进行hook

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        return this.cv != null ? this.cv.visitMethod(access, name, desc, signature, exceptions) : null;
    }
  • access:方法的访问标志(如ACC_PUBLIC, ACC_PRIVATE等)
  • name:方法名称
  • desc:方法描述符(如(Ljava/lang/String;)V
  • signature:泛型签名信息(可为 null)
  • exceptions:方法声明抛出的异常类名数组
visitField

当扫描到类的字段定义时,就会调用这个方法,通过重写该方法来对字段进行分析或修改

public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
    return this.cv != null ? this.cv.visitField(access, name, desc, signature, value) : null;
}
MethodVIsitor
  • 参数与注解处理

    public void visitParameter(String name, int access)
    public AnnotationVisitor visitAnnotationDefault()
    public AnnotationVisitor visitAnnotation(String desc, boolean visible)
    

    用于处理方法参数、注解默认值和各种类型的注解

  • 字节码指令处理

    public void visitCode() // 开始访问方法代码
    public void visitInsn(int opcode) // 访问各种字节码指令
    public void visitVarInsn(int opcode, int var)
    public void visitFieldInsn(int opcode, String owner, String name, String desc)
    

    对应 Java 字节码中的各种指令类型(如变量操作、字段访问、方法调用等)

    方法说明
    visitInsn(int opcode)处理无操作数的字节码指令,如NOPIADDRETURN等。
    visitIntInsn(int opcode, int operand)处理需要单个整数操作数的指令,如BIPUSHSIPUSHNEWARRAY等。
    visitVarInsn(int opcode, int var)处理局部变量操作指令,如ILOADISTOREALOAD等。
    visitTypeInsn(int opcode, String type)处理与类型相关的指令,如NEWANEWARRAYCHECKCAST等。
    visitFieldInsn(int opcode, String owner, String name, String desc)处理字段访问指令,如GETFIELDPUTFIELDGETSTATIC等。
    visitMethodInsn(int opcode, String owner, String name, String desc)(已弃用) 处理方法调用指令(ASM 5 及以下版本),如INVOKEVIRTUALINVOKESTATIC等。
    visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf)处理方法调用指令(ASM 6 + 版本),增加了itf参数以明确是否为接口方法调用。
    visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs)处理动态调用指令(Java 7 + 的invokedynamic)。
    visitJumpInsn(int opcode, Label label)处理跳转指令,如IFEQGOTOJSR等。
    visitLdcInsn(Object cst)处理常量加载指令,如LDCLDC_WLDC2_W等。
    visitIincInsn(int var, int increment)处理局部变量自增指令(IINC)。
    visitTableSwitchInsn(int min, int max, Label dflt, Label... labels)处理tableswitch指令(用于等值跳转的分支表)。
    visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels)处理lookupswitch指令(用于非连续值的分支表)。
    visitMultiANewArrayInsn(String desc, int dims)处理多维数组创建指令(multianewarray)。
  • 特殊处理方式

    public void visitMaxs(int maxStack, int maxLocals)
    public void visitEnd()
    

    visitMaxs设置方法的操作数栈和局部变量表的最大深度,visitEnd标记方法访问结束

类似ClassVisitor,每一个方法都会沿着责任链传递给下一个访问者。

以下对一些指令进行详细说明:

visitCode
public void visitCode() {
        if (this.mv != null) {
            this.mv.visitCode();
        }

    }
visitJumpInsn
Label l92 = new Label();
mv.visitJumpInsn(IFNE, l92);  // 如果条件为真,跳转到l92标记的位置
// ... 创建并抛出IOException的代码 ...
mv.visitLabel(l92);  // 标记l92的实际位置

192是一个Label对象,IFNE操作会检查前一个操作的返回值,如果返回值未true,就会跳转到mv.visitLabel(l92),然后继续执行代码

visitTypeInsn
mv.visitTypeInsn(NEW, "xbear/javaopenrasp/filters/rce/PrcessBuilderFilter")

new操作会创建一个PrcessBuilderFilter对象引用放入栈顶

visitInsn
mv.visitInsn(DUP);

将栈顶元素复制一份并压回栈顶

visitMethodInsn
mv.visitMethodInsn(INVOKESPECIAL, "xbear/javaopenrasp/filters/rce/PrcessBuilderFilter", "<init>", "()V", false);

第四个参数false标识不是接口方法,构造函数属于类方法,因此此处调用的是构造方法

visitVarInsn
mv.visitVarInsn(ASTORE, 1);

将操作数栈顶的引用值存入局部变量表索引1的位置,供后续指令使用

mv.visitVarInsn(ALOAD, 1);

从局部变量表索引1的位置取出并存入操作数栈顶

visitFieldInsn
mv.visitFieldInsn(GETFIELD, "java/lang/ProcessBuilder", "command", "Ljava/util/List;");

GETFIELD是 Java 字节码中用于访问实例字段的指令

意思是访问字段command,该字段在ProcessBuilder类中定义为:

private List<String> command;

如果要访问静态字段则使用GETSTATIC操作码

visitLineNumber

ASM 库中用于生成调试信息的方法,它将字节码中的特定位置(由 Label 标记)与原始 Java 源代码中的行号关联起来

methodVisitor.visitLabel(someLabel);       // 标记字节码位置
methodVisitor.visitLineNumber(42, someLabel);  // 关联到源代码第42行
完整实例
  1. 创建过滤器实例:

    mv.visitTypeInsn(NEW, "xbear/javaopenrasp/filters/rce/PrcessBuilderFilter");
    mv.visitInsn(DUP);
    mv.visitMethodInsn(INVOKESPECIAL, "xbear/javaopenrasp/filters/rce/PrcessBuilderFilter", "<init>", "()V", false);
    mv.visitVarInsn(ASTORE, 1); // 将过滤器实例存入局部变量1
    

    这部分代码创建了一个PrcessBuilderFilter对象并调用其无参构造函数。注意,由于new指令和构造函数调用是两个独立的步骤,因此需要分开写,而且,在实际调用该对象时,也不会直接使用new后得到的引用,而是使用DUP创建一个引用后使用引用

  2. 获取命令列表:

    mv.visitVarInsn(ALOAD, 1); // 加载过滤器实例
    mv.visitVarInsn(ALOAD, 0); // 加载this(ProcessBuilder实例)
    mv.visitFieldInsn(GETFIELD, "java/lang/ProcessBuilder", "command", "Ljava/util/List;");
    

    这里通过ProcessBuildercommand字段获取要执行的命令列表。注意,当JVM 调用一个方法时(无参构造函数),会为其创建一个栈帧,该栈帧局部变量表中的第一个参数默认是this,第二个参数则为主动存入的过滤器实例

  3. 执行过滤检查:

    mv.visitMethodInsn(INVOKEVIRTUAL, "xbear/javaopenrasp/filters/rce/PrcessBuilderFilter", "filter", "(Ljava/lang/Object;)Z", false);
    

    调用过滤器的filter方法,传入命令列表,返回一个布尔值(是否允许执行)。

  4. 检查过滤结果:

    Label l92 = new Label();
    mv.visitJumpInsn(IFNE, l92); // 如果filter返回true(允许执行),跳转到l92
    

    如果过滤通过(返回true),跳转到标签l92继续执行原命令;否则继续执行异常处理。

  5. 阻止执行并抛出异常:

    mv.visitTypeInsn(NEW, "java/io/IOException");
    mv.visitInsn(DUP);
    mv.visitLdcInsn("invalid character in command because of security");
    mv.visitMethodInsn(INVOKESPECIAL, "java/io/IOException", "<init>", "(Ljava/lang/String;)V", false);
    mv.visitInsn(ATHROW);
    

    如果过滤失败(返回false),创建一个带有安全警告的IOException并抛出,阻止命令执行。ATHROW 指令用于将当前操作数栈顶的异常对象抛出,并终止当前方法的执行

  6. 标签位置:

    mv.visitLabel(l92); // 过滤通过,继续执行原命令
    

    标签l92标记了正常执行流程的位置。

ASM字节码操作插件

IDEA ASM Bytecode Viewer,可以将java代码转换字节码以及ASM和Groovy格式的代码

以下是原代码:

public void test() throws IOException {
        Config.initConfig();
        PrcessBuilderFilter filter = new PrcessBuilderFilter();
        if (!filter.filter(this.command)) {
            throw new IOException("invalid character in command because of security");
        }
    }

快速转换的asm代码:

methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "()V", null, new String[]{"java/io/IOException"});
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(51, label0);
            methodVisitor.visitMethodInsn(INVOKESTATIC, "xbear/javaopenrasp/config/Config", "initConfig", "()V", false);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLineNumber(52, label1);
            methodVisitor.visitTypeInsn(NEW, "xbear/javaopenrasp/filters/rce/PrcessBuilderFilter");
            methodVisitor.visitInsn(DUP);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "xbear/javaopenrasp/filters/rce/PrcessBuilderFilter", "<init>", "()V", false);
            methodVisitor.visitVarInsn(ASTORE, 1);
            Label label2 = new Label();
            methodVisitor.visitLabel(label2);
            methodVisitor.visitLineNumber(53, label2);
            methodVisitor.visitVarInsn(ALOAD, 1);
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitFieldInsn(GETFIELD, "xbear/javaopenrasp/filters/rce/PrcessBuilderFilter", "command", "Ljava/util/List;");
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "xbear/javaopenrasp/filters/rce/PrcessBuilderFilter", "filter", "(Ljava/lang/Object;)Z", false);
            Label label3 = new Label();
            methodVisitor.visitJumpInsn(IFNE, label3);
            Label label4 = new Label();
            methodVisitor.visitLabel(label4);
            methodVisitor.visitLineNumber(54, label4);
            methodVisitor.visitTypeInsn(NEW, "java/io/IOException");
            methodVisitor.visitInsn(DUP);
            methodVisitor.visitLdcInsn("invalid character in command because of security");
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/io/IOException", "<init>", "(Ljava/lang/String;)V", false);
            methodVisitor.visitInsn(ATHROW);
            methodVisitor.visitLabel(label3);
            methodVisitor.visitLineNumber(56, label3);
            methodVisitor.visitFrame(Opcodes.F_APPEND, 1, new Object[]{"xbear/javaopenrasp/filters/rce/PrcessBuilderFilter"}, 0, null);
            methodVisitor.visitInsn(RETURN);
            Label label5 = new Label();
            methodVisitor.visitLabel(label5);
            methodVisitor.visitLocalVariable("this", "Lxbear/javaopenrasp/filters/rce/PrcessBuilderFilter;", null, label0, label5, 0);
            methodVisitor.visitLocalVariable("filter", "Lxbear/javaopenrasp/filters/rce/PrcessBuilderFilter;", null, label2, label5, 1);
            methodVisitor.visitMaxs(3, 2);
            methodVisitor.visitEnd();

实现

以github的项目为例:

https://github.com/xbeark/javaopenrasp

定义一个入口,在jvm启动时加载,然后内部通过addTransformer注册自定义的transform

image-20250712212505290

自定义的Transformer实现自ClassFileTransformerClassFileTransformer用于类文件的转换,是Java字节码增强技术的重要组成部分。

在ClassFileTransformer中有两个transform重载,第一个是:

default byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException {
        return null;
    }

第二个是:

default byte[]
    transform(  Module              module,
                ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException {

        // invoke the legacy transform method
        return transform(loader,
                         className,
                         classBeingRedefined,
                         protectionDomain,
                         classfileBuffer);
    }

涉及的入参如下:

  • loader :类加载器,负责将类的字节码加载到jvm中
  • className:类名,以 “/” 代替点号分隔包名和类名,用于标识具体的类,如java/util/List
  • classBeingRedefined:如果是重新转换类(如通过retransformClasses方法触发),则该参数为正在被重新定义的类对象;如果是类首次加载,则该参数为null
  • protectionDomain:表示类的保护域,它包含了类的安全相关信息,如类的权限等。
  • classfileBuffer:是一个字节数组,包含了类的原始字节码数据,可对其进行修改以实现字节码增强等功能,方法的返回值也是字节数组,应返回修改后的字节码数据,如果无需修改则返回null
  • module:待转换类的模块,自java9引入

返回值:返回转换后的结果,如果未转换就返回null

项目实现了ClassFileTransformer

image-20250712220240982

其中EXPAND_FRAMES用于展开堆栈映射帧(StackMapTable)属性:

  • 解析 StackMapTable:Java 字节码中的StackMapTable用于帮助 JVM 在类加载时验证字节码的类型安全性。EXPAND_FRAMES会将StackMapTable中的压缩帧信息转换为完整的扩展格式,使得字节码中的栈帧信息更加清晰和详细。
  • 确保字节码验证通过:在对字节码进行修改时,如插入新指令、删除指令等操作,可能会破坏原有的栈帧信息,导致字节码验证失败。使用EXPAND_FRAMES可以让 ASM 自动处理栈帧相关的内容,保证修改后的字节码在 JVM 中能够正确通过验证,避免出现VerifyError等错误。
  • 影响visitFrame方法:EXPAND_FRAMES会对MethodVisitor.visitFrame()方法的参数产生影响。因为展开栈帧后,栈帧的结构和内容发生了变化,传递给visitFrame方法的参数也会相应改变,开发者可以根据这些变化来编写更复杂的字节码处理逻辑。

以下是加载的部分配置:

image-20250712220312849

当遇到java/lang/ProcessBuilder类,就会加载自定义的ProcessBuilderVisitor,自定义的Visitor继承自ClassVisitorClassVisitor是 Java 字节码操作库ASM(Java 字节码操作框架)中的核心接口,用于访问和修改 Java 类的字节码。通过实现该接口,可以在类加载到 JVM 之前对其进行动态修改。

继续看项目中实现的ProcessBuilderVisitor

image-20250712222601647

自定义的 ProcessBuilderVisitorAdapter继承自AdviceAdapterAdviceAdapter 是 ASM(Java 字节码操作库)中的一个实用类,继承自 MethodVisitor,专门用于简化方法字节码的修改。

image-20250712224226240

在上述实现中onMethodEnter重载自AdviceAdapter,在进入目标方法(这里是start)前触发,此处转换为java代码:

xbear.javaopenrasp.filters.rce.PrcessBuilderFilter processBuilderFilter = new xbear.javaopenrasp.filters.rce.PrcessBuilderFilter();
boolean isValid = processBuilderFilter.filter(this.command);
if (!isValid) {
    throw new IOException("invalid character in command because of security");
}

PrcessBuilderFilter内部实现:

image-20250715003350277

至此,该项目RASP的实现逻辑解读完,总结:

  • 定义一个java Agent,加载方式使用premain启动时加载,然后通过Instrumentation注册Transformer

  • 基于ClassFileTransformer实现Transformer,在内部transform方法中加载自定义ClassVisitor来处理字节码,通过在ClassVisitor重载一些如visitMethod的方法来对所有被调用的方法进行hook

  • visitMethod中无法对方法入参进行处理,因此需要加载自定义的MethodVisitor并对其部分方法进行重载从而对入参进行hook

运行

环境:jdk 17

引入依赖:

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>9.8</version>
</dependency>

<dependency>
            <artifactId>asm-commons</artifactId>
            <groupId>org.ow2.asm</groupId>
            <version>9.8</version>
</dependency>

注意,AdviceAdapter被包含在在asm-commons中


运行目标jar包时指定参数

java -javaagent:javaopenrasp\target\javaopenrasp.jar -jar target.jar

或者直接在IDEA添加jvm 启动参数并运行:

-javaagent:javaopenrasp\target\javaopenrasp.jar

路径可以是绝对路径或相对路径

测试代码:

image-20250715015948526

执行结果:

image-20250715015933187

最后

java RASP借助Java Agent、Instrumentation和ASM完成了对危险类和方法的hook,当满足配置的条件后,就会触发防护。在解决了依赖和jdk版本的问题后,对原有的javaopenrasp进行二次开发以适配更多的防护也就成了比较轻松的事了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值