找回密码
 会员注册
查看: 17|回复: 0

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

[复制链接]

4

主题

0

回帖

13

积分

新手上路

积分
13
发表于 2024-10-9 03:19:24 | 显示全部楼层 |阅读模式
//文 | 美团信息安全团队许乐 、孙绥 、石东华、陈驰、郁丛祥、卢世宇等。01背景RASP 是 Runtime Application Self-Protection(运行时应用自我保护)的缩写,是一种应用程序安全技术。RASP 技术能够在应用程序运行时检测并阻止应用级别的攻击。随着云计算和大数据的发展,应用程序安全越来越受到重视。RASP 技术作为一种新型的安全防护手段,正在逐渐被业界接受并广泛应用。其中Java RASP 是一种针对 Java 应用程序的 RASP 技术。通过在 Java 虚拟机(JVM)级别进行监控和防护,能够有效防止对 Java 应用程序的攻击。1.1RASP建设挑战在业界,RASP的部署形式一般有agentmain、premain两种方式,二者各有优劣。适合不同的业务场景,以及安全需求:agentmain:业务无需改动,无需重启,热插拔,动态升级。有性能抖动,业务有感知。premain:需要改动,需要重启,前置注入,升级需要重启。无性能抖动,业务无感知。美团的RASP建设时,大部分业务都已经在线上运营,而且有多个发布平台,没有提供一个统一的方式来更改启动参数,也就是说无法通过premain方式实现快速部署。为了抓住主要矛盾,快速解决大部分风险问题,我们选择了agentmain方式。1.1.1 业务场景复杂技术方案的设计,依赖于业务形态。美团内部的业务服务中,Java语言占比80%以上,是主要的风险所在。2010年至今,有特别复杂的业务部署形态、业务依赖环境、繁多的JDK等等,这些都是RASP技术方案的挑战:业务部署方式:物理机、宿主机、富容器、轻容器等;发布环境:由于历史原因公司已知的发布系统至少有3个;Web中间件:Spring Boot、Jetty、Tomcat、WebLogic、自研框架等;JDK版本:OpenJDK、OracleIDK、 MJDK、Kona、Dragonwell、毕昇等;进程数量:单个主机上进程数量和生命周期差异大,有的几千个进程,生命周期有分钟级、年级等。问题的拆解思路依旧是抓住主要矛盾,以JDK版本为例,各个版本JDK的主机占比如下图1:图1 公司RASP JDK版本分布占比业务目标确定后,解决方案同样具体到某一类的JDK上。同样,在发布环境、Web中间件的差异上,对RASP也有了更多的兼容要求。1.1.2 对业务性能影响大agentmain的动态注入机制,对JVM的影响是不可规避的。影响大小可以从与其他安全防护产品的部署位置看出,下图2是常见的基础安全防护产品:WAF、HIDS和RASP,他们与业务的隔离方式有以下几类:主机隔离进程(容器)隔离无隔离(或者类加载器隔离)图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指标抖动情况运行时注入TP9999指标突增下图4为运行时注入TP9999指标抖动情况。单机维度,注入时TP9999从5ms飙升到1000ms,大幅度增加,TP9999出现明显的尖刺,对响应时间敏感的服务影响特别大。图4 ?运行时注入TP9999指标抖动情况启动时性能差与检测逻辑执行耗时长在RASP启动时,大量请求进入到检测流程中,此时RASP检测代码没有完成预热,检测方法处于字节码解释运行模式,执行效率低,从而导致启动时TP线高。如果正常的请求检测耗时过长,将严重影响业务的TP线,甚至导致请求超时。在RASP运行过程中,因为检测引擎执行耗时长也会导致业务超时。1.1.3 升级变更难由于原生Java Agent的限制问题,JVM一旦加载了Agent,就无法进行更新,只能等待JVM重启。图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在部署中遇到的第一个重大问题。1.1.4 监控难当JVM加载Java Agent后,由于其运行在业务的同一层面,必然会对业务产生一定的影响。这些影响可能包括CPU使用率飙高、TP9999线的波动,甚至可能出现故障如内存泄漏、磁盘打满、核心转储(core dump)、触发JDK Bug、线程死锁、GC时间变长等等各种问题。业务反馈的线上各类问题的占比如下图6所示。图6 ?RASP各类故障占比由于RASP接入对用户无感知,一旦出现这些问题,业务方定位问题的源头往往耗费大量时间。业务需要对业务状态日志、GC日志、系统变更日志等进行详细的排查,以确定问题的根因。在实际的运行过程中,往往是业务最先反馈RASP影响,而RASP不能做到对故障及时感知与处理。1.2RASP架构介绍美团 RASP 利用 Java agent?和 instrumentation 技术,通过 ASM 修改类字节码,实时分析检测命令执行、文件访问、反序列化、JNDI、SQL注入等入侵行为。它最初是从开源项目 btrace 演化而来,后使用 Golang 重写了 btrace 的进程注入的功能,即架构中的 RASP Daemon 部分、在 Java Agent 端也参考了一些开源项目和公司内部的性能诊断工具。经过多年的迭代,RASP 逐渐形成目前的架构。通过RASP管理端进行主机维度的配置下发,将最新配置更新应用到 RASP Daemon。日志收集和jar包下载使用公司基础组件,通过这些组件的协同工作,实现对 RASP 部署过程的管理,包括支持灰度发布、配置回滚、降级和一键关闭操作。下图7为 RASP 的配置分发流程。图7 RASP 的配置分发流程02解决方案2.1?灰度部署方式和复杂场景的兼容2.1.1 RASP 启动方式传统的RASP直接修改JVM启动参数增加RASP的Java Agent参数,即premain 方式。而美团的RASP在最初只支持运行注入agentmain方式,不支持premain。原因主要是下面的2个方面:在RASP项目建立时,公司的机器节点数量已经有几十万规模了,安全风险较大,急需立即推动覆盖,agentmain运行时注入方式能够满足快速上线的需求;项目初期公司没有统一的服务发布平台,并且每个业务线的发布脚本不统一,如果通过修改JVM启动参数来启动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_OPTIONSfi// 启动Java进程...2.1.2 配置分发方案在 RASP 升级新版本时,为尽可能地提高稳定性,需要按照一定策略进行灰度升级。公司内部分为测试和生产等多种环境,并且测试环境服务器数量为万级别,RASP 需先在线下环境稳定运行足够时候后再开始线上环境灰度;服务按照重要性(或者环境复杂性)从低到高划分为普通服务、重要服务、高优服务3个类别,依次进行。每一个服务需要再按照主机数量的百分比进行灰度,一个服务下的主机不能同时进行 RASP 的升级,需按照 10%、30%、50%、100% 的比例灰度。2.1.3 Web/JDK版本识别与准入RASP Daemon(golang语言) 通过识别进程的cmdLine、JDK、Tomcat、Jetty、Spring Boot等的关键jar包,解析出JDK版本、Web类型和版本。对于已经兼容的服务可以开启注入,对于无法识别或者与RASP不兼容的服务关闭注入(es、jetty等个别版本),最大程度的减少对业务的影响。2.1.4 ?组件的兼容性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包拆分前后对比对于拆分后的架构,首次注入 RASP Agent 加载V1.0的插件,在需要对插件进行更新时,清除RASP PluginV1.0对象的引用和PluginClassLoader对象,然后创建新的PluginClassLoader实例重新加载并初始化V1.1版本插件,从而实现插件的卸载与热更新。上面拆分方案实现依靠自定义RASP类加载器,RASP的类加载器层次结构(agentmain)如下图9所示:图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加载,并且脚本在磁盘加密在运行时解密。2.3?premain & agentmain 两种方式兼顾agentmain和premain方是Java Agent的两种启动方式,agentmain在Java进程启动后加载,而premain在Java进程启动前加载。由于启动时机不一样,带来的差异主要有agentmain 更新加载更加灵活,但是字节码修改时存在性能问题,特别是对性能比较敏感的服务;而premain需要将javaagent参数加入到JVM启动命令行中,完全依赖业务启动,不太灵活,但是性能上比较稳定。美团RASP采用agentmain与premain 结合方式,平衡灵活性与性能。原则上premain逻辑尽可能的简单,避免频繁的迭代与升级。2.3.1 premain 一期方案RASP在加载时,Java进程的CPU会短暂的升高甚至打满,并且CPU核数越少,升高越明显持续时间越长。根因是Java Agent首次加载时会触发JVM中的code cache区域清零机制(可以认为是JDK的bug),大量热点代码的编译导致JIT编译线程将CPU打满,并且这种现象在CPU核数低于4核时表现尤为明显。Manifest-Version: 1.0Premain-Class: com.meituan.rasp.agent.RaspAgentAgent-Class: com.meituan.rasp.agent.RaspAgentCan-Redefine-Classes: trueCan-Retransform-Classes: trueCan-Set-Native-Method-Prefix: true为了解决运行时CPU飙高问题,我们引入空的premain包(premain v1.0)(仅开启上面的字节码转换的开关Can-Redefine-Classes,无任何逻辑,也不修改字节码),在应用启动前加载,该方案取得较大优化效果。因为无任何代码,代码兼容性风险极小(并不是没有),因此能快速上线解决CPU飙高问题。以某个业务的主机为例子,在优化前后的cpu.busy 指标如下图10所示(注入前后10分钟)。图10 ?cpu.busy指标优化前后对比图10中注入时间为 2022-11-23 05:20:00,红色为优化前的cpu.busy指标,优化前即使注入前系统负载很低(4核8G,cpu.busy
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-7 07:19 , Processed in 0.636315 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表