美团RASP大规模研发部署实践总结

背景

RASP是Runtime Application Self-Protection(运行时应用自我保护)的缩写,是一种应用程序安全技术。RASP 技术能够在应用程序运行时检测并阻止应用级别的攻击。随着云计算和大数据的发展,应用程序安全越来越受到重视。RASP 技术作为一种新型的安全防护手段,正在逐渐被业界接受并广泛应用。其中Java RASP 是一种针对 Java 应用程序的 RASP 技术。通过在 Java 虚拟机(JVM)级别进行监控和防护,能够有效防止对 Java 应用程序的攻击。

RASP建设挑战

在业界,RASP的部署形式一般有agentmainpremain两种方式,二者各有优劣。适合不同的业务场景,以及安全需求。

  1. agentmain:业务无需改动,无需重启,热插拔,动态升级。有性能抖动,业务有感知。
  2. premain:需要改动,需要重启,前置注入,升级需要重启。无性能抖动,业务无感知。

美团的RASP建设时,大部分业务都已经在线上运营,而且有多个发布平台,没有提供一个统一的方式来更改启动参数,也就是说无法通过premain方式是实现快速部署。为了抓住主要矛盾,快速解决大部分风险问题,我们选择了agentmain方式。

业务场景复杂

技术方案的设计,依赖于业务形态。美团内部的业务服务中,Java语言占比80%以上,是主要的风险所在。2010年至今,有特别复杂的业务部署形态、业务依赖环境、繁多的JDK等等,这些都是RASP技术方案的挑战。

  1. 业务部署方式:物理机、宿主机、富容器、轻容器等;
  2. 发布环境:由于历史原因公司已知的发布系统至少有3个;
  3. Web中间件:Spring Boot、Jetty、Tomcat、WebLogic、自研框架等;
  4. JDK版本:Oracle、OpenJDK、 MJDK、Kona、Dragonwell、毕昇等;
  5. 进程数量:单个主机上进程数量和生命周期差异大,有的几千个进程,生命周期有分钟级、年级等;

问题的拆解思路依旧是抓住主要矛盾,以JDK版本为例,各个版本JDK的主机占比如下图1;

图1 公司JDK版本分布占比

图1 公司JDK版本分布占比

业务目标确定后,解决方案同样具体到某一类的JDK上。同样,在发布环境、Web中间件的差异上,对RASP也有了更多的兼容要求。

对业务性能影响大

agentmain的动态注入机制,对JVM的影响是不可规避的。影响大小可以从与其他安全防护产品的部署位置看出,下图2是常见的基础安全防护产品:WAF、HIDS和RASP,他们与业务的隔离方式有以下几类:

  1. 主机隔离
  2. 进程(容器)隔离
  3. 无隔离(或者类加载器隔离)

图2 主机安全防护产品与业务的隔离等级

图2 主机安全防护产品与业务的隔离等级

与其他的安全产品相比,如网络应用防火墙(WAF)和主机入侵检测系统(HIDS),RASP与业务部署在同一Java虚拟机(JVM),其隔离级别是最低的。这就意味着,当RASP自身出现BUG或者与业务不兼容时,对业务造成直接影响。RASP 一旦出现故障那至少是S4级别(核心功能受影如资损、客诉,且预判5分钟无法恢复) 。从业务指标上分为cpu和执行耗时,执行耗时方面主要是对服务的TP9999影响较大,而CPU方面出现cpu.busy指标抖动情况。对于业务的指标影响,有以下几种:

运行时注入cpu.busy指标突增

下图3为特殊情况下运行时注入cpu.busy指标抖动情况,在RASP注入时间内(CPU分钟级别采样),Java 进程的CPU从0%飙升到50%,然后又恢复。如果RASP注入之前Java进程的CPU已经很高了,注入时CPU会直接打满(注入前后10分钟)。

图3 运行时注入cpu.busy指标抖动情况

