java运行时应用自保护RASP技术浅析

admin 2025年3月17日10:04:18评论17 views字数 13897阅读46分19秒阅读模式
概念介绍
RASP,全称Runtime application self-protection技术,也就是运行时应用的自我保护,这种保护技术主要体现在以探针的方式存在于代码中,与应用服务融为一体,对底层的关键api进行建立监测和防护。
所以说,RASP技术是一种基于服务器的技术,它可以在功能调用前或调用时能获取访问到当前方法的参数等信息,根据这些信息来判定是否遭受到攻击从而对攻击请求进行阻拦。
RSAP技术由于是从底层对运行时的事件触发进行监测,所以相比较于传统的基于流量监测的安全防护产品来说,RASP技术优势在于直接可以忽略绕过流量监测的攻击方式,比如说传参的各种变形绕过手法,像url双编码、base64编码或者是分块传输绕过等等,以及一些代码逻辑的绕过,对于直接监测底层触发api事件对RASP技术来说就达不到攻击的目的了。相当于说,RASP不会去关注请求的具体内容,而是只看程序运行过程中是否会触发安全威胁,从而去控制应用程序的执行流程,并且实时检测和阻止漏洞攻击行为。
举个例子,以防御sql注入为例,下面这两张图可以直观地比较出基于流量监测的waf产品和RASP技术的不同。
首先是waf产品对http请求的流量监测,原理是通过判断参数中是否包含有单引号或者关键字等规则来进行拦截:

java运行时应用自保护RASP技术浅析

不同于waf产品的防护点位置,RASP技术的主要防护点是在应用程序运行的过程中,RASP技术可以做到程序底层拼接的sql语句到数据库之前进行拦截,如果sql语句没有危险操作,则正常放行,不会影响程序本身的功能,如果存在恶意攻击,则直接将恶意攻击的请求进行拦截或净化参数。

java运行时应用自保护RASP技术浅析

java运行时应用自保护RASP技术浅析

所以从了解完原理后,我们可以发现RASP技术相比较于传统waf的优点在于可以减少大量误报,并且由于是基于底层执行过程的监测,还可以抵御一些未知的漏洞攻击,实现一个纵向的深度防护。
而缺点也比较明显,由于是要嵌入到代码中去执行拦截的,所以不可避免的就是对服务器造成一些性能影响,现在大部分RASP 对性能影响在5%左右。并且也具有一定的开发和部署成本,而且还需要根据应用开发的技术不同使用不同的 RASP
java中RASP技术实现
javaRASP的实现思路,基本思路类似于 Java 中的 AOP 技术,将 RASP 的探针代码注入到需要进行检测的地方,对安全威胁进行拦截。

2.1Java Instrumentation

Java Instrumentation允许开发者访问从 JVM 中加载类,并且允许对它的字节码做修改,加入我们自己的代码,这些都是在运行时完成的。在Instrumentation的实现当中,存在一个JVMTI的代理程序,通过调用JVMTI当中Java类相关的函数来完成Java类的动态操作。
Instrumentation的最大作用,就是类定义动态改变和操作,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 -javaagent 参数我们可以指定一个特定的 JAR 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序,在真正的程序没有运行之前先运行指定的jar包,但是对jar包有两个要求:

ljar包的MANIFEST.MF文件必须指定Premain-Class

lPremain-Class指定的类必须实现premain()方法。

使用javaagent需要几个步骤:

l定义一个MANIFEST.MF文件,必须包含Premain-Class选项,也需要加入Can-Redefine-ClassesCan-Retransform-Classes选项

l创建Premain-Class指定的类,类中包含premain函数,该方法可以进一步加载RASP实现原理

lMANIFEST.MF和写好的各种类打包成jar

l启动java时,添加-javaagent:xx.jar,即可让java先自动执行写好的premain方法

简单来说,Java agent是一个我们通过使用JVM提供的Instrumentation API来开发出来的jar包,可以用来修改已加载到JVM中的字节码文件。
写个demo示例,首先需要创建个agentidea中新建个maven项目,然后再新建个PreMain类:

package com.example;

import java.lang.instrument.ClassFileTransformer;

import java.lang.instrument.IllegalClassFormatException;

import java.lang.instrument.Instrumentation;

import java.security.ProtectionDomain;

public class PreMain {

    public static void premain(String agentArgs, Instrumentation inst) {

        System.out.println("agentArgs:" + agentArgs);

        inst.addTransformer(new DefineTransformer(), true);

    }

    static class DefineTransformer implements ClassFileTransformer {

        @Override

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

