Java RASP初探
2025-08-07 16:17:40

RASP简单来说就是利用Java Agent提供的API来对危险系统函数等进行插桩,通过修改java字节码在某些系统函数进行危险操作前加入检测逻辑。

JVM启动前

java.lang.instrument.Instrumentation提供了preMain方法,配合-javaagent参数启动即可

Agent.java

1
2
3
4
5
6
7
import java.lang.instrument.Instrumentation;

public class Agent {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new AgentTransform());
}
}

AgentTransform.java

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class AgentTransform implements ClassFileTransformer {
@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;
}
}

JVM启动后

JVM启动后,不方便重启,此时想要插桩,java.lang.instrument.Instrumentation提供了agentMain方法

1
2
3
4
5
6
7
8
import java.lang.instrument.Instrumentation;

public class agentMain {
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println("agentmain start");
instrumentation.addTransformer(new agentMainTransform());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class agentMainTransform implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {

return classfileBuffer;
}
}

写一个java demo,当作一个已经启动的项目

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.management.ManagementFactory;

public class project {
public static void main(String[] args) throws InterruptedException {
System.out.println("------ Project start ------");
String processName = ManagementFactory.getRuntimeMXBean().getName();
String pid = processName.split("@")[0];
System.out.println("当前进程ID: " + pid);
Thread.sleep(300000000);
System.out.println("------ Project stop ------");
}
}

启动后

1
2
------ Project start ------
当前进程ID: 21500

此时JVM进程号为21500,将agent注入到这个进程中

1
2
3
4
5
6
7
8
9
import com.sun.tools.attach.VirtualMachine;

public class attachAgent {
public static void main(String[] args) throws Throwable {
VirtualMachine attach = VirtualMachine.attach("21500");
attach.loadAgent("C:\\Users\\worker\\Desktop\\RASP\\RASPLearn\\target\\RASPLearn-1.0-SNAPSHOT.jar");
attach.detach();
}
}

注入后

image-20250807161319745

Jar打包

法一

Premain-class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>org.example.Agent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>

Agent-class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Agent-Class>org.example.Agent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
法二
1
jar cfm test.jar .\manifest.txt -C C:\Users\worker\Desktop\RASP\RASPLearn\target\classes\org\example test.class

manifest.txt (空行必须要有)

1
2
Main-Class: org.example.preMainTest

执行

1
java -javaagent:C:\Users\worker\Desktop\RASP\RASPLearn\target\RASPLearn-1.0-SNAPSHOT.jar -jar C:\Users\worker\Desktop\RASP\RASPLearn\src\tempJars\test.jar

打印输出了在main函数前后加载的类

image-20250804114837872

也可以在IDEA中直接配置运行

image-20250804134756363

image-20250804134820392

javassist字节码修改

使用的API

ClassPool

1
2
3
4
5
getDefault:返回一个ClassPool对象,ClassPool是单例模式,保存了当前运行环境中创建过的CtClass

get/getCtClass:根据类名获取该类的CtClass对象,而后进一步操作CtClass对象 -> 不建议使用

makeClass:根据字节码文件创建CtClass对象 -> 推荐使用

CtClass:对class文件的解析,将其描述为一个java对象

1
2
3
4
5
6
7
8
9
10
11
12
13
detach,将一个classClassPool中删除,减小内存消耗

getDeclaredMethod(String arg), 获取一个CtMethod对象

getDeclaredMethods,获取所有的这个类中所有的方法,并返回CtMethod数组

getConstructor(String arg),获取一个指定的构造方法,返回CtConstructor对象

getConstructors,获取所有的构造方法,返回CtConstructor数组

toBytecode,将ctClass对象的字节码转换成字节数组,这个方法在rasp中非常需要

writeFile,将ctClass对象的修改保存到文件中

CtMethod:对类中的方法的描述,可以通过这个对象,在一个方法前后添加代码

1
2
3
4
5
6
7
insertBefore,很明显,在给定的方法前插入一段代码

insertAfter,也很明显,在给定方法的所有return前,插入一段代码,报错会掠过这些代码

insertAt,在指定位置插入代码,一般不这么操作,改变系统类中方法的逻辑,可能会引发更多的问题

setBody,将方法的内容设置为指定的代码,如果是abstrict修饰的方法,该修饰符将被移除。指定的代码用$1,$2代表实际传入的参数
代码Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class AgentTransform implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if ("java/lang/ProcessBuilder".equals(className)) {
try {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
CtMethod ctMethod = ctClass.getDeclaredMethod("start");
ctMethod.insertBefore("{ System.out.println(\"------ Insert Start ------\"); }");
byte[] byteCode = ctClass.toBytecode();
ctClass.detach();
return byteCode;
} catch (Exception ex) {
ex.printStackTrace();
}
}

return classfileBuffer;
}
}

因为ProcessBuilder是系统类,不能用getCtClass直接获取,因为getCtClass方法走的不是JVM加载 (双亲委派机制),它走的是javassist自己的机制来读取class字节码文件 (classpath),而ProcessBuilder类是在Bootstrap ClassLoader中被加载 (双亲委派的最后一层),这导致若使用getCtClass会报错”找不到ProcessBuilder

可以直接使用ClassPool.makeClass方法,通过Java Agent提供的真实字节码来创建ctClass,避免找不到ProcessBuilder

