RASP初尝

rasp学习

RASP是Runtime application self-protection的缩写,中文翻译为应用程序运行时防护,其与WAF等传统安全防护措施的主要区别于其防护层级更加底层——在功能调用前或调用时能获取访问到当前方法的参数等信息,根据这些信息来判定是否安全。

优势点在于可以忽略各种绕过流量检测的攻击方式(如分段传输,编码等),只关注功能运行时的传参是否会产生安全威胁。简单来说,RASP不看过程,只看具体参数导致方法实现时是否会产生安全威胁。

Java RASP核心技术栈:

  • Instrumentation通过JVMTI实现的Agent,负责获取并返回当前JVM虚拟机的状态或转发控制命令。
  • 字节码操作框架,用于修改字节码(如ASM、Javassist等)

javaagent提供了两种模式:

  • premain:允许在main开始前修改字节码,也就是在大部分类加载前对字节码进行修改。每次修改需要重启服务。
  • agentmain:允许在main执行后通过com.sun.tools.attach的Attach API attach到程序运行时中,通过retransform的方式修改字节码,也就是在类加载后通过类重新转换(定义)的方式在方法体中对字节码进行修改,其本质还是在类加载前对字节码进行修改。只需要需要重新attach。

环境搭建

https://xz.aliyun.com/news/4534

https://xz.aliyun.com/news/4535

在第一篇文章中创建MANIFEAT.MF的内容如下,需要指定Premain-Class

1
2
3
4
5
Manifest-Version: 1.0
Premain-Class: cn.org.javaweb.test.agent.Agent
Can-Retransform-Classes: true
Can-Redefine-Classes: true
Can-Set-Native-Method-Prefix: true

case1

设置Rasp拦截,打印初始化tomcat时需要加载的所有类名

Agent.java

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
package cn.org.javaweb.test.agent;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class AgentTransform implements ClassFileTransformer {

/**
* @param loader
* @param className
* @param classBeingRedefined
* @param protectionDomain
* @param classfileBuffer
* @return
* @throws IllegalClassFormatException
*/
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {

className = className.replace("/", ".");

System.out.println("Load class:" + className);
return classfileBuffer;
}


}

AgentTransform.java

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
package cn.org.javaweb.test.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class AgentTransform implements ClassFileTransformer {

/**
* @param loader
* @param className
* @param classBeingRedefined
* @param protectionDomain
* @param classfileBuffer
* @return
* @throws IllegalClassFormatException
*/
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {

className = className.replace("/", ".");

System.out.println("Load class:" + className);
return classfileBuffer;
}


}

web正常启动

case2

打印执行命令,所需要使用的函数调用

创建一个ClassVisitor类,需要继承ClassVisitor类并且实现Opcodes类

TestClassVistor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cn.org.javaweb.test.agent;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class TestClassVisitor extends ClassVisitor implements Opcodes {
public TestClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5,cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);

System.out.println(name + "--->" + desc);
return mv;
}
}

对ProcessorBuilder进行hook用户执行的命令

回到AgentTransform中,对transform方法的内容进行修改

AgentTransform.java

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
package cn.org.javaweb.test.agent;

import org.apache.commons.io.IOUtils;
import org.objectweb.asm.*;

import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.regex.Pattern;

public class AgentTransform implements ClassFileTransformer {

/**
* @param loader
* @param className
* @param classBeingRedefined
* @param protectionDomain
* @param classfileBuffer
* @return
* @throws IllegalClassFormatException
*/
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {

className = className.replace("/", ".");

try {
if (className.contains("ProcessBuilder")) {
System.out.println("Load class: " + className);

ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new TestClassVisitor(classWriter);

classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);

classfileBuffer = classWriter.toByteArray();
}
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}

同样先生成Agent.jar再运行tomcat,能够看到执行命令所有的调用栈

case3

拿到用户所执行的命令

新建ProcessBuilderHook.java

1
2
3
4
5
6
7
8
9
10
11
package cn.org.javaweb.test.agent;
import java.util.Arrays;
import java.util.List;
public class ProcessBuilderHook {

public static void start(List<String> commands) {
String[] commandArr = commands.toArray(new String[commands.size()]);
System.out.println(Arrays.toString(commandArr));
}
}

打开TestClassVisitor,对visitMethod方法进行更改

-

