Java Instrument机制于JDK 1.5版本引入,是一种Java中的字节码增强技术(Bytecode Instrumentation)。可以理解为一种JVM级别的AOP实现方式,可以实现向JVM中一个运行时程序加载一个jar包、并由该jar包对运行时程序进行字节码修改的效果。最初目的为实现JVM监控和类的动态修改。作为插桩技术,其在安全领域的应用包括不限于RASP、IAST等。
IAST(Interactive application security testing)交互式应用安全检测是一种应用安全测试方法,由Gartner公司与2012年提出。基于instrumentation机制,IAST可以与依赖库进行交互,并从内部对运行时应用进行分析,实现对代码漏洞的发现和诊断。(图片来源[1])
Java字节码增强技术
工作原理
Java中的Instrumentation主要依赖于java.lang.instrument
包,实现上有两种类型:
区别在于,静态的方法可以通过-javaagent
参数将要织入的代码以Jar包的方式传递给运行时程序:
但这种方式的缺点在于,该agent jar包必须在运行时程序的命令行处加载运行,即JVM开启时载入。现实情况中不一定可以做到通过命令行传入。因而也有了JDK 1.6引入的动态的方法。
动态的方法下,运行时程序可以随时attach一个给定的agent jar包,具有更灵活的特点。
为了深入了解该机制的工作原理,那就需要先了解JVM中的JVM TI(JVM tool interface)——Java虚拟机工具接口。
JVM TI
JVM TI的前身其实是JVMDI(JVM profiler interface)和JVMPI(JVM debug interface),二者分别用于Java程序的调试和调节性功能,后来分别于JDK 6、JDK 7被弃用[2]。
JVM
TI是一套由虚拟机提供的native
接口,通过这些接口可以实现对运行时Java程序的调试,且可以查看该程序的运行状态、设置回调函数、控制环境变量等,实现优化程序性能的目的。如Eclipse的调试器就是调用JVMTI实现的。
该工具位于Java平台调试架构JPDA(Java platform debugger
architechture)中的最底层,其中Debugee
是被调试者,通过JDWP
于调试者JDI
进行通信。
为了调用JVM TI接口,我们需要传入一个客户端程序——agent,并注册一些我们感兴趣的JVM事件(event),当JVM发生了这些事件时,JVM TI服务端就会通过JDWP通知我们的客户端。
这里也叫agent,但是和我们本文需要关注的、instrument机制的agent有所区别。我理解是包含关系,即instrument机制传入的agent是要比这里提到的更上层的(即jar包格式)。而这里的agent需要使用C/C++进行开发,在windows中为DLL、Linux下为so,并通过图中的方法进行传递,后者为绝对路径:
The JVM TI specification supports the use of multiple simultaneous JVM TI agents. Each agent has its own JVM TI environment. That is, the JVM TI state is separate for each agent - changes to one environment do not affect the others[3].
当然,也支持传入多个agent,每个agent都有自己独立的JVM TI操作环境,互相独立不影响。但最终影响都会反应到JVM上,类似线程并发操作变量问题的感觉。
介绍就到这里,回到我们关心的instrument机制本身。
这里我们需要关注的,就是JVM TI提供的设置回调函数的这个功能,JVM TI提供了大量的JVM事件供我们选择:
而这里我们需要了解两个事件:JVMTI_EVENT_VM_INIT
和JVMTI_EVENT_CLASS_FILE_LOAD_HOOK
。
JVMTI_EVENT_VM_INIT
标志着JVM初始化的完成,若JVM初始化失败则不会触发该事件。当该事件发生后,agent就可以开始调用任意的JVM TI接口了。JVMTI_EVENT_CLASS_FILE_LOAD_HOOK
标志着JVM已经拿到了class文件,但是还没有将它加载到JVM的内存中去。
看到第二个事件的含义,大概也就理解了Instrument机制的工作机制了,文档中也写的很清晰:
The agent can instrument the existing class file data sent by the VM to include profiling/debugging hooks. See the description of bytecode instrumentation for usage information[3].
即agent可以关注该事件,并对JVM要加载的class文件进行修改——即字节码增强技术。
流程
了解了Instrument机制的底层原理、JVM TI的两个事件后,我们来看一下agent加载的不同阶段,以及整体的一个增强的过程。
agent生命周期
首先是start-Up阶段,根据agent是在JVM初始化时加载还是运行施加在,会执行不同的函数。
对于前者,JVM首先会调用Agent_OnLoad()
方法;而后者会调用Agent_OnAttach()
方法。最终都会调用Agent_OnUnload()
方法结束生命周期。
Java Instrument流程
这里以最常见的通过-javaagent
加载agent的方式为例,也即静态方式加载。找到了其他大佬博客的一张图,非常清晰[4]:
- 通过
-javaagent
参数传入agent,JVM初始化时调用Agent_OnLoad()
函数。 - 在
Agent_OnLoad()
函数中,通过JVM TI接口,注册VMInit
和ClassFileLoadHook
事件的回调函数。 - JVM启动初始化结束,触发
VMInit
事件的回调函数,agent开始执行自己的逻辑。 - 在agent主函数
premain
中注册自己的字节码修改类ClassFIleTransformer
实例。 - 运行时程序执行main函数。
- JVM加载*.class文件,触发
ClassFileLoadHook
事件的回调函数。 - agent执行
transform
函数,对准备加载的*.class字节码文件进行修改,并返回给JVM。
一个Demo
下面写一个Demo,学习一下基本的agent的代码编写方法并通过-javaagent:
进行传递,也即静态的方法。另外的动态agent方法的编写方法可以参考[5]。
首先我们定义一个待被织入的对象类:
1 | package target; |
该程序正常输出结果如下:
下面编写agent,常见的字节码操作工具除了java原生的,还有ASM
、Byte Buddy
等。ASM由于市面上采用的最多,这里以ASM为例,实现对上述代码输出的改变。
我们需要借助java.lang.instrument.ClassFileTransformer
类,通过实现该接口以编写agent,并通过transform
方法来对加载的类进行增强。
1 | package instrument; |
然后在resources下写MANIFEST.MF
,并将上述代码导出为jar包:
1 | Manifest-Version: 1.0 |
增加VM Option:
运行结果:
从结果可以看出,agent的方法的执行事件均在目标类的方法执行之前,这说明:agent代码的织入是在目标程序运行之前的,且当目标程序开始执行函数时、才会触发agent的织入。这也对应上了在学习Java Instrument流程时的特点。
IAST
要解决的问题
IAST于2012年被提出,作为一种AST(Application Security Testing)方案,在他之前已经有了SAST、DAST了。那么IAST的必要性在哪里?要解决什么问题?
我个人理解的总结:
技术上,目前市面上SAST的误报率较高、DAST需要发送一些脏数据,且无法了解内部执行细节从而对漏洞真正成因的发现带来一定困难。而IAST无需发送脏数据来触发漏洞,而是通过Instrument机制对调用链进行追踪,从而可以对漏洞的成因得到更深层次的了解。
场景上,随着DevOps的发展,为了追求更高效率的交付,DevSecOps的观念被提出、“安全左移”的思想也在不断推进。作为可以与流水线整合的安全测试方案,IAST是一个非常不错的选择。
但是虽然有上述优点,但该基于由于依赖Instrument机制,而不同语言如Python等该机制实现都不一样,因而有100种语言就得设计100种IAST方案,而不像DAST这种更高层面的,与语言无关。抱有兴趣的人可以阅读该材料[6]。
市面上已经有了很多开源产品如火线安全的洞态IAST[7]、百度的OpenRASP-IAST[8][]。商用的如Contrast Security的Contrast Assess[9]
IAST分类及流程
IAST从流量的性质上,通常认为,IAST可以分为主动式和被动式[10][11]:
被动式:即只需要一个agent即可,使用正常的业务流量进行测试。
Passive IAST is a security tool that requires a single agent to be run alongside an application[11].
主动式:不仅需要一个agent,还需要一个传统的DAST工具产生攻击流量,由IAST提供攻击的具体细节。
This approach requires two main components — a DAST tool and a sensor that attaches to running applications[11].