ASM字节码修改

ClassReader

ClassReader用于加载字节码

1
2
3
4
5
//直接加载class文件
byte[] byteCode = Files.readAllBytes(Paths.get("C:\\Users\\worker\\Desktop\\RASP\\RASPLearn\\target\\classes\\org\\example\\agentMain.class"));

//ClassReader构造函数加载
ClassReader classReader = new ClassReader("java/lang/ProcessBuilder");

ClassReader构造函数

image-20250807135919645

ClassVisitor

1、visit-访问类的方法

1
2
3
4
5
6
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
//每一次访问一个类时的额外操作
...
super.visit(version, access, name, signature, superName, interfaces);
}

2、visitMethod-访问类中方法时的方法

1
2
3
4
5
6
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
//每一次访问某个类中的某个方法时的额外操作
...
return super.visitMethod(access, name, desc, signature, exceptions);
}
MethodVisitor

1、visitCode-标记字节码的开始

调用visitCode之后就可以写字节码指令了

2、visitMethodInsn-调用方法

接收五个参数

1
visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface)

opcode:操作码

1
2
3
4
5
INVOKEVIRTUAL	调用实例方法	obj.toString()
INVOKESTATIC 调用静态方法 Math.max(1, 2)
INVOKESPECIAL 调用构造方法/私有方法/父类方法 super()、构造器
INVOKEINTERFACE 调用接口方法 List.size()
INVOKEDYNAMIC 调用动态语言支持方法(高级用途) Lambda 表达式底层实现

owner:方法所在类的类名

name:方法名

descriptor:方法签名(返回类型)

isInterface:调用的是否为接口方法

3、visitFieldInsn-访问字段-读取/写入静态字段

1
visitFieldInsn(int opcode, String owner, String name, String descriptor)

opcode:操作码

1
2
3
4
GETSTATIC	读取静态字段	System.out
PUTSTATIC 写静态字段 给 static 字段赋值
GETFIELD 读取实例字段 obj.name
PUTFIELD 写实例字段 obj.name = "abc"

owner:字段所在类的类名

name:字段名

descriptor:字段类型

4、visitLdcInsn-加载常量

1
visitLdcInsn(Object cst)

cst:常量(字符串、数字等等)

5、visitVarInsn-加载局部变量/this

1
visitVarInsn(int opcode, int var)

opcode:操作码

1
2
3
4
ALOAD	加载引用类型(Object)	加载 this(索引0
ASTORE 存储引用类型到局部变量 保存对象引用
ILOAD 加载 int 类型局部变量 加载方法中的 int 参数
ISTORE 存储 int 到局部变量 保存 int 返回值

var:索引

1
2
3
mv.visitVarInsn(ALOAD, 0);  // 加载 this 引用
mv.visitVarInsn(ILOAD, 1); // 加载第一个 int 类型参数
mv.visitVarInsn(ASTORE, 2); // 将引用类型值存入局部变量 2
代码Demo
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
import org.objectweb.asm.*;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class AgentTransform implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {

if ("java/lang/ProcessBuilder".equals(className)) {
try {
ClassReader classReader = new ClassReader(className);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);

ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9, classWriter) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if ("start".equals(name) && "()Ljava/lang/Process;".equals(descriptor)) {
System.out.println("Hooking ProcessBuilder.start()");
return new ModifyMethod(mv);
}
return mv;
}
};

classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
return classWriter.toByteArray();

} catch (Exception ex) {
ex.printStackTrace();
}
}

return classfileBuffer;
}

static class ModifyMethod extends MethodVisitor {
public ModifyMethod(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}

@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("RASP: Start to detect ProcessBuilder.start()");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println",
"(Ljava/lang/String;)V", false);
}
}
}

写入System.out.Println("RASP: Start to detect ProcessBuilder.start()")

image-20250807152112322

感觉还是javassist操作起来更简便。

RASP检测ProcessBuilder

利用RASPProcessBuilder.start()命令执行函数进行防御,利用CtMethod.insertBefore在执行ProcessBuilder.start()前插入检测代码 -> 命令执行白名单,只允许执行安全命令 -> 如ping

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
public class AgentTransform implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if ("java/lang/ProcessBuilder".equals(className)) {
try {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
CtMethod ctMethod = ctClass.getDeclaredMethod("start");
ctMethod.insertBefore("{ \n" +
" System.out.println(\"------ Start detect field 'command' ------\"); \n" +
" java.util.List cmdList = this.command(); \n" +
" if (cmdList != null && cmdList.size() > 0) {\n" +
" String cmd = ((String) cmdList.get(0)).toLowerCase();\n" +
" if (!cmd.equals(\"ping\")) {\n" + // 用 .equals() 比较字符串
" throw new SecurityException(\"[RASP] Only 'ping' command is allowed. Detected: \" + cmd); \n" +
" }\n" +
" } else {\n" +
" throw new SecurityException(\"[RASP] Command list is empty or null.\");\n" +
" }\n" +
"}");

byte[] byteCode = ctClass.toBytecode();
ctClass.detach(); //释放缓存,重新从字节码中加载
return byteCode;
} catch (Exception ex) {
ex.printStackTrace();
}
}
return classfileBuffer;
}
}

当执行非白名单命令时抛出异常

image-20250804162820569

上一页
2025-08-07 16:17:40
下一页