图3 运行时注入cpu.busy指标抖动情况

运行时注入TP9999指标

下图4为运行时注入TP9999指标抖动情况。单机维度,注入时TP99995ms飙升到1000ms,大幅度增加,TP9999出现明显的尖刺,对响应时间敏感的服务影响特别大。

图4 运行时注入TP9999指标抖动情况

图4 运行时注入TP9999指标抖动情况

启动时性能差与检测逻辑执行耗时长

在RASP启动时,大量请求进入到检测流程中,此时RASP检测代码没有完成预热,检测方法处于字节码解释运行模式,执行效率低,从而导致启动时TP线高。如果正常的请求检测耗时过长,将严重影响业务的TP线,甚至导致请求超时。 在RASP运行过程中,因为检测引擎执行耗时长也会导致业务超时。

升级变更难

由于原生Java Agent的限制问题,JVM一旦加载了Agent,就无法进行更新,只能等待JVM重启。

图5 运行时Java Agent的实现原理与升级过程

图5 运行时Java Agent的实现原理与升级过程

图5左边的图展示了一个典型的运行时Java Agent的实现原理。在这个过程中,守护进程(这里指主动发起Attach的进程RASP Daemon)会attach到目标JVM上,然后RASP Agent的jar包会被JVM的AppClassLoader加载,接着Agent就会初始化并开始运行。然而,由于JVM类加载机制的限制,同一个类(Agent入口类)无法被AppClassLoader加载器加载两次。使用新的Agent jar包重新attach,即使attach成功,也不会加载新的类。因此想要增加新的功能或者进行bug修复,就必须等待业务进程重启后才能实现。

这也就是说,RASP功能的升级完全依赖于业务进程的重启时机。然而,我们发现线上有些业务,如大数据服务的核心节点,其重启时间可能长达半年甚至更长时间,这就使得RASP的功能升级过程变得异常漫长。由于服务长期未重启,RASP版本无法进行更新。影响主要有2个方面,一方面长期未重启服务的RASP版本低于最新版本,RASP Daemon需要兼容多种RASP Agent版本,这无疑提升了代码工程向下兼容的工作量和稳定性;另一方面,未重启的服务最新的hook点无法生效,也带来一定的安全风险。

热更新是强诉求

在美团内部,安全部门需要不对业务有过多打扰的前提下保障业务安全运行。大规模重启服务风险高,不具备可实施性。如果遇到紧急漏洞或者重大bug时,这种升级难的问题尤为突出。升级难的问题是RASP在部署中遇到的第一个重大问题。

监控难

当JVM加载Java Agent后,由于其运行在业务的同一层面,必然会对业务产生一定的影响。这些影响可能包括CPU使用率飙高、TP9999线的波动,甚至可能出现故障如内存泄漏、磁盘打满、核心转储(core dump)、触发JDK Bug、线程死锁、GC时间变长等等各种问题。业务反馈的线上各类问题的占比如下图6所示。

图6 RASP各类故障占比

图6 RASP各类故障占比

由于RASP接入对用户无感知,一旦出现这些问题,业务方定位问题的源头往往耗费大量时间。业务需要对业务状态日志、GC日志、系统变更日志等进行详细的排查,以确定问题的根因。在实际的运行过程中,往往是业务最先反馈RASP影响,而RASP不能做到对故障及时感知与处理。

RASP架构介绍

美团 RASP 利用 Java agent 和instrumentation技术,通过 ASM 修改类字节码,实时分析检测命令执行、文件访问、反序列化、JNDI、SQL注入等入侵行为。它最初是从开源项目btrace 演化而来,后使用Golang重写了btrace的进程注入的功能,即架构中的 RASP Daemon 部分,在 Java Agent 端也参考了一些开源项目和公司内部的性能诊断工具。经过多年的迭代,RASP 逐渐形成目前的架构。