            System.out.println("premain load Class: " + className);  // 注意这里的输出

            return classfileBuffer;

        }

    }

}

这个PreMain类中有个premain函数,在这个 premain 函数中,开发者就可以进行对类的各种操作,这里调用了Instrumentation对象的addTransformer函数,这个函数的作用是注册一段代码增强逻辑,它能对所有已定义的类生效。
同时还声明了一个静态类,该类实现了ClassFileTransformer接口,ClassFileTransformer接口就是用来让用户来实现代码增强逻辑的接口,只有一个transform方法,方法的参数是原来的类的字节码以及这个类的classloader对象,返回值则是被增强之后的类的字节码。
然后要pom.xml中添加如下内容

       <build>

        <plugins>

            <plugin>

                <groupId>org.apache.maven.plugins</groupId>

                <artifactId>maven-jar-plugin</artifactId>

                <configuration>

                    <archive>

                        <manifestEntries>

                            <Premain-Class>com.example.PreMain</Premain-Class>

                            <Can-Redefine-Classes>true</Can-Redefine-Classes>

                            <Can-Retransform-Classes>true</Can-Retransform-Classes>

                        </manifestEntries>

                    </archive>

                </configuration>

            </plugin>

        </plugins>

    </build>

这样配置完后用maven打包编译会自动根据里面的选项进行配置,跟MANIFEST.MF文件功能一样。
然后直接maven->package打包编译成jar包,这里我将打包后的jar包重命名为MyAgent.jar
然后再写个主运行程序demo,同样新建maven项目后,新建一个简单的Main业务类:

package com.example;

public class Main {

    public static void main(String[] args) {

        System.out.println("Main.main() in test project");

    }

}

然后在pom添加以下内容。

 <build>

        <plugins>

            <plugin>

                <groupId>org.apache.maven.plugins</groupId>

                <artifactId>maven-jar-plugin</artifactId>

                <configuration>

                    <archive>

                        <manifestEntries>

                            <Main-Class>com.example.Main</Main-Class>

                            <Can-Redefine-Classes>true</Can-Redefine-Classes>

                            <Can-Retransform-Classes>true</Can-Retransform-Classes>

                        </manifestEntries>

                    </archive>

                </configuration>

            </plugin>

        </plugins>

    </build>

将打包编译后的jar包,加上-javaagent参数加载我们刚刚编译的MyAgent.jar,从而将代码增加到字节码中,命令如下:

java -javaagent:MyAgent.jar -jar MyRaspDemoMain-1.0-SNAPSHOT.jar

java运行时应用自保护RASP技术浅析

可以看到在执行main函数之前先执行了我们插入的代码,打印出了执行main函数之前加载的类名。
RASP防护反序列化漏洞
前面已经介绍了RASP技术的实现demo,下面来看看RASP实战怎么防护java反序列化漏洞,参考其他师傅已经写好的github项目:https://github.com/xbeark/javaopenrasp
首先来看看agent类:

public class Agent {

    public static void premain(String agentArgs, Instrumentation inst)

            throws ClassNotFoundException, UnmodifiableClassException {

        Console.log("init");

        init();

        inst.addTransformer(new ClassTransformer());

    }

    private static boolean init() {

        Config.initConfig();

        return true;

    }

premain函数中,将类转换器添加到了Instrumentation,这样在类加载前,我们便有机会对字节码进行操作,植入Rasp的安全探针,并且初始化了config类,从而调用了其initConfig方法,看看Config类:

public class Config {

    public static Map<String, Map<String, Object>> moudleMap = new ConcurrentHashMap<String, Map<String, Object>>();

    @SuppressWarnings({ "rawtypes", "unchecked" })

    public static boolean initConfig() {

        String configStr = readConfig("/main.config");

        if (configStr == null) {

             Console.log("init failed because of config file error");

             return false;

        }

        Map configMap = (Map) JSONUtils.parse(configStr);

        List<Map> moudleList = (List<Map>) configMap.get("moudle");

        for (Map m: moudleList) {

             Map<String, Object> tmpMap = new ConcurrentHashMap<String, Object>();

             tmpMap.put("loadClass", m.get("loadClass"));

             tmpMap.put("mode", m.get("mode"));

             tmpMap.put("whiteList", new CopyOnWriteArrayList<String>((Collection) m.get("whiteList")));

             tmpMap.put("blackList", new CopyOnWriteArrayList<String>((Collection) m.get("blackList")));

             moudleMap.put((String)m.get("moudleName"), tmpMap);

        }

        Console.log(moudleMap.toString());

        return true;

    }

