JVMTI是什么
JVMTI(Java Virtual Machine Tool Interface)是JVM提供的一个编程接口,专供开发和监控工具使用。简单来说,它是Java虚拟机(JVM)提供的一套标准接口,开发者可以通过它来监控、调试和分析运行在JVM上的程序。该接口提供的功能覆盖多个方面,包括对类(Class)、线程(Thread)、堆(Heap)内存、监视器(Monitor)、栈帧(Frame)等的查询和操作。 这使得开发者能够在无需修改应用程序代码的情况下,监控和分析Java进程的运行状态。
JVMTI能做什么
JVMTI事件
jvm提供一组事件,用来通知本机动态库文件(.dll/.so)当前 JVM 处于什么状态,并且可以通过jvmti 提供SetEventCallbacks
函数设置对应事件的回调函数,通过SetEventNotificationMode
函数启动或禁用某个事件的通知。
事件简表
事件常量 | 触发时机 |
---|---|
JVMTI_EVENT_VM_INIT | JVM完成初始化操作时 |
JVMTI_EVENT_VM_DEATH | JVM终止时 |
JVMTI_EVENT_THREAD_START | 新线程在执行其初始方法前 |
JVMTI_EVENT_THREAD_END | 线程在执行完其初始方法后 |
JVMTI_EVENT_CLASS_FILE_LOAD_HOOK | Class文件加载时 |
JVMTI_EVENT_CLASS_LOAD | 某个类首次被载入时 |
JVMTI_EVENT_CLASS_PREPARE | 某个类准备完成时 |
JVMTI_EVENT_VM_START | JVM启动时 |
JVMTI_EVENT_EXCEPTION | 异常抛出时 |
JVMTI_EVENT_EXCEPTION_CATCH | 捕获到异常时 |
JVMTI_EVENT_SINGLE_STEP | 调试步进 |
JVMTI_EVENT_FRAME_POP | 执行退出指令或抛出异常而导致方法返回时 |
JVMTI_EVENT_BREAKPOINT | 线程执行到一个带有断点的位置时 |
JVMTI_EVENT_FIELD_ACCESS | 线程访问了具有观察点的属性时 |
JVMTI_EVENT_FIELD_MODIFICATION | 线程修改具有观察点的属性时 |
JVMTI_EVENT_METHOD_ENTRY | 进入Java方法(包括本地方法)时 |
JVMTI_EVENT_METHOD_EXIT | Java方法(包括本地方法)返回时 |
JVMTI_EVENT_NATIVE_METHOD_BIND | JVM绑定了一个本地方法到具体实现后 |
JVMTI_EVENT_COMPILED_METHOD_LOAD | JVM编译某个方法,并将编译内容载入到内存时 |
JVMTI_EVENT_COMPILED_METHOD_UNLOAD | 编译后的方法从内存中卸载时 |
JVMTI_EVENT_DYNAMIC_CODE_GENERATED | 当JVM动态生成组件时 |
JVMTI_EVENT_DATA_DUMP_REQUEST | JVM接收到转储数据的请求时 |
JVMTI_EVENT_MONITOR_WAIT | 线程要等待某个对象时 |
JVMTI_EVENT_MONITOR_WAITED | 线程结束等待某个监视器时 |
JVMTI_EVENT_MONITOR_CONTENDED_ENTER | 线程试图获取一个已经被其他线程持有的Java监视器时 |
JVMTI_EVENT_MONITOR_CONTENDED_ENTERED | 线程经过等待,终于进入到Java监视器后 |
JVMTI_EVENT_RESOURCE_EXHAUSTED | JVM的资源被耗尽时 |
JVMTI_EVENT_GARBAGE_COLLECTION_START | GC开始时 |
JVMTI_EVENT_GARBAGE_COLLECTION_FINISH | GC结束时 |
JVMTI_EVENT_OBJECT_FREE | 对象被GC回收后 |
JVMTI_EVENT_VM_OBJECT_ALLOC | GC分配对象内存时 |
应用场景
调试工具: 设置断点、单步执行代码。
性能监控工具: 查看内存使用、CPU消耗,找出程序慢的原因。
线程分析工具: 检查线程状态,发现死锁等问题。
代码覆盖工具: 测试时看哪些代码行被执行了。
字节码增强工具: 运行时修改class文件中JVM字节码指令。
JVMTI加载方式
启动加载:在Java进程启动的时候通过-agentpath:<pathname>=<options>
的方式启动,pathname是对应的jvmti接口实现的本机动态库文件(.dll/.so)的绝对路径,后面可以追加jvmti程序需要的参数。
附加加载:使用代码附加加载jvmti实现的本机动态库文件(.dll/.so)到 JVM 进程中。
JVMTI初体验
目标
通过附加jvm,遍历当前jvm已加载的类签名。
代码实现
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(jvmtidemo)
set(PRODUCT_NAME ${PROJECT_NAME})
set(CMAKE_CXX_STANDARD 17)
set(JAVA_HOME $ENV{JAVA_HOME})
include_directories(${JAVA_HOME}/include)
include_directories(${JAVA_HOME}/include/win32)
link_directories(${JAVA_HOME}/lib)
message(STATUS JAVA_HOME:${JAVA_HOME})
aux_source_directory(src SRC_LIST)
add_library(${PRODUCT_NAME} SHARED ${SRC_LIST})
jvmtidemo.cpp
#include <jvmti.h>
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
JNIEXPORT jint printLoadedClasses(JavaVM* vm)
{
jvmtiEnv* jvmti;
jint result = vm->GetEnv((void**)&jvmti, JVMTI_VERSION_1_2);
if (result != JNI_OK)
{
std::cout << "Unable to access jvm env" << std::endl;
return result;
}
jclass* classes;
jint count;
result = jvmti->GetLoadedClasses(&count, &classes);
if (result != JNI_OK)
{
std::cout << "JVMTI GetLoadedClasses failed" << std::endl;
return result;
}
for (int i = 0; i < count; i++)
{
char* sig;
char* genericSig;
jvmti->GetClassSignature(classes[i], &sig, &genericSig);
std::cout << "class signature = " << sig << std::endl;
}
return 0;
}
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM* vm, char* options, void* reserved)
{
std::cout << "Agent Onload" << std::endl;
return JNI_OK;
}
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved)
{
std::cout << "Agent OnAttach" << std::endl;
printLoadedClasses(vm);
return JNI_OK;
}
JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM* vm)
{
std::cout << "Agent OnUnload" << std::endl;
}
AgentAttacher.java
import com.sun.tools.attach.VirtualMachine;
public class AgentAttacher {
public static void main(String[] args) {
if(args.length != 2) {
System.out.println("Invalid Argument");
return;
}
String pid = args[0];
String agentPath = args[1];
attach(pid, agentPath, "");
}
public static void attach(String pid, String agentPath, String agentArgs) {
try {
VirtualMachine virtualMachine = VirtualMachine.attach(pid);
virtualMachine.loadAgentPath(agentPath, agentArgs);
} catch (Exception e) {
e.printStackTrace();
}
}
}
实验
编译
- 通过CMakeLists.txt和jvmtidemo.cpp编译出来jvmtidemo.dll代理库。
- 使用javac编译AgentAttacher.java。
加载
通过java AgentAttacher <pid> jvmtidemo.dll
命令附加加载代理库到指定jvm进程中, 即可打印出已加载的类签名。
基于字节码增强的Class文件保护方案
ClassFileLoadHook回调声明
void JNICALL ClassFileLoadHook(
jvmtiEnv* jvmti,
JNIEnv* jni,
jclass class_being_redefined,
jobject loader,
const char* name,
jobject protection_domain,
jint class_data_len,
const unsigned char* class_data,
jint* new_class_data_len,
unsigned char** new_class_data
);
参数
- jni_env: 类型为JNIEnv *,处理事件线程的JNI执行环境
- class_being_redefined: 类型为jclass,重定义或重转换的类,若是新载入的类,则为NULL
- loader: 类型为jobject,类载入器,若为NULL,则为启动类载入器
- name: 类型为const char*,目标类在JVM内部的限定名,例如java/util/List,使用自定义UTF-8编码
- protection_domain: 类型为jobject,载入类的保护域
- class_data_len: 类型为jint,当前类数据缓冲区的长度
- class_data: 类型为const unsigned char*,当前类数据缓冲区
- new_class_data_len: 类型为jint*,新的类数据缓冲区的长度
- new_class_data: 类型为unsigned char**,新的类数据缓冲区
核心思路
- 保护class文件将Class文件中方法的字节码进行加密。
- 运行时内存解密通过编译JVMTI Agent动态库,注册 JVMTI_EVENT_CLASS_FILE_LOAD_HOOK 事件回调。在ClassFileLoadHook函数中对已加密的方法的字节进行解密。
安全风险
Agent事件回调调用顺序
Agent解密动态库注册的JVMTI_EVENT_CLASS_FILE_LOAD_HOOK的事件回调必须要是最后一个调用,不然后面的事件回调传递的参数就是解密后的class文件,造成安全隐患。需要注意的是JVM 规范并没有明确指定多个代理之间的调用顺序,这通常取决于 JVM 实现和代理加载的顺序, 不过一般都是按注册顺序来调用的。
比较推荐的是Virbox Protector对于该方案的细节处理的比较好,具体可以参考深盾科技官网:https://h.virbox.com/vbp/docs/Java_Protector/Java_BCE_User_Guide/
实验
市面上某个该方案的实现并没有处理好Agent事件回调调用顺序。所以我们可以做个简单实验。 使用下面代码编译出来jvmtidemo.dll,执行命令 java -agentpath:jvmtidemo -javaagent:Test-encrypted.jar -jar Test-encrypted.jar
命令,可以在输出目录里看到解密后的class文件。
效果
Class解密前

