Java安全之JVMTI

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_INITJVM完成初始化操作时
JVMTI_EVENT_VM_DEATHJVM终止时
JVMTI_EVENT_THREAD_START新线程在执行其初始方法前
JVMTI_EVENT_THREAD_END线程在执行完其初始方法后
JVMTI_EVENT_CLASS_FILE_LOAD_HOOKClass文件加载时
JVMTI_EVENT_CLASS_LOAD某个类首次被载入时
JVMTI_EVENT_CLASS_PREPARE某个类准备完成时
JVMTI_EVENT_VM_STARTJVM启动时
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_EXITJava方法(包括本地方法)返回时
JVMTI_EVENT_NATIVE_METHOD_BINDJVM绑定了一个本地方法到具体实现后
JVMTI_EVENT_COMPILED_METHOD_LOADJVM编译某个方法,并将编译内容载入到内存时
JVMTI_EVENT_COMPILED_METHOD_UNLOAD编译后的方法从内存中卸载时
JVMTI_EVENT_DYNAMIC_CODE_GENERATED当JVM动态生成组件时
JVMTI_EVENT_DATA_DUMP_REQUESTJVM接收到转储数据的请求时
JVMTI_EVENT_MONITOR_WAIT线程要等待某个对象时
JVMTI_EVENT_MONITOR_WAITED线程结束等待某个监视器时
JVMTI_EVENT_MONITOR_CONTENDED_ENTER线程试图获取一个已经被其他线程持有的Java监视器时
JVMTI_EVENT_MONITOR_CONTENDED_ENTERED线程经过等待,终于进入到Java监视器后
JVMTI_EVENT_RESOURCE_EXHAUSTEDJVM的资源被耗尽时
JVMTI_EVENT_GARBAGE_COLLECTION_STARTGC开始时
JVMTI_EVENT_GARBAGE_COLLECTION_FINISHGC结束时
JVMTI_EVENT_OBJECT_FREE对象被GC回收后
JVMTI_EVENT_VM_OBJECT_ALLOCGC分配对象内存时

应用场景

调试工具: 设置断点、单步执行代码。

性能监控工具: 查看内存使用、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**,新的类数据缓冲区

核心思路

  1. 保护class文件将Class文件中方法的字节码进行加密。
  2. 运行时内存解密通过编译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;
}
滚动至顶部
售前客服
周末值班
电话

电话

13910187371