    ...

}

可以看到是从main.config文件中去读取配置信息,并分别根据这些信息来设置白名单和黑名单等。
main.config部分内容如下:

{

    "moudle":

    [

        {

             "moudleName": "java/lang/ProcessBuilder",

             "loadClass": "xbear.javaopenrasp.visitors.rce.ProcessBuilderVisitor",

             "mode": "log", 

             "whiteList":[],

             "blackList":

             [

             "calc", "etc", "var", "opt", "apache", "bin", "passwd", "login", "cshrc", "profile", "ifconfig", "tcpdump", "chmod",

             "cron", "sudo", "su", "rm", "wget", "sz", "kill", "apt-get", "find"

             ]

        },

        {

             "moudleName": "java/io/ObjectInputStream",

             "loadClass": "xbear.javaopenrasp.visitors.rce.DeserializationVisitor",

             "mode": "black", 

             "whiteList":[],

             "blackList":

             [

             "org.apache.commons.collections.functors.InvokerTransformer",

             "org.apache.commons.collections.functors.InstantiateTransformer",

             "org.apache.commons.collections4.functors.InvokerTransformer",

             "org.apache.commons.collections4.functors.InstantiateTransformer",

             "org.codehaus.groovy.runtime.ConvertedClosure",

             "org.codehaus.groovy.runtime.MethodClosure",

             "org.springframework.beans.factory.ObjectFactory"

             ]

        }, ...

在运行了Instrumentation代理的Java程序中,字节码的加载会经过我们自定义的ClassTransformer,在这里我们可以过滤出我们关注的类,并对其字节码进行相关的修改:

public class ClassTransformer implements ClassFileTransformer {

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,

                            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        byte[] transformeredByteCode = classfileBuffer;

        if (Config.moudleMap.containsKey(className)) {

            try {

                ClassReader reader = new ClassReader(classfileBuffer);

                ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);

                ClassVisitor visitor = Reflections.createVisitorIns((String) Config.moudleMap.get(className).get("loadClass"), writer, className);

                reader.accept(visitor, ClassReader.EXPAND_FRAMES);

                transformeredByteCode = writer.toByteArray();

            } catch (ClassNotFoundException e) {

                e.printStackTrace();

            } catch (NoSuchMethodException e) {

                e.printStackTrace();

            } catch (InstantiationException e) {

                e.printStackTrace();

            } catch (IllegalAccessException e) {

                e.printStackTrace();

            } catch (InvocationTargetException e) {

                e.printStackTrace();

            } catch (Exception e) {

                e.printStackTrace();

            }

        }

        return transformeredByteCode;

    }

}

如果说在运行过程出触发了相关的关键类,比如说跟反序列化漏洞相关的java.io.ObjectInputStream类,那就会利用ASM去生成对应的ClassVisitor类,也就是xbear.javaopenrasp.visitors.rce.DeserializationVisitor类。ASM是一个通用的Java字节码操作和分析框架,它可以用于修改现有类或直接以二进制形式动态生成类。
因为我们要做的是获取class中的各种信息,所以我们需要用到下面一些对象:

1. ClassReader:按照Java虚拟机规范中定义的方式来解析class文件中的内容,在遇到合适的字段时调用ClassVisitor中相对应的方法。

2. ClassVisitorjava中类的访问者,提供一系列方法由ClassReader调用。是一个抽象类,我们在使用的时候需要继承此类。使用此对象的时候需要指定asm api的版本。

3. ModuleVisitorJava中模块的访问者,作为ClassVisitor.visitModule方法的返回值,要是不关心模块的使用情况,可以返回一个null。使用此对象的时候需要指定asm api的版本。

4. AnnotationVisitorJava中注解的访问者,作为ClassVisitovisitTypeAnnotationvisitTypeAnnotation的返回值,要是不关心注解的使用情况,可以返回一个null。使用此对象的时候需要指定asm api的版本。

5. FieldVisitorJava中字段的访问者,作为ClassVisito.visitField的返回值,要是不关心字段的使用情况,可以返回一个null。使用此对象的时候需要指定asm api的版本。

6. MethodVisitorJava中方法的访问者,作为ClassVisito.visitMethod的返回值,要是不关心方法的使用情况,可以返回一个null。使用此对象的时候需要指定asm api的版本。

ClassReader类当中,有一个accept()方法,这个方法接收一个ClassVisitor类型的参数,因此accept()方法是将ClassReaderClassVisitor进行连接的桥梁accept()方法的代码逻辑就是按照一定的顺序来调用ClassVisitor当中的visitXxx()方法。

xbear.javaopenrasp.visitors.rce.DeserializationVisitor源码如下:

public class DeserializationVisitor extends ClassVisitor {

