前言
学习Java Agent内存马,需要看一看底层Native代码。
源码下载
我们安装的Oracle JDK只提供java和javax的源代码,所以我们还需要从OpenJDK下载源代码,下载地址,点击左边的zip即可下载。
Java InstrumentationImpl类
根据参考文章,加载代理最后就是为了产生Instrumentation对象,即该接口的实现InstrumentationImpl对象。InstrumentationImpl类中存在一个redefineClasses函数:
public void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException {
if (!isRedefineClassesSupported()) {
throw new UnsupportedOperationException("redefineClasses is not supported in this environment");
}
if (definitions == null) {
throw new NullPointerException("null passed as 'definitions' in redefineClasses");
}
for (int i = 0; i < definitions.length; ++i) {
if (definitions[i] == null) {
throw new NullPointerException("element of 'definitions' is null in redefineClasses");
}
}
if (definitions.length == 0) {
return; // short-circuit if there are no changes requested
}
redefineClasses0(mNativeAgent, definitions);
}
ClassDefinition是一个包括类名和类字节码的对象,也就是说这个函数可以通过一个ClassDefinition重新定义一个类。可以看到最后一行,函数进入了Native部分进行类的重定义,参数为一个long类型的私有属性和这个ClassDefinition对象,在源码中找到这个native函数:
/*
* Class: sun_instrument_InstrumentationImpl
* Method: redefineClasses0
* Signature: ([Ljava/lang/instrument/ClassDefinition;)V
*/
JNIEXPORT void JNICALL Java_sun_instrument_InstrumentationImpl_redefineClasses0
(JNIEnv * jnienv, jobject implThis, jlong agent, jobjectArray classDefinitions) {
redefineClasses(jnienv, (JPLISAgent*)(intptr_t)agent, classDefinitions);
}
这个long类型的其实是个native的对象指针,也就是说如果我们能找到这个指针,可能就可以通过不落地Agent文件的方式修改类代码,
Native
看到redefineClasses函数中第一句就是:
jvmtiEnv* jvmtienv = jvmti(agent);
jvmti是一个宏:
#define jvmti(a) a->mNormalEnvironment.mJVMTIEnv
作用是从agent结构的mNormalEnvironment-mJVMTIEnv中取出一个jvmtiEnv指针,然后这个agent指针就没有用了,后续就是通过这个jvmtienv指针重定义类:
jvmtiError errorCode = JVMTI_ERROR_NONE;
errorCode = (*jvmtienv)->RedefineClasses(jvmtienv, numDefs, classDefs);
所以问题就从如何获取agent指针变成了如何获取jvmtiEnv指针,其中也同样定义了一个RedefineClasses函数(C语言下):
/* 87 : Redefine Classes */
jvmtiError (JNICALL *RedefineClasses) (jvmtiEnv* env,
jint class_count,
const jvmtiClassDefinition* class_definitions);
常规手段好像是通过Native开发的dll在Native层面调用导出的JNI_GetCreatedJavaVMs函数获取JavaVM对象,再通过其GetEnv函数找到,然而这个手法需要落地一个dll文件,同样不开心。
根据参考文章,拿到这个jvmtienv指针的方式各平台各有不同。
Windows
WindowsVirtualMachine的Native函数enqueue可以直接注入并运行shellcode,使用System.loadLibrary加载attach库后即可使用。
ShellCode不会写。
Linux
解析libjvm.so文件计算函数偏移,再读取/proc/self/maps找到库加载基址,同样是编写shellcode,写入/proc/self/mem并执行。
还是ShellCode不会写。
拿到jvmtienv指针后,使用Unsafe申请内存并伪造agent对象,最后调用Java里面InstrumentationImpl类的redefineClasses函数完成类的重定义。