Class解密后

代码
#include <jvmti.h>
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
void save_class_file(const char* class_name, const jbyte* data, jint length)
{
char file_name[256];
snprintf(file_name, sizeof(file_name), "%s.class", class_name);
std::string filename = file_name;
std::replace(filename.begin(), filename.end(), '/', '_');
std::string path = fs::path("./dump").append(filename).string();
FILE* fp = fopen(path.c_str(), "wb");
if (fp) {
fwrite(data, 1, length, fp);
fclose(fp);
}
else
{
std::printf("failed to save class: %s\n", class_name);
}
}
void JNICALL ClassFileLoadHook(
jvmtiEnv* jvmti,
JNIEnv* jni,
jclass class_being_redefined,
jobject loader,
const char* name,
jobject protection_domain,
jint class_data_len,
const unsigned char* class_data,
jint* new_class_data_len,
unsigned char** new_class_data
) {
if (name && strstr(name, "Main"))
{
save_class_file(name, (jbyte*)class_data, class_data_len);
}
}
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM* vm, char* options, void* reserved)
{
std::cout << "Agent Onload" << std::endl;
jvmtiEnv* jvmti;
jvmtiCapabilities capabilities;
jvmtiEventCallbacks callbacks;
// 获取 JVMTI 环境
if (vm->GetEnv((void**)&jvmti, JVMTI_VERSION) != JNI_OK) {
return JNI_ERR;
}
// 启用类加载事件
memset(&capabilities, 0, sizeof(capabilities));
capabilities.can_generate_all_class_hook_events = 1;
capabilities.can_retransform_any_class = 1;
capabilities.can_retransform_classes = 1;
jvmti->AddCapabilities(&capabilities);
// 设置回调函数
memset(&callbacks, 0, sizeof(callbacks));
callbacks.ClassFileLoadHook = &ClassFileLoadHook;
jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
// 启用事件通知
jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL);
return JNI_OK;
}
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved)
{
std::cout << "Agent OnAttach" << std::endl;
return JNI_OK;
}
JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM* vm)
{
std::cout << "Agent OnUnload" << std::endl;
}