JVMTI

即 Java Virtual Machine Tool Interface(JVM工具接口), 是 JVM 的一个 native 接口: 一个用C或C++编写的库,在 JVM 的初始化过程中被加载。该库通过调用 JVMTI 和 JNI(Java Native Interface)来访问 JVM 状态,并可以使用事件处理函数注册接收 JVMTI 事件,当这些事件发生时,JVM会调用该函数。

利用 JVMTI 保护 Jar 包

截取 agentlib 的关键代码如下

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
{

	jvmtiEnv *jvmti;
	//Create the JVM TI environment(jvmti)
	jint ret = vm->GetEnv((void **)&jvmti, JVMTI_VERSION);
	if (JNI_OK != ret)
	{
		printf("ERROR: Unable to access JVMTI!\n");
		return ret;
	}
	jvmtiCapabilities capabilities;
	(void)memset(&capabilities, 0, sizeof(capabilities));

	capabilities.can_generate_all_class_hook_events = 1;
	capabilities.can_tag_objects = 1;
	capabilities.can_generate_object_free_events = 1;
	capabilities.can_get_source_file_name = 1;
	capabilities.can_get_line_numbers = 1;
	capabilities.can_generate_vm_object_alloc_events = 1;

	jvmtiError error = jvmti->AddCapabilities(&capabilities);
	if (JVMTI_ERROR_NONE != error)
	{
		printf("ERROR: Unable to AddCapabilities JVMTI!\n");
		return error;
	}

	jvmtiEventCallbacks callbacks;
	(void)memset(&callbacks, 0, sizeof(callbacks));

	callbacks.ClassFileLoadHook = &ClassDecryptHook;
	error = jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
	if (JVMTI_ERROR_NONE != error) {
		printf("ERROR: Unable to SetEventCallbacks JVMTI!\n");
		return error;
	}

	error = jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL);
	if (JVMTI_ERROR_NONE != error) {
		printf("ERROR: Unable to SetEventNotificationMode JVMTI!\n");
		return error;
	}

	return JNI_OK;
}

总结一下, 就是在 Agent_OnLoad 函数中:

  1. 通过 AddCapabilities 注册所需的功能
  2. 使用 SetEventCallbacks 指定 ClassFileLoadHook 事件回调函数
  3. 调用 SetEventNotificationMode 开启事件通知

ClassFileLoadHook

事件结构
void JNICALL
ClassFileLoadHook(jvmtiEnv *jvmti_env,
            JNIEnv* jni_env,
            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)

每当 JVM 获得类文件数据, 但在构建该类的内存表示之前,会发送这个事件。其中参数 class_data 即 JVM 读入的加密过的类文件, 在此回调完成解密后, 将解密的类文件传回 new_class_data

类加载过程

graph LR A["被加密的类"] B[Agent Lib] C["JVM"] A --> C C -- ClassFileLoadHook --> B B -- 解密 --> C

简而言之,注册一个ClassFileLoadHook,传入被加密的 class 文件,并对其解密。

解密

此类加密最明显的特征是启动时会添加参数-agentlib:xxx.dll,而xxx.dll就是在运行时解密 class 的核心所在。

根据上述流程,解密的话直接调用 ClassFileLoadHook 中的解密函数即可

具体实现

由于我们只需要对类进行解密, 不必依赖于真正的JVM环境, 这里我选择手动创建一个JavaVM对象

JavaVM jvm = JavaVM();

并将其中的GetEnv指向自己定义的函数myGetEnv

jvm.functions = (JNIInvokeInterface_*)malloc(sizeof(JNIInvokeInterface_));
memset(jvm.functions, 0, sizeof(JNIInvokeInterface_));
jvm.functions->GetEnv = myGetEnv;

myGetEnv中, 对需要用的函数进行模拟

jint JNICALL myGetEnv(JavaVM* vm, void** penv, jint version) {
    cout << "Invoke getEnv, version:" << version << endl;;
    jvmti.functions = (jvmtiInterface_1_*)malloc(sizeof(jvmtiInterface_1_));
    memset((jvmtiInterface_1_*)jvmti.functions, 0, sizeof(jvmtiInterface_1_));
    jvmti.functions->AddCapabilities = myAddCapabilities;
    jvmti.functions->SetEventCallbacks = mySetEventCallbacks;
    jvmti.functions->SetEventNotificationMode = mySetEventNotificationMode;
    jvmti.functions->Allocate = myAllocate;

    *penv = &jvmti;
    return JNI_OK;
}

其中在mySetEventCallbacks中即可获取解密函数的地址, 并进行解密

jvmtiError JNICALL mySetEventCallbacks(jvmtiEnv* env,
    const jvmtiEventCallbacks* callbacks,
    jint size_of_callbacks) {
    cout << "Invoke SetEventCallbacks:" << callbacks << ", Get Decrypt Func Addr:" << callbacks->ClassFileLoadHook << endl;
    // ...
    return JVMTI_ERROR_NONE;
}

加载 dll 并手动调用Agent_OnLoad

HMODULE hdll = LoadLibraryExA("agent.dll", NULL, NULL);
// ...
pAgent_OnLoad fOnload = getOnloadAddr(hdll);
// ...
if (fOnload(&jvm, nullptr, nullptr) == JNI_OK)
    cout << "Succes to exec onload Func" << endl;
else
    cout << "An error occure when invoke onload Func" << endl;

解密工具(含源码)

Usage
> dejvmti.exe -h
Usage:
  -h [ --help ]                     produce help message
  -l [ --lib ] arg                  set agentlib path
  -i [ --in ] arg                   set input path
  -o [ --out ] arg (=decrypted.jar) set output path

> dejvmti -l jagent.dll -i test.jar

本地下载