通过RASP管理端进行主机维度的配置下发,将最新配置更新应用到 RASP Daemon。日志收集和jar包下载使用公司基础组件,通过这些组件的协同工作,实现对 RASP 部署过程的管理,包括支持灰度发布、配置回滚、降级和一键关闭操作。下图7为 RASP 的配置分发流程。

图7 RASP的配置分发流程

图7 RASP的配置分发流程

解决方案

灰度部署方式和复杂场景的兼容

RASP 启动方式

传统的RASP直接修改JVM启动参数增加RASP的Java Agent参数,即premain方式。而美团的RASP在最初只支持运行注入agentmain方式,不支持premain。原因主要是下面的2个方面:

  1. 在RASP项目建立时,公司的机器节点数量已经有几十万规模了,业务先行,安全补位。已经面临风险,需要尽快实现安全能力覆盖。
  2. 早期公司内部服务发布平台不完善,有多个发布系统,并且每个业务线的发布脚本不统一,统一控制的力度弱。

综合业务现状与安全诉求,比较符合技术选型的是agentmain机制。无需业务改动,也不依赖统一的代码发布平台,做到安全部门可控的能力覆盖。

经过多年部署,RASP已经覆盖大部分业务,具备相应安全能力。但也逐步遇到业务抱怨RASP注入带来的性能抖动问题。随着公司基础组件建设,也逐步统一了代码发布系统,在JAVA类服务的管控上有了统一的控制入口。同时,IDC内服务形态逐渐从VM虚拟机演化到容器,RASP的服务环境也与以往不一样。

当下主要矛盾发生变化,业务形态发生变化,支持premain的技术方案迫在眉睫。RASP联合服务发布与镜像团队在拉起服务之前将RASP的Java Agent以环境变量的方式设置到服务启动脚本的上下文中。下面为部署脚本中关于RASP环境变量的设置片段。

// 前置检查...

// 增加环境变量
if [[ $RASP_SWITCH=="ON" ]];then 
    JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -javaagent:rasp-premain.jar" && export JAVA_TOOL_OPTIONS
fi

// 启动Java进程...

配置分发方案

在 RASP 升级新版本时,为尽可能地提高稳定性,需要按照一定策略进行灰度升级。

  1. 公司内部分为测试和生产等多种环境,并且测试环境服务器数量为万级别,RASP 需先在线下环境稳定运行足够时候后再开始线上环境灰度;
  2. 服务按照重要性(或者环境复杂性)从低到高划分为普通服务、重要服务、高优服务3个类别,依次进行。
  3. 每一个服务需要再按照主机数量的百分比进行灰度,一个服务下的主机不能同时进行 RASP 的升级,需按照 10%、30%、50%、100% 的比例灰度。

Web/JDK版本识别与准入

RASP Daemon(golang语言) 通过内核进程事件,感知新进程。再识别进程的cmdLine、JDK、Tomcat、Jetty、Spring Boot等的关键jar包,解析出JDK版本、Web类型和版本。对于已经兼容的服务可以开启注入,对于无法识别或者与RASP不兼容的服务关闭注入(es、jetty等个别版本),最大程度的减少对业务的影响。

组件的兼容性

JDK 兼容性:美团RASP除了使用ASM包之外基本上不使用第三方组件,降低供应链攻击,同时减少对不同版本JDK的专有特性依赖,对于JDK的代码也尽可能的本地化到RASP工程中,屏蔽JDK的版本差异性。
Java Agent 兼容性:公司有多种Java Agent 包括性能诊断,安全扫描、动态调试、流量录制、热部署、链路追踪等约十多种,这些工具实现原理都是基于Instrument。 冲突主要在还是在字节码修改上,例如RASP与jdwp的兼容上,最初版本的RASP在业务类中增加方法数量,当用户开启远程debug时,本地代码的方法数量与远程不一样,导致JVM崩溃。Java Agent应该遵循的规范:

