f10@t's blog

Java Instrument机制及管窥IAST

字数统计: 2.6k阅读时长: 10 min
2023/07/08
  • 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包,实现上有两种类型:

  • 静态(JDK 1.5)——Load-Time Instrumentation[3]
  • 动态(JDK 1.6)——Dynamic Instrumentation[3]

区别在于,静态方法可以通过-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)中的最底层,其中Debuggee被调试者,通过JDWP调试者JDI进行通信。

  • JVM TI is a two-way interface. A client of JVM TI, hereafter called an agent, can be notified of interesting occurrences through events. [3]
  • Agents can be informed of many events that occur in application programs. [3]

为了调用JVM TI接口,我们需要传入一个客户端程序——agent,并注册一些我们感兴趣的JVM事件(event),当JVM发生了这些事件时,JVM TI服务端就会通过JDWP通知我们的客户端。

这里虽然也叫agent,但是和我们本文需要关注的、instrument机制的agent有所区别。

我理解二者是包含关系,即instrument机制传入的agent是要比这里提到的更上层的(即jar包格式)。而这里的agent需要使用C/C++进行开发,在windows中为DLL、Linux下为so,并需要通过下图中的-agentpath参数进行传递,且为绝对路径:

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_INITJVMTI_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]

  1. 通过-javaagent参数传入agent,JVM初始化时调用Agent_OnLoad()函数。
  2. Agent_OnLoad()函数中,通过JVM TI接口,注册VMInitClassFileLoadHook事件的回调函数
  3. JVM启动初始化结束,触发VMInit事件的回调函数,agent开始执行自己的逻辑
  4. 在agent主函数premain中注册自己的字节码修改类ClassFIleTransformer实例。
  5. 运行时程序执行main函数。
  6. JVM加载*.class文件,触发ClassFileLoadHook事件的回调函数。
  7. agent执行transform函数,对准备加载的*.class字节码文件进行修改,并返回给JVM。

一个Demo

下面写一个Demo,学习一下基本的agent的代码编写方法并通过-javaagent:进行传递,也即静态的方法。另外的动态agent方法的编写方法可以参考[5]

首先我们定义一个待被织入的对象类:

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
package target;

/**
* @author lzwgiter
* @email float311@163.com
* @since 2023/7/8
*/
public class TargetClass {

public static void target() {
System.out.println(System.currentTimeMillis());
System.out.println("可以我好了");
}

public static void foo() {
System.out.println(System.currentTimeMillis());
System.out.println("得得得得得!");
}

public static void main(String[] args) {
foo();
target();
}
}

该程序正常输出结果如下:

下面编写agent,常见的字节码操作工具除了java原生的,还有ASMByte Buddy等。ASM由于市面上采用的最多,这里以ASM为例,实现对上述代码输出的改变。

我们需要借助java.lang.instrument.ClassFileTransformer类,通过实现该接口以编写agent,并通过transform方法来对加载的类进行增强。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package instrument;

import jdk.internal.org.objectweb.asm.*;
import jdk.internal.org.objectweb.asm.commons.AdviceAdapter;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

/**
* @author lzwgiter
* @email float311@163.com
* @since 2023/7/8
*/
public class InstrumentMain {
public static class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, classWriter) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if ("target".equals(name)) {
// 对目标函数进行增强
return new AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, descriptor) {
@Override
protected void onMethodEnter() {
// 进入方法时
System.out.println(System.currentTimeMillis());
System.out.println("@ agent @ 嗨嗨嗨!");
}

@Override
protected void onMethodExit(int i) {
// 退出方法时
System.out.println(System.currentTimeMillis());
System.out.println("@ agent @ 泰裤辣!");
}
};
} else {
return methodVisitor;
}
}
};
classReader.accept(classVisitor, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
// 返回增强后的字节数组,即字节码
return classWriter.toByteArray();
}
}

public static void premain(String agentArgs, Instrumentation instrumentation) {
// agent的主函数
System.out.println("@ agent @ premain方法已执行");
instrumentation.addTransformer(new MyClassFileTransformer(), true);
}
}

然后在resources下写MANIFEST.MF,并将上述代码导出为jar包:

1
2
3
4
5
Manifest-Version: 1.0
Premain-Class: instrument.InstrumentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true
(注意这里是有一个空行的)

增加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].

参考

CATALOG
  1. 1. Java字节码增强技术
    1. 1.1. 工作原理
      1. 1.1.1. JVM TI
      2. 1.1.2. 流程
        1. 1.1.2.1. agent生命周期
        2. 1.1.2.2. Java Instrument流程
    2. 1.2. 一个Demo
  2. 2. IAST
    1. 2.1. 要解决的问题
    2. 2.2. IAST分类及流程
  3. 3. 参考