  • 最后, ClassReader调用accept方法 完成整个调用流程

修改TestClassVisitor类,其中(Ljava/util/List;)V定义了一个方法的参数类型和返回类型:

解析参数列表 (Ljava/util/List;):

  • 参数列表总是用括号 () 包裹起来。
  • L 表示引用类型(对象类型)。
  • Ljava/util/List; 表示 java.util.List 类型的参数,L 后面是类的全限定名,末尾用分号 ; 结尾。
  • 括号中的内容表示方法的所有参数类型,这里仅有一个参数,类型为 java.util.List。

解析返回类型 V:

  • 返回类型紧跟在括号 () 之后。
  • V 表示方法返回 void(无返回值)。
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
package cn.org.javaweb.test.agent;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.AdviceAdapter;

public class TestClassVisitor extends ClassVisitor implements Opcodes {
public TestClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);

if ("start".equals(name) && "()Ljava/lang/Process;".equals(desc)) {
System.out.println(name + "----->" + desc);

return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
@Override
public void visitCode() {

mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "java/lang/ProcessBuilder", "command", "Ljava/util/List;");
mv.visitMethodInsn(INVOKESTATIC, "cn/org/javaweb/test/agent/ProcessBuilderHook", "start", "(Ljava/util/List;)V", false);

super.visitCode();
}
};
}
return mv;
}
}

ALOAD 0 就是将 this 引用加载到操作数栈

GETFIELD 是 Java 字节码中的一条指令,用于从对象中获取实例字段的值。访问 ProcessBuilder 类的 command 字段(类型为 List)

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

INVOKESTATIC 是 Java 字节码中的一条指令,用于调用静态方法。调用静态方法 cn/org/javaweb/test/agent/ProcessBuilderHook.start,传递获取的 command 字段;方法签名为 (Ljava/util/List;)V,表示接收一个 List 类型参数且无返回值。

1
mv.visitMethodInsn(INVOKESTATIC, "cn/org/javaweb/test/agent/ProcessBuilderHook", "start", "(Ljava/util/List;)V", false);

上述代码用于执行ProcessBuilder时,打印执行的命令

RASP、WAF、EDR的区别

RASP、WAF和EDR是三种安全技术,可以单独使用,也可以组合使用,如图\ref{fig7}。WAF一般部署在Web应用程序的前线,作用于流量层,能阻断大部分明显的恶意流量。但是WAF也具有如下缺点:因为WAF严重依赖正则表达式和模式匹配,很容易被绕过;需要大量的人工调整才能应对0day攻击;缺乏上下文,导致较高的误报率。相比之下RASP集成于应用内部,拥有完整的上下文,能在攻击到达主机前阻止并检测攻击。与WAF相比,RASP真正地与应用融为了一体。不仅如此,RASP还对0day攻击有防护能力,且几乎不需要人工调整,导致的也误报很少。最后,EDR在进程级别运行,负责主机安全。EDR获取的信息相对有限,主要管理进程意图、进程链等方面。但在攻击调查期间,明确入侵点至关重要,EDR无法给出攻击入口。

所以RASP在扩展检测与响应(XDR)中发挥着重要作用,提供应用程序层面的监测。

代码层面RASP的实现

premain

在程序启动前就指定agent的位置

在具体展示两种模式的例子前,先补充一下agent的运行条件,无论用那种模式写出来的agent,都需要将agent打成jar包,这也就是上面需要先用maven打包的原因,同时在jar包中应用META-INF/MANIFEST.MF中指定agent的相关信息,下面是个例子:

1
2
3
4
5
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.lucifaer.javaagentLearning.agent.PreMainTranceAgent
Agent-Class: com.lucifaer.javaagentLearning.agent.AgentMainTranceAgent

