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 ------"); } }
|
启动后
此时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(); } }
|
注入后

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
函数前后加载的类

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


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,将一个class从ClassPool中删除,减小内存消耗
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
| byte[] byteCode = Files.readAllBytes(Paths.get("C:\\Users\\worker\\Desktop\\RASP\\RASPLearn\\target\\classes\\org\\example\\agentMain.class"));
ClassReader classReader = new ClassReader("java/lang/ProcessBuilder");
|
ClassReader
构造函数

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); mv.visitVarInsn(ILOAD, 1); mv.visitVarInsn(ASTORE, 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()")

感觉还是javassist
操作起来更简便。
RASP
检测ProcessBuilder
利用RASP
对ProcessBuilder.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" + " 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; } }
|
当执行非白名单命令时抛出异常
