Java RASP浅析——以百度OpenRASP为例

栏目: Java · 发布时间: 6年前

内容简介:Java RASP浅析——以百度OpenRASP为例

作者:c0d3p1ut0s

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、 PythonRuby 等多种开发语言,为了避免在不同平台上重新实现检测逻辑,引入了插件系统; 选择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 中。这个方法中主要的代码如下

//........省略部分代码........
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() 的具体代码如下:

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对象,代码如下:

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 中,如下所示:

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 方法:

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函数来检测。代码如下

checkProcess = processList.get(i);
function = checkProcess.getFunction();
try {
    tmp = function.call(this, scope, function, functionArgs);
} 
//......省略......

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

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

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

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 关于作者


以上所述就是小编给大家介绍的《Java RASP浅析——以百度OpenRASP为例》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

自制编译器

自制编译器

[日] 青木峰郎 / 严圣逸、绝云 / 人民邮电出版社 / 2016-6 / 99.00元

本书将带领读者从头开始制作一门语言的编译器。笔者特意为本书设计了CЬ语言,CЬ可以说是C语言的子集,实现了包括指针运算等在内的C语言的主要部分。本书所实现的编译器就是C Ь语言的编译器, 是实实在在的编译器,而非有诸多限制的玩具。另外,除编译器之外,本书对以编译器为中心的编程语言的运行环境,即编译器、汇编器、链接器、硬件、运行时环境等都有所提及,介绍了程序运行的所有环节。一起来看看 《自制编译器》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试