字节码的修改应该遵循下面的基本原则:不允许新增、修改和删除成员变量 ;不允许新增和删除方法 ;不允许修改方法签名(来源于:Java 字节码规范);

Java Agent的jar包应该采用自定义类加载加载,依赖包名称前缀替换等方式,避免与其他Java Agent和业务依赖的冲突;

与其他Java Agent约定,在类查找遍历修改时排除其他的Java Agent的包名称,避免相互引用;

对于热部署等Java Agent,由于它不遵循字节码修改的基本规范,很遗憾,目前无法兼容,只能排除关闭注入;

2.2 RASP的运行时注入与更新

运行时注入方式解决了RASP的首次注入不依赖业务重启服务的问题,但是随着部署场景的增加,不可避免的要对RASP进行更新迭代,如何升级成为一个让人头疼的问题。于是更新也不依赖业务重启,成为一个需要解决的最大问题。

插件热更新是一项具有挑战性的技术,也是RASP建设初期要求具备的核心特征之一。由于美团拥有上百万个Java服务节点,一般的Java Agent安装和升级都需要重启Java进程,对于如此庞大规模的服务来说,这并非易事。在超大规模下,如果依赖业务重新发布的方式来使RASP生效,需要等待所有的服务重启一遍。RASP项目没有权限重启业务。因此,对于RASP来说,插件热更新是至关重要的。

在最初的版本中,当RASP注入到业务中后,如果需要更新功能(如修改策略或hook点),仍然需要重新启动Java进程。如果业务不重启,之前版本的RASP会残留在进程中无法卸载,而新版本需要兼容这些无法卸载的部分。这导致线上存在多个不同版本的RASP,不同版本之间的兼容性几乎无法实现,这种方式是行不通的。

因此, RASP借鉴了Tomcat的类加载器架构,将功能分为两类:第一类是需要频繁迭代的功能,如hook点、资源监控、检测引擎、通信等;第二类是几乎不需要改动的部分,如插件加载和初始化部分。将第一类功能抽取出来,形成一个单独的插件包(RASP Plugin),插件包由自定义类加载器加载,使得这部分具备运行时更新的能力。而RASP Agent引导包仅保留几个类,负责初始化插件jar包。下图8展示了拆分前后的对比:

图8 mt-rasp jar包拆分前后对比

图8 mt-rasp jar包拆分前后对比

对于拆分后的架构,首次注入 RASP Agent 加载V1.0的插件,在需要对插件进行更新时,清除RASP PluginV1.0对象的引用和PluginClassLoader对象,然后创建新的PluginClassLoader实例重新加载并初始化V1.1版本插件,从而实现插件的卸载与热更新。上面拆分方案实现依靠自定义RASP类加载器,RASP的类加载器层次结构(agentmain)如下图9所示:

图9 RASP的类加载器层次结构

图9 RASP的类加载器层次结构

从顶层类加载器开始依次说明RASP包的功能和所属的类加载器。

  • rasp-boot.jar:定义全局变量,能够被所有类访问到,使用BootstrapClasLoader加载;
  • rasp-agent.jar :标准的Java Agent 入口类,定义了agentmain/premain 等Agent初始方法、加载plugin并初始化,使用AppClassLoader加载;
  • rasp-plugin.jar :RASP核心实现,包括hook点、检测逻辑、资源监控等功能,使用自定义类加载器RaspClassLoader加载;
  • Script.class :定义检测逻辑,父加载器为RaspClassLoader,使得脚本类能够访问rasp-plugin.jar中的类,使用自定义类加载器ScriptClassLoader加载,并且脚本在磁盘加密在运行时解密。

premain & agentmain 两种方式兼顾

