0x00 前言

RASP(Runtime Application self-protection)是一种在运行时检测攻击并且进行自我保护的一种技术。关于RASP技术和RASP实践,这是我写的第二篇文章,上一篇文章在这里,是关于RASP技术在PHP中的实践,无论是从设计思路还是技术实现来说,PHP RASP和OpenRASP Java实现都比较相似。有兴趣的同学可以粗略看看,不必太关心技术细节,了解设计思路、工作原理即可,很多时候,对技术宏观的把握对理解技术细节和设计思路非常有用。

0x01 RASP技术

关于RASP的发展、RASP与各种WAF的区别以及RASP的简单原理可以看《一类PHP RASP的实现》这篇文章的RASP概念我的WAF世界观这两部分,这两部分大约可以回答关于RASP技术的两个问题,一是RASP技术是工作在哪一层,为了解决什么问题而存在的,二是它大致是怎么实现的,这里就不写了。下面以OpenRASP为例分析一下Java RASP的实现。

0x02 JVMTI && Java Instrumentation

JVMTI是JVM提供的一些回调接口集。JVM在特定的状态会执行特定的回调函数,开发者实现这些回调函数就可以实现自己的逻辑。Java Instrumentation就是利用JVMTI实现的。

Java Instrumentation是Java强大功能的一个体现,Java Instrumentation允许开发者访问从JVM中加载的类,并且允许对它的字节码做修改,加入我们自己的代码,这些都是在运行时完成的。无需担心这个机制带来的安全问题,因为它也同样遵从适用于Java类和相应的类加载器的安全策略。

0x03 OpenRASP简要分析

OpenRASP以Java Instrumentation的方式工作在JVM层,它主要通过hook可能引发漏洞的关键函数,在这些关键函数执行之前添加安全检查,根据上下文和关键函数的参数等信息判断请求是否为恶意请求,并终止或继续执行流。

Java Instrumentation允许开发者添加自定义的字节码转换器来对Java字节码进行自定义的操作转化,从而实现在不修改源代码的情况下,实现AOP。当然,有一些开源的Java字节码类库帮助开发者操作Java字节码。OpenRASP的开发者选择了ASM这个框架,相比其他的框架,ASM的优点是更加底层、更加灵活,功能也更加丰富。

OpenRASP另一个值得称道的做法是使用了js来编写规则,通过Java语言实现的js引擎来执行脚本。OpenRASP官网关于为什么用JavaScript实现检测的逻辑的解释是 OpenRASP会支持PHP、DotNet、NodeJS、Python、Ruby等多种开发语言,为了避免在不同平台上重新实现检测逻辑,引入了插件系统; 选择JS作为插件开发语言。当然,这是优点之一。笔者认为,另一个优点是使用JS插件系统可以很方便的支持热部署。笔者曾经有幸参与某商业RASP产品的研发和测试,各语言规则的重复编写和热部署是两个令我们头痛的问题,而使用js引擎就可以很好的解决这两个问题。

0x04 Talk is cheap

俗话说,Talk is cheap,show me the code. 下面简要分析一下OpenRASP的代码。OpenRASP是一个Java Instrumentation,它的入口是public static void premain(String agentArg, Instrumentation inst)函数,OpenRASP中的premain方法在com.fuxi.javaagent.Agent中。这个方法中主要的代码如下

1
2
3
4
5
6
//........省略部分代码........
JarFileHelper.addJarToBootstrap(inst);
//........省略部分代码........
PluginManager.init();
initTransformer(inst);
//........省略部分代码........

JarFileHelper.addJarToBootstrap(inst)的关键是JarFileHelper中inst.appendToBootstrapClassLoaderSearch(new JarFile(localJarPath))这行代码,它的作用是将rasp.jar加入到bootstrap classpath里,优先其他jar被加载。在Java Instrumention的实现中,这行代码应该是很常见的。为什么要这样做呢?在Java中,Java类加载器分为BootstrapClassLoader、ExtensionClassLoader和SystemClassLoader。BootstrapClassLoader主要加载的是JVM自身需要的类,由于双亲委派机制的存在,越基础的类由越上层的加载器进行加载,因此,如果需要在由BootstrapClassLoader加载的类的方法中调用由SystemClassLoader加载的rasp.jar,这违反了双亲委派机制。所以,而rasp.jar添加到BootstrapClassLoader的classpath中,由BootstrapClassLoader加载,就解决了这个问题。

接着是PluginManager.init(),初始化插件系统。PluginManager.init()的具体代码如下:

1
2
3
JSContextFactory.init();
updatePlugin();
initFileWatcher();

JSContextFactory.init()的主要作用是初始化js引擎,这里使用的js引擎是Mozilla的Rhino,Mozilla旗下提供了各种语言的js引擎的成熟实现,例如用C/C++实现的js引擎SpiderMonkey等。JSContextFactory.init()先初始化了js引擎,执行了一堆js文件,笔者js水平有限,就不分析了。接着把jsstdout注入到js环境中,处理js环境中的输出。把JSTokenizeSql和JSRASPConfig注入到RASP对象中,为js环境提供sql_tokenize方法,提供对SQL语句进行tokenize的能力。接下来updatePlugin()方法读取插件目录下的js文件,执行js脚本,加载插件。initFileWatcher()添加了文件监控,一旦插件目录下的js文件发送变化,则调用updatePluginAsync()执行clean方法,执行js脚本,更新插件,实现热部署功能。

