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 { @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 { @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 { @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;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个参数,而其他两个是压栈局部变量表第一个参数?
而如果检测 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 { 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); ClassVisitor cv = new ClassVisitor (Opcodes.ASM5, cw) { @Override public MethodVisitor visitMethod (int i, final String s, String s1, String s2, String[] 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(); mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System" , "out" , "Ljava/io/PrintStream;" ); mv.visitLdcInsn("CALL " +s+ " method" ); 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