agentmainpremain方是Java Agent的两种启动方式,agentmain在Java进程启动后加载,而premain在Java进程启动前加载。由于启动时机不一样,带来的差异主要有agentmain 更新加载更加灵活,但是字节码修改时存在性能问题,特别是对性能比较敏感的服务;而premain需要将javaagent参数加入到JVM启动命令行中,完全依赖业务启动,不太灵活,但是性能上比较稳定。美团RASP采用agentmainpremain结合方式,平衡灵活性与性能。原则上premain逻辑尽可能的简单,避免频繁的迭代与升级。

premain 一期方案

RASP在加载时,Java进程的CPU会短暂的升高甚至打满,并且CPU核数越少,升高越明显持续时间越长。根因是Java Agent首次加载时会触发JVM中的code cache区域清零机制(可以认为是JDK的bug),大量热点代码的编译导致JIT编译线程将CPU打满,并且这种现象在CPU核数低于4核时表现尤为明显。

Manifest-Version: 1.0
Premain-Class: com.meituan.rasp.agent.RaspAgent
Agent-Class: com.meituan.rasp.agent.RaspAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true

为了解决运行时CPU飙高问题,我们引入空的premain包(premain v1.0)(仅开启上面的字节码转换的开关Can-Redefine-Classes,无任何逻辑,也不修改字节码),在应用启动前加载,该方案取得较大优化效果。因为无任何代码,代码兼容性风险极小(并不是没有),因此能快速上线解决CPU飙高问题。以某个业务的主机为例子,在优化前后的cpu.busy指标如下图10所示(注入前后10分钟)。

图10 cpu.busy指标优化前后对比

图10 cpu.busy指标优化前后对比

图10中红色为优化前的cpu.busy指标,优化前即使注入前系统负载很低(4核8G,cpu.busy <2%),注入瞬间CPU依然飙升很高(28%);蓝色为优化后的cpu.busy指标,优化后cpu.busy曲线较平滑,无明显尖刺。

premain 二期方案

采用premain一期方案的原因是代码足够简单,几乎没有兼容性问题,因此能够快速大规模部署解决棘手的cpu抖动问题,上线效果较好。但是大部分服务虽然CPU不飙高了,但是还有少部分的服务TP9999指标依然影响较大。

修改字节码TP9999线升高分析

premain一期方案主要用来快速解决cpu.busy抖动问题,但是对于性能比较敏感(如tp9999 < 50ms)的业务,运行时字节码修改不可避免的造成STW,从而导致TP9999 线升高。因为字节码修改时需要进入JVM的safepoint,执行字节码转换的方法VM_RedefineClasses::doit会导致应用短暂地暂停响应,这部分代码执行慢就会影响应用TP9999。大批量修改字节码测得火焰图如下图11所示。

图11 批量字节码转换时的Java进程火焰图

图11 批量字节码转换时的Java进程火焰图

从火焰图11看出,RedefineClasses 耗时主要在VM_RedefineClasses::AdjustCpoolCacheAndVtable::do_klass ,hotspot JDK官方也有类似的issue。转换一个类的主要耗时在redefine_single_class方法,初步测试耗时占比在40%~80% 之间,并且一次转换类越多,占STW总耗时越大。

类转换STW时间与服务负载(QPS)关系

在同一服务(硬件配置8核8G)测试了修改类的个数、服务负载和STW时间的关系如下图12所示:

图12 类转换STW时间与服务QPS、转换类数量的关系

图12 类转换STW时间与服务QPS、转换类数量的关系

从上面数据可以对比看出:

  1. 业务请求QPS为0时,STW时间并不为0;
  2. QPS为20时,一次转换的类越多,单个类的STW耗时越长;
  3. 转换相同数量的类,随着QPS的增加,STW时间有增加,但是不明显;

从上面的分析可以看出,修改字节码无法避免的产生STW(当然,优化这部分JDK代码理论上是可以实现的,但是技术难度较高,短期内无法解决),因此只能从规避的角度出发来解决。原则上只要保证字节码修改时没有请求即可。