接下来是initTransformer(inst)方法,它调用inst.addTransformer(new CustomClassTransformer(), true)方法添加了CustomClassTransformer这个Class转换器,这样,每一个类的字节码在加载之前都会调用CustomClassTransformer.transform(..)(参数省略)方法,对字节码进行更改之后,字节码被载入JVM中,接下来继续类加载过程:加载->验证->准备->解析->初始化。CustomClassTransformer类在初始化的时候创建了很多个ClassHook对象,代码如下:

1
2
3
4
5
6
7
8
9
10
public CustomClassTransformer() {
hooks = new HashSet<AbstractClassHook>();

addHook(new WebDAVCopyResourceHook());
addHook(new CoyoteInputStreamHook());
addHook(new DeserializationHook());
addHook(new DiskFileItemHook());
addHook(new FileHook());
//.....省略......
}

我们看一下CustomClassTransformer.transform(..)这个方法,如果当前加载的类是需要转换的,即hook.isClassMatched(className)返回true,就会调用hook.transformClass(className, classfileBuffer)对字节码进行转化。transformClass的代码在com.fuxi.javaagent.hook.AbstractClassHook中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
public byte[] transformClass(String className, byte[] classfileBuffer) {
try {
ClassReader reader = new ClassReader(classfileBuffer);
ClassWriter writer = new ClassWriter(reader, computeFrames() ? ClassWriter.COMPUTE_FRAMES : ClassWriter.COMPUTE_MAXS);
LOGGER.debug("transform class: " + className);
ClassVisitor visitor = new RaspHookClassVisitor(this, writer);
reader.accept(visitor, ClassReader.EXPAND_FRAMES);
return writer.toByteArray();
} catch (RuntimeException e) {
LOGGER.error("exception", e);
}
return null;
}

这段代码调用了ASM库,关于ASM库的详情请看这里,为了方便读者,笔者翻译了一下,在这里。如上文中所说,ASM是一个强大的字节码操作库。它主要使用了设计模式中的访问者模式,使用访问者模式的好处是数据结构与数据操作分开。在ASM中哪些是数据结构呢?转换前字节码中类、方法、注释、成员变量等就是数据结构,对这些字节码的操作就是数据操作。在ASM中,开发者不需要关心ASM解析字节码,遍历类、方法、注释等是怎样实现,只需要知道ClassReader.accept()接受一个ClassVisitor实例作为参数,在ASM遍历类、方法、注释时,ASM会调用ClassVisitor.visitMethod()ClassVisitor.visitAnnotation()等方法。开发只需要重写这些方法,就可以操作方法、注释的字节码。如上面的代码所示,OpenRASP开发者创建了一个RaspHookClassVisitor类,重写了visitvisitMethod方法。在visitMethod方法中,ClassHook的hookMethod方法被调用,下面以FileHook为例,看一下hookMethod方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public MethodVisitor hookMethod(int access, String name, String desc, String signature, String[] exceptions, MethodVisitor mv) {
if (name.equals("listFiles")) {
return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
@Override
protected void onMethodEnter() {
loadThis();
invokeStatic(Type.getType(HookHandler.class),
new Method("checkListFiles", "(Ljava/io/File;)V"));
}
};
}
return mv;
}

invokeStatic(Type.getType(HookHandler.class),new Method("checkListFiles", "(Ljava/io/File;)V")这行代码调用了静态方法HookHandler.checkListFiles来实现对java.io.File.listFiles方法的检测。

各关键函数的检测有些区别,我就不分析了,很多都是最后调用com.fuxi.javaagent.plugin.check进行检测,而com.fuxi.javaagent.plugin.check最后调用了各js函数来检测。代码如下

1
2
3
4
5
6
checkProcess = processList.get(i);
function = checkProcess.getFunction();
try {
tmp = function.call(this, scope, function, functionArgs);
}
//......省略......

对检测细节感兴趣的可以一个一个跟。

0x05 关于OpenRASP规则的几点说明

OpenRASP提供了一些官方插件,当然,相关的规则还是需要根据业务和开发水平来定制。开发者水平参差不齐,什么奇葩的实现方式都有。例如,笔者之前参与研发的RASP也有这么一条规则:禁止在SQL语句中出现常量比较操作。但是,这条规则在实际中却有不少误报。很多开发会这样写(用伪代码表示一下):

1
2
3
4
String sql="select * from table where 1=1";
for(key,value in condition.items()){
sql+=" and "+key+"="+"value";
}

开发会自己添加1=1,为了能方便的在循环中在SQL后面的where语句中直接加and,而1=1永远为真,不影响后面的逻辑。所以说,规则永远是和场景分不开的,同样,不深入开发、不深入客户很难做好安全产品。说白了,做安全,开发安全产品和软件工程的目标是一致的,即让大多数的Stakeholder(利益相关者)满意。开发当然也是RASP产品的Stakeholder。好了,扯远了。

OpenRASP中对SQL做了词法分析来实现”零规则”检测。实际上,词法分析的主要作用是将SQL语句分割成数据和代码。SQL注入的本质是代码注入,有了词法分析的帮助,Web层就可以判断对SQL语句的字符串处理是否改变了SQL的逻辑。这是词法分析为什么可以用来检测SQL注入的原因,类似的,利用词法分析也可以检测其他代码注入。不过把SQL的词法分析做好并不容易,一个原因是SQL语句语法复杂,不同的数据库有不同的语法,不同的数据库还有不同的奇葩特性。从代码来看OpenRASP的词法分析暂时还没有区分不同的数据库。

RASP并不能高效地防御所有的漏洞,还是那句话,Web服务器、解释器/JVM、数据库、操作系统各有各的防御阵地,各有各的优势,充分发挥它们的优势,才能更好的做好安全防护。希望这篇文章能对想用RASP产品或者想研发RASP的公司有一些帮助。

0x06 Reference

0x07 关于作者