    public String className;

    public DeserializationVisitor(ClassVisitor cv, String className) {

        super(Opcodes.ASM5, cv);

        this.className = className;

    }

    @Override

    public MethodVisitor visitMethod(int access, String name, String desc,

                                     String signature, String[] exceptions) {

        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);

        if ("resolveClass".equals(name) && "(Ljava/io/ObjectStreamClass;)Ljava/lang/Class;".equals(desc)) {

            mv = new DeserializationVisitorAdapter(mv, access, name, desc);

        }

        return mv;

    }

}

Apache Commons Collections反序列化漏洞检测 在这个ClassVisitor中,在resolveClass执行之前对其中的恶意参数进行过滤,可以对Apache Commons Collections反序列化执行漏洞进行有效的防护。相关处理类是DeserializationVisitorAdapter

public class DeserializationVisitorAdapter extends AdviceAdapter {

    public DeserializationVisitorAdapter(MethodVisitor mv, int access, String name, String desc) {

        super(Opcodes.ASM5, mv, access, name, desc);

    }

    @Override

    protected void onMethodEnter() {

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

        mv.visitInsn(DUP);

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

        mv.visitVarInsn(ASTORE, 2);

        mv.visitVarInsn(ALOAD, 2);

        mv.visitVarInsn(ALOAD, 1);

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

        Label l92 = new Label();

        mv.visitJumpInsn(IFNE, l92);

        mv.visitTypeInsn(NEW, "java/io/IOException");

        mv.visitInsn(DUP);

        mv.visitLdcInsn("invalid class in deserialization because of security");

        mv.visitMethodInsn(INVOKESPECIAL, "java/io/IOException", "<init>", "(Ljava/lang/String;)V", false);

        mv.visitInsn(ATHROW);

        mv.visitLabel(l92);

    }

`Hook`了相关代码执行,并跳转至反序列化参数执行的过滤器中,过滤逻辑如下,根据main.config配置文件中的mode字段去判断根据什么模式匹配,其中包括白名单、黑名单等匹配模式:

public class DeserializationFilter implements SecurityFilterI {

    @Override

    public boolean filter(Object forCheck) {

        String moudleName = "java/io/ObjectInputStream";

        ObjectStreamClass desc = (ObjectStreamClass) forCheck;

        String className = desc.getName();

        String mode = (String) Config.moudleMap.get(moudleName).get("mode");

        switch (mode) {

            case "block":

                Console.log("block: " + className);

                return false;

            case "white":

                if (Config.isWhite(moudleName, className)) {

                    Console.log("pass: " + className);

                    return true;

                }

                Console.log("block:" + className);

                return false;

            case "black":

                if (Config.isBlack(moudleName, className)) {

                    Console.log("block: " + className);

                    return false;

                }

                Console.log("pass: " + className);

                return true;

            case "log":

            default:

                Console.log("pass: " + className);

                Console.log("log stack trace:rn" + StackTrace.getStackTrace());

                return true;

        }

    }

}

RASP防护实践测试
使用springboot框架在本地写个存在反序列化漏洞的demo项目,代码如下:

String rememberMe = cookie.getValue();

byte[] decoded = Base64.getDecoder().decode(rememberMe);

ByteArrayInputStream bytes = new ByteArrayInputStream(decoded);

ObjectInputStream in = new ObjectInputStream(bytes);

in.readObject();

in.close();

cookie获取rememberMe字段的值并base64解码后进行反序列化,另外再导入common-collection依赖库,然后maven编译打包成jar包,然后用ysoserial工具生成cc5链的payload,同时对payload进行base64编码:

java运行时应用自保护RASP技术浅析

然后将payload贴到cookie中的rememberMe字段,未使用RSAP防护之前成功触发反序列化漏洞执行了命令:

java运行时应用自保护RASP技术浅析

使用RASP防护开启demo项目,如下:

java运行时应用自保护RASP技术浅析

再次重放刚刚的反序列化攻击请求。

java运行时应用自保护RASP技术浅析

可以看到现在的攻击请求在反序列化过程中已经被DeserializationFilterr拦截了。
Reference

https://blog.csdn.net/hy1273383167/article/details/116211211

https://github.com/xbeark/javaopenrasp

原文始发于微信公众号(SAINTSEC):java运行时应用自保护RASP技术浅析

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年3月17日10:04:18
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   java运行时应用自保护RASP技术浅析https://cn-sec.com/archives/3848834.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息