RASP的运行时注入与更新
java agent技术中agentmain可以在应用程序已经运行的时候通过attach API将agent的jar包加载到jvm中,但是随着业务的更改或者RASP的升级更新,都需要加载新的jar包,但是在不方便重启jvm的情况下,如何能在运行时实现热更新是一大问题
RASP借鉴了tomcat的类加载构造
背景:tomcat的类加载机制:因为在tomcat中会运行多个web应用程序,但是多个程序之间可能会使用不同版本的一些库或者类,所以tomcat的类加载模式为:对于大多数类(包括Java的核心库、共享库以及部署在${CATALINA_HOME}/lib
下的库),还是使用双亲委派模型来进行加载;对于web应用私有的类(位于Web应用自身的/WEB-INF/classes
目录或/WEB-INF/lib
目录下的类),Tomcat的Web应用类加载器会在尝试父类加载器之前优先尝试加载这些类
第一类是需要频繁迭代的功能,如hook点、资源监控、检测引擎、通信等;
第二类是几乎不需要改动的部分,如插件加载和初始化部分
将第一类功能抽取出来,形成一个单独的插件包(RASP Plugin),这部分包含实际的安全逻辑,插件包由自定义类加载器加载(这种类加载器能够在运行时从指定位置(如本地文件系统、远程服务器等)加载插件jar包,并且允许卸载旧版本的插件,然后加载新版本,从而实现了对插件的热更新)。
而RASP Agent引导包仅保留几个类,主要用于初始化过程,比如启动自定义类加载器、设置插件包的加载路径等。引导包负责找到并初始化插件jar包。这意味着它需要知道插件的位置,并能够触发自定义类加载器去加载这个插件。一旦插件被成功加载,后续的安全逻辑就可以由插件中的代码来执行
工作流程:
首次注入agent的时候,首先调用agent中的方法进行初始化,在初始化过程中会创建一个自定义类加载器(如PluginClassLoader),这个类专门用于加载RASP插件,首次的时候从指定位置加载V1.0的插件,并完成插件的初始化工作
更新插件到V1.1版本,首先确保没有对旧版本插件对象的引用,下一步销毁当前使用的自定义类加载器,接下来创建一个新的自定义类加载器实例,用这个新的类加载器实例来加载新版的插件,再执行必要的初始化逻辑,此时新的插件就开始生效了
背景:java中类加载器并不提供卸载单个类的功能,但是一个类加载器不再被引用并且可以被垃圾回收时,它所加载的所有类也会被一同回收,所以如果想卸载某个类,可以通过丢弃整个类加载器实例,并确保没有对该类加载器或其加载的任何类的强引用,这样垃圾回收器就可以回收这个类加载器及其加载的所有类。(实际上是通过回收这个类加载器间接卸载这个类)
参考文章:美团RASP大规模研发部署实践总结 - 美团技术团队