1. 前言
RASP(Runtime Application self-protection)运行时应用自我保护,是一种在运行时检测攻击并且进行自我保护的技术。早在 2012 年,Gartner 就开始关注 RASP,惠普、WhiteHat Security 等多家国外安全公司陆续推出 RASP 产品。Gartner 在 2014 年应用安全报告里将 RASP 列为应用安全领域的关键趋势,并将其定义为:
Applications should not be delegating most of their runtime protection to the external devices. Applications should be capable of self-protection (i.e., have protection features built into the application runtime environment).
(应用程序不应该依赖外部组件进行运行时保护,而应该具备自我保护的能力,也即建立应用运行时环境保护机制。)
RSAP 将自身注入到应用程序中,与应用程序融为一体,能实时监测和阻断安全攻击,使程序自身具备自我保护的能力,并且应用程序无需在编码时进行任何的修改,只需进行简单的配置即可。这意味着,RASP 运行在程序执行期间,使程序能够自我监控和识别有害的输入和行为。
2. RASP vs WAF
- 误报率低:WAF 放置在 Web 应用程序外层,依赖于分析网络流量,拦截所有它认为可疑的输入而并不分析这些输入是如何被应用程序处理的。RASP 通过对应用程序上下文,会精确分析用户输入在应用程序里的行为,根据分析结果区分合法行为还是攻击行为,然后对攻击行为进行响应和处理。 RASP 不依赖于网络流量分析,大大减少了误报。
- 保护全面性:WAF 在分析与过滤用户输入并检测有害行为方面比较有效,但是对应用程序的输出检查则毫无办法。RASP 不但能监控用户输入,也能监控应用程序组件的输出,这就使 RASP 具备了全面防护的能力。RASP 解决方案能够定位 WAF 通常无法检测到的严重问题——未处理的异常、会话劫持、权限提升和敏感数据披露等等。
然而,没有任何安全解决方案是完美的,每个方案都有优点也有缺点,RASP 也一样,虽然它很多独特的优势,但也存在一些不可忽视的不足,需要安全研发人员不断的改善。
- 性能损耗:RASP 实时拦截、深入检测用户数据流,这是对精确度和误判率都有很大的帮助,但是对用户性能有一些影响,这些性能消耗也必然影响到用户的体验,这也是影响企业客户部署 RASP 的很大一方面原因。现在 RASP 的提供商在优化方面做了很大努力,大部分 RASP 对性能影响在 5% 左右。
- 部署成本:RASP 是针对应用程序的,每个应用程序都必须有独立的探针,不能像防火墙一样只在入口放置一个设备就可以了,并且需要根据应用开发的技术不同使用不同的 RASP。比如 PHP 应用与 Java 应用需要不同的 RASP 产品,增加了部署成本。
3. Java Instrumentation
JVMTI(Java Virtual Machine Tool Interface)是 JVM 提供的一些供用户扩展的本地编程接口集。JVM 在特定的状态会执行特定的回调函数,开发者实现这些回调函数就可以实现自己的逻辑。Java Instrumentation 是 Java SE 5 的新特性,就是利用 JVMTI 实现的。
Java Instrumentation 允许开发者访问从 JVM 中加载类,并且允许对它的字节码做修改,加入我们自己的代码,这些都是在运行时完成的。无需担心这个机制带来的安全问题,因为它也同样遵从适用于 Java 类和相应的类加载器的安全策略。它也是 Java AOP 的一种实现机制。
Instrumentation 的最大作用,就是类定义动态改变和操作,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 -javaagent 参数指定一个特定的 JAR 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。
步骤
1.编写 Java 代理类
这个类中,premain 方法是关键,对比于一般的入口 main 一样,这里的 premain 是在 main 之前执行的。它会告诉 JVM 如何处理加载上来的 Java 字节码。
1 2 3 |
public static void premain(String agentArgs, Instrumentation inst); [1] public static void premain(String agentArgs); [2] |
其中,[1] 的优先级比 [2] 高,将会被优先执行([1] 和 [2] 同时存在时,[2] 被忽略)。在这个 premain 函数中,开发者可以进行对类的各种操作。
2.打包代理类
将这个 Java 类打包成一个 JAR 文件,并在其中的 META-INF/MAINIFEST.MF 属性当中加入” Premain-Class”来指定步骤 1 当中编写的那个带有 premain 的 Java 类。
3.运行
用如下方式运行带有 Instrumentation 的 Java 程序:
1 2 |
java -javaagent:agent_jar_path[=options] java_app_name |
对 Java 类文件的操作,可以理解为对一个 byte 数组的操作(将类文件的二进制字节流读入一个 byte 数组)。开发者可以在“ClassFileTransformer”的 transform 方法当中得到,操作并最终返回一个类的定义(一个 byte 数组)。
4. RASP 技术简析
Java Rasp 技术原理大致如下图:
(图源来自网络)
Java RASP 实现的基本思路类似于 Java 中的 AOP 技术,将 RASP 的 Hook 代码注入到需要进行检测的地方,根据上下文和关键函数的参数等信息判断请求是否为恶意请求,并终止或继续执行流。
通过上文分析我们知道,只要在 JVM 的 -javaagent 参数中配置我们的 RASP 保护程序,就能够轻松实现 Java 的 RASP。
我们通过简单的举例,来说明 Instrumentation 的基本使用方法:
1 2 3 4 5 6 7 8 9 10 |
public class JavaRASPApp { public static void premain(String agentArgs, Instrumentation instru) throws ClassNotFoundException, UnmodifiableClassException { System.out.println("premain"); //注册自定义字节码转换器 instru.addTransformer(new AgentTransformer()); } } |
在运行了 Instrumentation 代理的 Java 程序中,字节码的加载会经过我们自定义的 AgentTransformer,在这里我们可以过滤出我们关注的类和方法,并对其字节码进行相关的修改。
AgentTransformer 类实现了 Java 的代理程序机制提供的 ClassFileTransformer 接口 ,能够在运行时(Runtime)对类的字节码进行替换与修改,通过它的 transform 方法可以得到 ClassLoader(类加载器)、className(类名)、classfileBuffer(字节码)等,此时我们可以在 transform 方法中做文章,即可实现我们的防护程序。
在该示例实现中,我们使用 Javasisst 操作字节码,进行探针织入,最终返回修改后的字节码。
假设我们有一个简单的类 Pepole,其中有一个方法可以增加年龄:
1 2 3 4 5 6 7 8 |
public class Pepole { private String name; private Integer age; public void increaseAge(int n) { this.age += n; } } |
接下来,我们实现 Transformer 类,这个类实现了 ClassFileTransformer 接口:
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 |
public class AgentTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { /** * 此处使用javassist API进行修改 */ ClassPool pool=ClassPool.getDefault(); try { if (!className.equals("Pepole")) { return null; } CtClass ct = pool.get(className); CtMethod m = ct.getDeclaredMethod("increaseAge"); //在方法执行前插入代码 m.insertBefore("{ System.out.print(\"n:\"+$1); }"); //在方法执行后插入代码 m.insertAfter("{System.out.println(this.name); System.out.println(this.age);}"); return ct.toBytecode(); } } ... } |
使用反编译工具查看修改后的 increaseAge 方法结果:
1 2 3 4 5 6 |
public void increaseAge(int n) { System.out.print("n:" + n); this.age += n; System.out.println(this.name); System.out.println(this.age); } |
从上述代码示例中可以看出,Javassist 框架会将类的字节码依照事件驱动模型逐步解析每个方法,当触发了我们需要 Hook 的方法,我们如果在该方法的开头或者结尾插入了进入检测函数的字节码,则会把 Hook 好的字节码返回给 transformer 从而载入 JVM,实现 RASP。
5. 结论
RASP 对应用程序的代码设计没有任何影响,通过无嵌入、无需修改代码的方式实现安全保护。我们可以在重点关注的数据的关键流转节点加入 RASP 探针进行安全过滤,就可以拦截 SQL、会话、有害请求、OGNL 等等信息。如果探针部署的足够充分,可以有效的防御 XSS、CSRF、RCE、SQL 注入等 Web 攻击。
当然,RASP 的缺点也很明显,一是对服务器性能有影响,二是部署问题,需要在每个服务器上部署。这是我们需要持续解决的问题。
本文作者:姜天宇