一种可行的方案是将字节码转换的逻辑前移到JVM启动前(即业务没有流量或者主动摘除流量),并且尽量避免有请求时大批量的回滚/修改字节码,能够在一定程度上避开或者缓解STW影响业务请求响应时间。

premain修改字节码

对于高频率调用的方法如http body参数读取、sql.execute等,使用premain修改字节码插入RASP检测逻辑,premain agent做到轻量级。对RASP的架构做出相应修改,新增rasp-premain.jar,让服务启动前进行加载并初始化,将字节码的转换逻辑前置到启动时,如下图13所示,蓝色jar包为新增的rasp-premain.jar

图13 RASP类加载器增加premain agent

图13 RASP类加载器增加premain agent

为了最大程度复用之前的系统架构,premain加载后虽然字节码已经被转换,但 RASP的功能在逻辑上是关闭的,需要等到 agentmain 注入之后打开检测开关。premain 只做字节码转换,没有日志和通信等功能,不能单独工作。如图14所示,优化前后TP9999指标有较明显改善。

图14 优化前后注入时TP9999指标
图14 优化前后注入时TP9999指标

运行时性能优化与整体指标

  • rasp加载之后的流量预热

在RASP的流量控制层增加对流量的计数,RASP初次接入流量时控制接入流量的比例(如1%),使得业务业务流量能够预热RASP检测逻辑,预热时间或者次数达到设定的阈值后,再开启100%的流量检测。

  • 软降级与逻辑开关

业务负载较高的场景(CPU飙高、hook逻辑执行严重超时等),为了避免RASP检测逻辑加剧性能恶化,RASP采样软降级措施,关闭对应hook类的逻辑开关,使得部分流量不执行检测逻辑。如果性能进一步恶化,RASP运行模式降级为观察上报模式,待系统资源检恢复正常过后,资源监测通过后自动恢复到检测阻断模式。

  • 延时回滚字节码

RASP更新插件代码时,需要将plugin的全部对象置空,否则会有内存泄漏问题,特别是元空间的内存泄漏,将导致业务将运行越来越慢,直到停止运行。从前面的STW时间结论来看,运行时的字节码回滚(和修改机制相同)也会产生STW,因此RASP将hook代码的逻辑开关关闭后,字节码依然留在业务类中,在清理完各种对象引用关系后,依然能够卸载plugin插件。

监控体系建设

全局维度的监控指标:

  • 主机注入覆盖率大盘;
  • coredump总数;
  • 高峰期字节码的修改数量;
  • 熔断超时数量和比例;

单机维度指标:从业务层面到系统层面如下(列举部分)

  • 业务层面:检测引擎执行耗时、TP9999、请求出错率等
  • JVM层面: 堆内存、元空间/永久代、线程死锁、插件加载次数限制、GC、STW耗时、字节码转换等
  • 进程层面:Java进程CPU、内存、coredump、守护进程状态等
  • 系统维度:系统CPU、系统内存、系统磁盘空间、网络等

图15 RASP监控的指标分布
图15 RASP监控的指标分布

系统指标和进程指标对于Golang来说很容易获取,相关api较多。这里仅以JVM指标元空间使用率(MetaSpace)的检测为例子说明。RASP Daemon 执行attach获取目前JVM的最大元空间(MaxMetaSpaceSize)指标,然后读取 /tmp/hsperfdata_${user}/pid 文件解析元空间的占用(usedMetaspaceSize参数在jvm里面是sun.gc.metaspace.used),计算出元空间的占用比例和剩余空间,当剩余空间不足时,禁止RASP Agent注入,防止RASP成为压垮业务的最后一根稻草。

性能影响

测试配置: 8核/8G/150G

压力:QPS梯度100,持续120s,稳定施压 120s

  • cpu.busy

表1 注入前后的cpu.busy指标