以上面的case2为例

Premain-Class和Agent-Class是用来配置不同模式的agent实现类,Can-Redefine-Classes和Can-Retransform-Classes是用来指示是否允许进行类重定义和类重新转换,这两个参数在一定的情况下决定了是否能在agent中利用ASM对加载的类进行修改。由于我们这里用的premain,所以不要Agent-Class配置也可以

  • 需要实现ClassFileTransformer,重载transform方法。当 JVM 加载某个类时,会调用 transform 方法,允许开发者对字节码进行修改。
  • 需要访问类,所以声明ClassReader,来获取类
  • 需要对类中的内容进行修改,所以声明ClassWriter,该类继承于ClassReader
  • 实例化访问者classVisitor来进行类访问,所以TestClassVisitor需要继承ClassVisitor,且重载其中的方法来修改字节码:
    • 如果需要访问注解,则实例化AnnotationVisitor
    • 如果需要访问参数,则实例化FieldVisitor
    • 如果需要访问方法,则实例化MethodVisitor

表达式注入监测

下面是一个省略了分离各个组件(流式写法,写到一个文件),监测MVEL,OGNL,SpEL表达式注入漏洞的demo

重点是PramainAgentTransform,里面指定了需要进行hook的类名以及方法,根据className来判断当前agent拦截的类是否是需要hook的类,如果是,则直接进入ASM修改流程。

在ClassVisitor中调用visitMethod方法去访问hook类中的每个方法,根据方法名判断当前的方法是否是需要hook的方法,如果是,则调用visitCode方法在访问具体代码时获取方法的相关参数(这里是获取表达式),并在执行逻辑中插入expression方法的调用,在运行时将执行流经过新添加的方法,就可以打印出表达式以及调用链了。

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package cn.org.javaweb.test.agent.premain;

import org.objectweb.asm.*;
import org.objectweb.asm.Opcodes;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
//真正执行hook的地方

public class PremainAgentTransform implements ClassFileTransformer {
private static List<MethodHookDesc> expClassList = new ArrayList<MethodHookDesc>();

static {
expClassList.add(new MethodHookDesc("org.mvel2.MVELInterpretedRuntime", "parse",
"()Ljava/lang/Object;"));
expClassList.add(new MethodHookDesc("ognl.Ognl", "parseExpression",
"(Ljava/lang/String;)Ljava/lang/Object;"));
expClassList.add(new MethodHookDesc("org.springframework.expression.spel.standard.SpelExpression", "<init>",
"(Ljava/lang/String;Lorg/springframework/expression/spel/ast/SpelNodeImpl;" +
"Lorg/springframework/expression/spel/SpelParserConfiguration;)V"));
}
@Override
//对所有加载的类都进行遍历
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
final String class_name = className.replace("/", ".");

for (final MethodHookDesc methodHookDesc : expClassList) {
if (methodHookDesc.getHookClassName().equals(class_name)) {
final ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
final int api = Opcodes.ASM5;

try {
ClassVisitor classVisitor = new ClassVisitor(api, classWriter) {
@Override
public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
final MethodVisitor methodVisitor = super.visitMethod(i, s, s1, s2, strings);

if (methodHookDesc.getHookMethodName().equals(s) && methodHookDesc.getHookMethodArgTypeDesc().equals(s1)) {
return new MethodVisitor(api, methodVisitor) {
@Override
public void visitCode() {
if ("ognl.Ognl".equals(class_name)) {
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
} else {
methodVisitor.visitVarInsn(Opcodes.ALOAD, 1);
}
methodVisitor.visitMethodInsn(
Opcodes.INVOKESTATIC, PremainAgent.class.getName().replace(".", "/"), "expression",
"(Ljava/lang/String;)V", false
);
}
};
}
return methodVisitor;
}
};
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
classfileBuffer = classWriter.toByteArray();
} catch (Throwable t) {
t.printStackTrace();
}
}
}
return classfileBuffer;
}
}

