Java 代码解密:使用 Frida 还原 JVMTI Agent 加密保护的java类
字数 1435 2025-08-22 12:23:19
使用 Frida 还原 JVMTI Agent 加密保护的 Java 类 - 详细教学文档
1. 背景与问题描述
在 Java 应用程序安全分析中,经常会遇到使用 JVMTI Agent 对类文件进行加密保护的情况。这种保护方式通过在类加载时动态解密字节码,使得直接反编译 JAR 文件时无法获取原始类内容。
典型特征:
- 启动参数中包含
-agentpath指向.so文件(Linux)或.dll文件(Windows) - 反编译工具返回空白结果或异常
- 使用 JVMTI(JVM Tool Interface)技术而非普通 Java Agent
2. JVMTI Agent 工作原理
JVMTI Agent 通过注册 ClassFileLoadHook 回调函数拦截类加载过程:
void JNICALL ClassFileLoadHook(jvmtiEnv *jvmti_env, JNIEnv *jni_env,
jclass class_being_loaded, 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) {
// 解密逻辑在此执行
*new_class_data_len = class_data_len;
*new_class_data = const_cast<unsigned char *>(class_data);
}
关键点:
- Agent 在
Agent_OnLoad中注册回调 - JVM 加载类时触发回调
- Agent 可以修改
new_class_data返回解密后的字节码
3. 技术分析路线
3.1 逆向分析难点
- 直接逆向
.so文件可能遇到反编译困难 - 解密函数定位困难
- 需要理解 JVM 内部工作机制
3.2 替代方案
通过分析 JVM 源码(OpenJDK)找到关键函数:
post_class_file_load_hook(在jvmtiExport.cpp中)- 这是 JVM 调用
ClassFileLoadHook的实际入口
4. Frida 动态分析方案
4.1 环境准备
-
目标机器:
- 上传
frida-server可执行文件 - 启动服务:
frida-server -l 0.0.0.0:27042
- 上传
-
分析机器:
- 安装 Frida 工具包:
pip install frida-tools - 验证连接:
frida-ps -H <target_ip>
- 安装 Frida 工具包:
4.2 关键 Hook 点
4.2.1 模块加载检测
由于 libjvm.so 可能延迟加载,需要 Hook dlopen:
const moduleName = "libjvm.so";
var dlopen = Module.findExportByName(null, "dlopen");
Interceptor.attach(dlopen, {
onEnter: function(args) {
var soName = args[0].readCString();
if(soName.indexOf(moduleName) != -1) {
this.hook = true;
}
},
onLeave: function(retval) {
if(this.hook) {
hookFunction();
}
}
});
4.2.2 Hook post_class_file_load_hook
函数签名:
static void post_class_file_load_hook(Symbol* h_name, Handle class_loader,
Handle h_protection_domain, unsigned char **data_ptr,
unsigned char **end_ptr, JvmtiCachedClassFileData **cache_ptr)
关键参数:
h_name: 类名(Symbol 对象)data_ptr/end_ptr: 类字节码的起始和结束指针
4.2.3 Symbol 对象解析
Symbol 内存结构:
_refcount (2 bytes) | _length (2 bytes) |
_identity_hash (4 bytes) | _body[1] (variable length)
读取类名实现:
function getSymbolString(symbolAddr) {
const length = ptr(symbolAddr).add(2).readU16();
const symbolBodyAddr = ptr(symbolAddr).add(8); // _body 偏移8字节
const byteArray = symbolBodyAddr.readByteArray(length);
return Memory.readUtf8String(byteArray, length);
}
4.2.4 完整 Hook 实现
function hookFunction() {
const baseAddress = Module.findBaseAddress(moduleName);
const targetAddress = baseAddress.add(functionOffset); // 函数偏移
Interceptor.attach(targetAddress, {
onEnter: function(args) {
this.ptrAddress = args[3]; // data_ptr 的地址
this.endPtrAddress = args[4]; // end_ptr 的地址
this.name = getSymbolString(args[0]);
},
onLeave: function(retval) {
const ptr = this.ptrAddress.readPointer();
const endPtr = this.endPtrAddress.readPointer();
const length = endPtr.sub(ptr).toInt32();
if (length > 0) {
const data = ptr.readByteArray(length);
const dumpPath = "/tmp/"+this.name.replaceAll("/", ".")+".class";
const file = new File(dumpPath, "wb");
file.write(data);
file.close();
}
}
});
}
5. 执行与结果
启动命令:
frida -H <target_ip> -l hook.js -f /bin/java -- -agentpath:./agent.so -jar app.jar
结果:
- 解密后的类文件将保存到
/tmp/目录 - 文件名格式为完整类名(包路径转换为点分隔)
6. 进阶技巧
6.1 触发所有类加载
为了完整解密所有类,可以:
- 编写特殊 ClassLoader 强制加载所有类
- 使用反射扫描 JAR 文件并尝试加载
6.2 注意事项
- 确保使用与目标环境完全一致的 JDK 版本
- 函数偏移量需要通过 IDA 分析目标
libjvm.so确定 - 生产环境可能需要静态编译 Frida-server
7. 总结
本方法通过 Frida Hook JVM 内部函数,在加密 Agent 解密后获取原始类字节码,具有以下优势:
- 不依赖逆向分析加密逻辑
- 可获取完整解密后的类文件
- 即使类加载失败也能获取字节码
- 适用于各种 JVMTI 加密方案
通过这种动态分析方法,可以有效绕过基于 JVMTI 的 Java 类保护机制,为安全分析和研究提供有力工具。