前言
从github上发现了一个通过rasp防御log4j漏洞的开源项目,之前一直没学习过rasp,正好通过这个案例,阅读源码看一下rasp是如何实现的。
项目地址:https://github.com/boundaryx/cloudrasp-log4j2
分析
首先阅读了一下README,发现项目本质是一个java-agent
启动的入口在rasp-loader这个模块中,我们分析的话就分析不重启安装这种方式,重启安装原理是一样的。这两种只是java-agent的两种模式。
rasp-loader的主类为cn.boundaryx.rasp.EntryPoint
如果当rasp-loader.jar参数不足时,首先调用RASPVMLoader.listJVMPID获取了当前机器所有的jvm信息,并输出jvm pid和进程名称的对应。这里我们关注一下RASPVMLoader类。
RASPVMLoader在初始化的时候,首先直接调用Class.forName来获取VirtualMachine的类对象,如果抛出了ClassNotFoundException异常(我记得windows还是linux默认Class.forName获取不到类对象)
如果抛出异常的话则通过getToolsPath来查找当前jdk的tools.jar的位置,找到了即返回tools.jar的File对象
然后实例化一个tools.jar的URLClassLoader,来读取jar中的com.sun.tools.attach.VirtualMachine
以上操作也是java agent一般获取com.sun.tools.attach.VirtualMachine类对象的方式,具有学习意义。
然后回到main方法中RASPVMLoader.listJVMPID的调用,其实就是简单的反射调用VirtualMachine获取jvm信息
接着看main方法,如果参数充足的话,例如:
1 | java -jar rasp-loader.jar attach 10001 |
则进入else if中
这里,我们关注一下raspvmLoader.attach,raspvmLoader.loadAgent,raspvmLoader.detach
发现同样也是反射调用VirtualMachine的一些方法,将rasp-loader.jar作为java-agent的jar包加载到指定的jvm内存中
然后看一下项目的dist目录中的rasp-loader.jar的MANIFEST.MF文件,看一下其Agent-Class和Premain-Class是哪个类。最后发现都是cn.boundaryx.rasp.Agent类。Agent-Class和Premain-Class分别是运行时加载的agent和运行前加载的agent的主类。
照之前说的我们主要分析不重启安装这种方式,也就是运行时加载agent这种方式
cn.boundaryx.rasp.Agent.agentmain
agentmain方法的agentArgs为之前EntryPoint的main方法中传过来的attach这个字符串
因此直接走到了install方法
从注释可以看出来,install方法作用是安装rasp
首先调用了instrumentation.appendToBootstrapClassLoaderSearch将两个jar包加入了classloader的搜索目录
然后调用raspLoader.loaderJar(RASP_CORE_JAR),把rasp-core.jar中的jar加入到classloader的搜索范围内
回到install方法,然后从classloader中找到cn.boundaryx.rasp.RASPLauncher方法,然后调用其launch方法。内容比较简单,直接进入classTransformer.init()方法中看初始化了什么操作
首先向Instrumentation中注册了Transform,第二个参数true标志着,可以对已经加载的类重新进行转换处理
接着看this.getRetransform的实现,首先调用了instrument的getAllLoadedClasses方法获取了所有加载类,如果JndiHook.classMatch(clazz.getName().replace(“.”, “/“))为真就进入循环
JndiHook.classMatch实现如下,即类名为:javax/naming/InitialContext
然后如果类不是接口或者可修改并且不以java.lang.invoke.LambdaForm开头,则进入inst.retransformClasses方法中。inst.retransformClasses会调用之前inst.addTransformer加进去的Transformer类的transform方法进行修改
在这个项目中,tansform方法源码如下,直接看关键部分,源码第41行把当前类也就是javax/naming/InitialContext组合成CtClass对象,然后作为入参传入JndiHook.transform方法
cn.boundaryx.rasp.hooks.transform的源码如下,调用到beforeMethod(ctClass)
beforeMethod源码如下,调用到getEnhancedCodeWithException(JndiAnalyzer.class,”checkJndiStr”,”$1”, String.class)
getEnhancedCodeWithException源码如下,可以看到其返回值就是一个字符串的源码,源码内容为
1 | try cn.boundaryx.rasp.analyzer.JndiAnalyzer.checkJndiStr($1) catch (cn.boundaryx.rasp.exception.RASPSecurityException RASPSecurityException) {throw RASPSecurityException;} |
看下cn.boundaryx.rasp.analyzer.JndiAnalyzer.checkJndiStr,发现该方法主要获取当前调用的堆栈的信息,如果当前堆栈中有org.apache.logging.log4j.core.lookup.Interpolator.lookup,则抛出异常,告知被RASP拦截
回到JndiHook.beforeMethod方法中,项目把以上变量名为src的代码插入到ctClass也就是javax/naming/InitialContext的lookup方法的最前面,完成对jvm中javax/naming/InitialContext类的修改。因此,打上这个agent之后,每次调用javax/naming/InitialContext的lookup方法时,都会检查当前调用堆栈中是否包含org.apache.logging.log4j.core.lookup.Interpolator.lookup,如果包含则说明此处的lookup是通过log4j的漏洞调用到的,然后就抛出异常,进行告警。
总结
这个项目比较具有启发性意义,这个rasp的原理实际上就是对类进行了修改,检查其堆栈是否有异常。以后其他场景用到的时候可以对其进行二次修改。