基准数据(不加载RASP) 注入数据(加载RASP) CPU指标增量值
QPS=20 3.47% 4.18% 0.71%
QPS=100 11.70% 11.76% 0.06%
QPS=200 20.95% 21.05% 0.15%
QPS=300 32.12% 32.78% 0.66%
QPS=400 41.23% 44.2% 2.97%
QPS=500 52.78% 56.5% 3.73%
最大QPS 620 587.8
拟合方程 拟合方程:y = 0.103x + 1.203 拟合度:0.999 拟合方程:y = 0.109x + 0.827拟合度:0.998

cpu.busy 绝对值增加: 0.06%~3.73%,整体性能与开源的RASP相当

  • 堆内存增加

QPS超过350时系统cpu达到35%,触发弹性扩容,QPS压测到350可以测出最大内存损耗。

表2 注入前后的内存增加值

最小值 平均值 最大值
基准(QPS=350) 422.38MB 638.34MB 3.10GB
注入(QPS=350) 457.69MB 821.59MB 3.30GB
差值 32MB 183MB 0.2GB

注入前后对比,压测到系统弹性扩容的最大QPS,最大堆内存增加约200M,整体性能与开源的RASP相当

  • 元空间增加

元空间/永久代增加2MB,优于开源RASP产品

  • 请求耗时

当前请求耗时控制在5ms内,优于开源RASP产品

漏洞检测

支持的漏洞类型

经过近多年的研发迭代,目前具备的漏洞检测类型如下,基本覆盖常见漏洞(部分):命令执行 (支持native方法)、SQL注入、文件访问、反序列化攻击、JNDI、表达式等等。

实时检测与阻断

不同语言实现的脚本性能比较

开源方案中采用了JavaScript引擎作为实现方式,JS脚本可以被Java、php和c++等各种语言兼容,具备较强的通用性。但是经过测试,与原生Java相比,这些方案在性能上存在较大的差距。尽管JavaScript引擎具有不同语言通用性的大优势,但在执行性能方面并不满足高性能场景下RASP的需求。在美团,相比于性能,检测引擎的语言通用性并不是最重要的考虑因素。下面简单对比一下JavaScript和Java实现的检测引擎的性能。因为检测脚本主要涉及字符串的各种操作,我们选择了字符串累加的for循环作为测试场景。

// java
c+='c'
// javascript
c=c+'c'

经过测试,我们发现Java在执行这种字符串操作的性能方面表现更好。Java作为一种编译型语言,具有较高的执行效率和优化能力。它可以通过使用StringBuilder等高效的字符串操作类来提高性能。相比之下,JavaScript作为一种解释型语言,执行效率相对较低。因此,在高性能场景下,使用Java实现的检测引擎往往能够更好地满足需求。尽管JavaScript引擎具有通用性,但在性能要求较高的场景下,选择使用Java实现的原生检测引擎更为合适

表3 10万量级的for循环中跑出结果如下(单位ms)

javascript java
平均执行耗时 585 6.5

可以看出Java语言实现的检测引擎,性能上具备优越性。美团RASP使用Java语言构建检测引擎,能够满足性能上的需求。

检测脚本的实现

在RASP Plugin中定义了检测脚本需要实现的接口,脚本的实现类由RASP Server下载到磁盘上;RASP Agent定时检测脚本文件是否更新,如果脚本更新,使用新的类加载器加载磁盘上的class文件,并创建实例。

阻断与热修复

在RASP中,通常会在hook方法的执行之前(before)、返回(return)和抛出异常处(throw)增加检测逻辑。RASP通过使用ASM字节码框架,在方法的before、return和throw处织入检测逻辑的字节码(下图16黄色框)。

图16 RASP阻断热修复控制流程
图16 RASP阻断热修复控制流程