上面 methodVisitor.visitVarInsn的结果就作为expression方法的参数。为什么ognl是压栈入局部变量表第0个参数,而其他两个是压栈局部变量表第一个参数?

  • 如果是在静态方法中,0 指代方法的第一个参数;1指代方法的第二个参数。

  • 如果是在实例方法中,索引 0 通常指代 this,即当前对象;索引 1 通常指代方法的第一个参数。

    ognl.Ognl#parse

    Expression即为静态方法,所以压入0

而如果检测 MVELInterpretedRuntime#parser,需要压入哪个变量才能获得exp呢?

parse没有接收任何参数,那么从哪里找变量呢?

在字节码中,通过指令 GETSTATIC 和 PUTSTATIC 操作静态变量,而不涉及局部变量表

attach

在运行时进行部署的一种方式

与premain不同的是agentmain需要实现一个额外的函数retransform,使用instumentation.retransformClasses去在运行中修改字节码

agent入口:AttachAgent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cn.org.javaweb.test.agent.attach;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AttachAgent {

public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
// for (Class clazz : inst.getAllLoadedClasses()) {
// System.out.println(clazz.getName());
// }
CustomClassTransformer transformer = new CustomClassTransformer(inst);
transformer.retransform();
}
}

HOOK逻辑

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
53
54
55
56
57
58
59
60
61
62
63
64
package cn.org.javaweb.test.agent.attach;

import org.objectweb.asm.*;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
import java.util.LinkedList;

public class CustomClassTransformer implements ClassFileTransformer {
private Instrumentation inst;
public CustomClassTransformer(Instrumentation inst) {
this.inst = inst;
inst.addTransformer(this, true);
}

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("In Transform");
//读取字节码
ClassReader cr = new ClassReader(classfileBuffer);
//修改字节码
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
//以ASM模式访问字节码
ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) {
@Override
public MethodVisitor visitMethod(int i, final String s, String s1, String s2, String[] strings) {
// return super.visitMethod(i, s, s1, s2, strings);
final MethodVisitor mv = super.visitMethod(i, s, s1, s2, strings);
if ("say".equals(s)) {
return new MethodVisitor(Opcodes.ASM5, mv) {
@Override
public void visitCode() {
super.visitCode();
//获取System的out变量
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("CALL " +s+ " method");//修改out的值,下面一行代码则调用PrintStream的println打印字符串
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
};
}
return mv;
}
};
cr.accept(cv, ClassReader.EXPAND_FRAMES);
classfileBuffer = cw.toByteArray();
return classfileBuffer;
}

public void retransform() throws UnmodifiableClassException {
LinkedList<Class> retransformClasses = new LinkedList<Class>();
Class[] loadedClasses = inst.getAllLoadedClasses();
for (Class clazz : loadedClasses) {
if ("cn.org.javaweb.test.agent.attach.MainTest".equals(clazz.getName())) {
if (inst.isModifiableClass(clazz) && !clazz.getName().startsWith("java.lang.invoke.LambdaForm")) {
inst.retransformClasses(clazz);
}
}
}
}
}

然后maven打包

agentmain模式和premain的大致写法是没有区别的,最大的区别在于如果想要利用agentmain模式来对运行后的类进行修改,需要利用Instrumentation.retransformClasses方法来对需要修改的类进行重新转换。

现在需要穿件一个agent启动程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package cn.org.javaweb.test.agent.attach;

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;
public class AgentStart {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
if (vmd.displayName().endsWith("MainTest")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("D:\\code\\java-sec-code\\JavawebAgent\\agent\\target\\agent.jar", "Attach!");
System.out.println("ok");
virtualMachine.detach();
}
}
}
}

这段代码遍历当前系统中所有的JVM,找到名为”MainTest”结尾的JVM实例,vmd.id()指该JVM的进程号,然后loadAgent去重新Attach


RASP初尝
https://ledraw.github.io/2025/09/22/RASP初尝/
作者
Ledraw
发布于
2025年9月22日
许可协议