这里以在方法返回之前增加hook逻辑为例子说明阻断/热修复的流程:

  1. 字节码插桩:使用ASM工具识别方法中的返回指令如(return、areturn等),在返回指令之前插入RASP的检测方法的字节码,使用instrumentrestransform api将修改后的字节码替换原来的字节码。
  2. 运行时检测:当检测引擎返回阻断异常对象时,方法的异常处理抛出阻断异常,终止方法的执行(上图16红色箭头的流程);当检测引擎返回对象时,提前返回指定的对象,修改返回的返回值(上图16中蓝色箭头的流程);返回Null,表示既不阻断也不返回对象(上图16绿色箭头的流程),不改变当前方法的执行流程和返回对象。

热修复与阻断的区别在于热修复返回的是一个对象,这个对象是修复后的正确的对象。

成果

美团RASP经过多年的建设,在覆盖对象、部署方式、性能优化、兼容性和安全策略等多个方面逐步迭代,现在已覆盖绝大多数Java服务,支持众多web容器部署,基本覆盖常见的安全漏洞,整体覆盖率上达到了较高水位,并且多次检测出海量的漏洞攻击,成为美团IDC基础安全纵深防御体系中最重要的安全能力。

总结

本文主要介绍了美团RASP在研发过程中遇到的问题和解决方案。首先介绍了RASP的痛点问题,包括业务场景复杂、升级变更难、对业务性能影响大和缺少监控等。对于RASP的升级问题,引入了插件热更新的技术,可以在不重启Java进程的情况下,即时地更新RASP的功能。为了降低对业务性能的影响,介绍了采取的优化措施,包括低峰期注入、启动时流量预热、软降级与逻辑开关以及插件卸载时不回滚字节码等关键技术。然后介绍了RASP的监控体系建设,包括监控指标的定义和收集。最后介绍了RASP的性能与灰度策略,通过对性能损耗的测试和分析,可以看出RASP对CPU和QPS的影响较小。在灰度策略方面,RASP结合了业务形态,特性影响等,选择合适的验证机制和测试方法。

后续规划:

  • 新型容器形态支持:美团IDC形态中,逐步从VM、富容器过度到轻容器,未来轻容器的会快速增加,RASP的管控机制、容器隔离机制,都是未来RASP的挑战;
  • 低打扰无感接入:宿主业务的低打扰,注入性能影响,小众场景的覆盖,依旧是RASP的核心重点,让业务无感、自动、默认接入RASP,提升整体IDC防御水位;
  • 管控、监控自动化:管控端的配置下发依赖链路较多、流程较长,配置变更成本风险高,优化为更高效、更实时、更准确的机制;

本文作者

许乐 、孙绥 、石东华、陈驰、郁丛祥、卢世宇等来自于美团信息安全部。

招聘

范围包括不限于HIDS、RASP、EDR、零信任产品等基础安全方向研发

可直接在本公众号内发送简历。

岗位职责

负责美团应用程序自我防护系统的开发,通过Java字节码技术实现Java应用程序的防护;工程能力做到百万级别的Java应用覆盖,保障稳定性和性能;RASP/IAST等安全产品技术预研;

岗位要求

  • 具备计算机科学、信息安全或相关领域的本科及以上学历;
  • 熟练掌握 Java、Python、Go 等编程语言中的一项或多项;
  • 至少能够精通一个Java web框架的原理,有一定的字节码技术的研发使用经验;
  • 有过中间件开发经验,大规模覆盖应用者优先;
  • 具备出色的分析问题和解决问题的能力,良好的团队协作和沟通能力;
  • 具备良好的学习总结能力,并乐于分享;

优先条件

  • 有过JVM技术研究经验;
  • 有过RASP/IAST/APM开发经验;

岗位亮点

  • 技术本身的难度;
  • 挑战工业界的工程能力

知识共享许可协议CFC4N的博客CFC4N 创作,采用 知识共享 署名-非商业性使用-相同方式共享(3.0未本地化版本)许可协议进行许可。基于https://www.cnxct.com上的作品创作。转载请注明转自:美团RASP大规模研发部署实践总结

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据