内存马是国内目前比较流行的web层权限维持方式,研究文章也特别多。本人阅读了rebeyond师傅的Java 内存攻击技术漫谈后,尝试利用其中的技术开发用于内存马检测的工具。
检测
首先,内存马分为两类,一类是利用web中间件组件或框架的特性在web执行流程中嵌入恶意代码来执行命令,例如tomcat的filter,servlet,springmvc的controller等,这类内存马在检测时也可以直接检测相应的组件。
另一类就是java-agent型的内存马,通过上传jar包,attach web应用,调用instrument,使用redefine或者retransform直接修改其关键类的代码,如冰蝎修改的就是 javax.servlet.http.HttpServlet类,在一般的web访问流程中都会调用该类,这类内存马检测时同样也需要java-agent来进行检测。
内存马防检测
以上为内存马简单的介绍,现在来看一下其进阶的技术,在rebeyond师傅的Java 内存攻击技术漫谈一文中,谈到了如何阻断java-agent的attach过程,这就为使用agent的检测制造了困难。
具体的实现可以看一下
我们来看一下大致的实现
instrument机制实现类agent内存马的注入,但是也可以实现对内存马进行检测。
这里给出的方法就是注入内存马后将instrument机制破坏的,使其无法检测进程的类字节码等。
以下为instrument的工作流程
1.检测工具作为Client,根据指定的PID,向目标JVM发起attach请求;
2.JVM收到请求后,做一些校验(比如上文提到的jdk.attach.allowAttachSelf的校验),校验通过后,会打开一个IPC通道。
3.接下来Client会封装一个名为AttachOperation的C++对象,发送给Server端;
4.Server端会把Client发过来的AttachOperation对象放入一个队列;
5.Server端另外一个线程会从队列中取出AttachOperation对象并解析,然后执行对应的操作,并把执行结果通过IPC通道返回Client。
以下是windows端的防检测
我们来梳理一下loadagent整个流程
现在看来只要将jvmLib导出的两个函数JVM_EnqueueOperation和_JVM_EnqueueOperation@20 NOP掉即可完成instrument流程的破坏。
来看一下rebeyond师傅的处理方法
用JNI,核心代码如下:
unsigned char buf[]="\xc2\x14\x00"; //32,direct return enqueue function
HINSTANCE hModule = LoadLibrary(L"jvm.dll");
//LPVOID dst=GetProcAddress(hModule,"ConnectNamedPipe");
LPVOID dst=GetProcAddress(hModule,"_JVM_EnqueueOperation@20");
DWORD old;
if (VirtualProtectEx(GetCurrentProcess(),dst, 3, PAGE_EXECUTE_READWRITE, &old)){WriteProcessMemory(GetCurrentProcess(), dst, buf, 3, NULL);VirtualProtectEx(GetCurrentProcess(), dst, 3, old, &old);}
/*unsigned char buf[]="\xc3"; //64,direct return enqueue function
HINSTANCE hModule = LoadLibrary(L"jvm.dll");
//LPVOID dst=GetProcAddress(hModule,"ConnectNamedPipe");
LPVOIDdst=GetProcAddress(hModule,"JVM_EnqueueOperation");
//printf("ConnectNamedPipe:%p",dst);DWORD old;
if (VirtualProtectEx(GetCurrentProcess(),dst, 1, PAGE_EXECUTE_READWRITE, &old)){WriteProcessMemory(GetCurrentProcess(), dst, buf, 1, NULL);
VirtualProtectEx(GetCurrentProcess(), dst, 1, old, &old);
}*/
虽然有师傅给出了如何去绕过这一阻断,但是在rebeyond师傅的文章中,只要阻断了instrument流程图中的任意一个环节就行,导致阻断的方法可能多种多样,每一种都需要针对性的方法去绕过。
因此,我思考能否彻底将这里的阻断进行绕过,即不使用外部agent进行attach,也能调用instrument。恰巧rebeyond师傅的文章中提到了如何进行无文件落地的agent型内存马攻击,其中通过自己构造instrument,来达到不需要上传agent包,就能够调用instrument来修改关键类的效果。
NoAgent
如何在服务端构造instrument的具体实现可以看一下
议题解析与复现--《Java内存攻击技术漫谈》(二)无文件落地Agent型内存马
这里讲一下大致原理
首先来看一下java-agent正常情况下的创建流程
-
1. 在客户端和目标JVM建立IPC连接以后,客户端会封装一个用来加载agent.jar的AttachOperation对象,这个对象里面有三个关键数据:actioName、libName和agentPath; 2. 服务端收到AttachOperation后,调用enqueue压入AttachOperation队列等待处理; 3. 服务端处理线程调用dequeue方法取出AttachOperation; 4. 服务端解析AttachOperation,提取步骤1中提到的3个参数,调用actionName为load的对应处理分支,然后加载libinstrument.so(在windows平台为instrument.dll),执行AttachOperation的On_Attach函数(由此可以看到,Java层的instrument机制,底层都是通过Native层的Instrument来封装的); 5. .ibinstrument.so中的On_Attach会解析agentPath中指定的jar文件,该jar中调用了redefineClass的功能; 6. 执行流转到Java层,JVM会实例化一个InstrumentationImpl类,这个类在构造的时候,有个非常重要的参数mNativeAgent:这个参数是long型,其值是一个Native层的指针,指向的是一个C++对象JPLISAgent。 7. InstrumentationImpl实例化之后,再继续调用InstrumentationImpl类的redefineClasses方法,做稍许校验之后继续调用InstrumentationImpl的Native方法redefineClasses0 8. 执行流继续走入Native层
看起来是不是很复杂,其实我们只需要关注server端做了什么
来看一下server端的调用栈,我们在server端的agentmain处下断点,可以发现server端的调用栈是从InstrumentationImpl类开始的,这就是上述的第六步,而之前几步都是client 或者native层的操作。因此在java层,我们可以直接从InstrumentationImpl类入手构造恶意代码。
这样就要先构造InstrumentationImpl类,看一下构造函数,结合之前debug生成的信息,发现var3=true,var4=false,需要构造的只要var1,即mNativeAgent,这个参数是long型,其值是一个Native层的指针,指向的是一个C++对象JPLISAgent。说明我们需要在native层构造合适的C++对象JPLISAgent。
private InstrumentationImpl(long var1, boolean var3, boolean var4) {
this.mNativeAgent = var1;//需要构造这个参数
this.mEnvironmentSupportsRedefineClasses = var3;
this.mEnvironmentSupportsRetransformClassesKnown = false;
this.mEnvironmentSupportsRetransformClasses = false;
this.mEnvironmentSupportsNativeMethodPrefix = var4;
}
要在native层构造参数,我使用了unsafe来实现内存分配
Unsafe unsafe = null;
try { Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); unsafe = (sun.misc.Unsafe) field.get(null);} catch (Exception e) { throw new AssertionError(e);}
接着就是看一下JPLISAgent的结构了
struct _JPLISAgent {
JavaVM * mJVM; /* handle to the JVM */
JPLISEnvironment mNormalEnvironment; /* for every thing but retransform stuff */
JPLISEnvironment mRetransformEnvironment;/* for retransform stuff only */
jobject mInstrumentationImpl; /* handle to the Instrumentation instance */
jmethodID mPremainCaller; /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */
jmethodID mAgentmainCaller; /* method on the InstrumentationImpl for agents loaded via attach mechanism */
jmethodID mTransform; /* method on the InstrumentationImpl that does the class file transform */
jboolean mRedefineAvailable; /* cached answer to "does this agent support redefine" */
jboolean mRedefineAdded; /* indicates if can_redefine_classes capability has been added */
jboolean mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */
jboolean mNativeMethodPrefixAdded; /* indicates if can_set_native_method_prefix capability has been added */
char const * mAgentClassName; /* agent class name */
char const * mOptionsString; /* -javaagent options string */
};
JPLISAgent结构复杂,所以我们从后面的redefineclass入手,看一下哪些参数需要。
void
redefineClasses(JNIEnv * jnienv, JPLISAgent * agent, jobjectArray classDefinitions) {
jvmtiEnv* jvmtienv = jvmti(agent);
jboolean errorOccurred = JNI_FALSE;
jclass classDefClass = NULL;
jmethodID getDefinitionClassMethodID = NULL;
jmethodID getDefinitionClassFileMethodID = NULL;
jvmtiClassDefinition* classDefs = NULL;
jbyteArray* targetFiles = NULL;
jsize numDefs = 0;
...
这里根据用法可以看出jvmti是一个宏或函数,搜索一下可以发现这是个宏
可以确定redefineclass需要mNormalEnvironment参数。
来看一下这个参数的结构。
struct _JPLISEnvironment {
jvmtiEnv * mJVMTIEnv; /* the JVM TI environment */
JPLISAgent * mAgent; /* corresponding agent */
jboolean mIsRetransformer; /* indicates if special environment */
};
可以看到这个结构里存在一个回环指针mAgent,又指向了JPLISAgent对象,另外,还有个最重要的指针mJVMTIEnv,这个指针是指向内存中的JVMTIEnv对象的,这是JVMTI机制的核心对象。另外,经过分析,JPLISAgent对象中还有个mRedefineAvailable成员,必须要设置成true。
这样一来,我们只要想办法获取到mJVMTIEnv就能完成构造。
在《Java内存攻击技术漫谈》文章中,由于讲的是攻击技术,且过程中不能有文件落地,所以获取目标机器的mJVMTIEnv比较复杂,但是我们做得是检测工具,没有那么多限制,直接使用JNI,配合dll就能完成地址的获取。
以下是dll的代码
#include "pch.h"
#include "getAgent.h"
#include"getJPSAgent.h"
#include "jvmti.h"
JNIEXPORT void JNICALL Java_getJPSAgent_caloffset
(JNIEnv*, jobject) {
struct JavaVM_* vm;
jsize count;
typedef jint(JNICALL* GetCreatedJavaVMs)(JavaVM**, jsize, jsize*);
//本来想直接调用GetCreatedJavaVMs函数但是缺少特定头文件,因此只能typedef定义另一个结构相同的函数
GetCreatedJavaVMs jni_GetCreatedJavaVMs;
// ...
jni_GetCreatedJavaVMs = (GetCreatedJavaVMs)GetProcAddress(GetModuleHandle(
TEXT("jvm.dll")), "JNI_GetCreatedJavaVMs");
//由于jvm.dll在java程序开始时就已经加载,因此可以直接获取dll中JNI_GetCreatedJavaVMs的地址
jni_GetCreatedJavaVMs(&vm, 1, &count);//获取jvm对象的地址
struct jvmtiEnv_* _jvmti_env;
HMODULE jvm = GetModuleHandle(L"jvm.dll");//获取jvm基址
vm->functions->GetEnv(vm, (void**)&_jvmti_env, JVMTI_VERSION_1_2);//获取_jvmti_env的地址,即即指向JVMTIEnv指针的指针。
printf(" hModule jvm = 0x%llx\n", jvm);
printf(" struct JavaVM_* vm = 0x%llx\n", vm);
printf(" _jvmti_env = 0x%llx\n", _jvmti_env);
;
}
然后将获取的地址放到相应位置就能完成构造了。
以下是获取instrument对象的代码
public Object genImp(String dlladdress,detect getJPSAgent) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
System.load(dlladdress);
long native_jvmtienv = getJPSAgent.caloffset();
Unsafe unsafe = null;
try { Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (sun.misc.Unsafe) field.get(null);}
catch (Exception e) {
throw new AssertionError(e);}
long JPLISAgent = unsafe.allocateMemory(0x100000);
//unsafe.putLong(jvmtiStackAddr,jvmtiAddress);
unsafe.putLong(native_jvmtienv+8,0x30010100000071eel);
unsafe.putLong(native_jvmtienv+0x168,0x9090909000000200l);//实现redefineClass
System.out.println("long:"+Long.toHexString(native_jvmtienv+0x168));
unsafe.putLong(JPLISAgent,unsafe.getLong(native_jvmtienv) -0x9D6760);
unsafe.putLong(JPLISAgent + 8, native_jvmtienv);//实现retransform,mNormalEnvironment.mJVMTIEnv;
unsafe.putLong(JPLISAgent + 0x10, JPLISAgent);// mNormalEnvironment.mAgent;
unsafe.putLong(JPLISAgent + 0x18, 0x00730065006c0000l);//mNormalEnvironment.mIsRetransformer; 决定是否可以retransform
//make retransform env
unsafe.putLong(JPLISAgent + 0x20, native_jvmtienv);//mRetransformEnvironment.mJVMTIEnv
unsafe.putLong(JPLISAgent + 0x28, JPLISAgent);//mRetransformEnvironment.mAgent
unsafe.putLong(JPLISAgent + 0x30, 0x0038002e00310001l);//mRetransformEnvironment.mIsRetransformer
unsafe.putLong(JPLISAgent + 0x38, 0);//jobject mInstrumentationImpl;
unsafe.putLong(JPLISAgent + 0x40, 0);// jmethodID mPremainCaller;
unsafe.putLong(JPLISAgent + 0x48, 0);//jmethodID mAgentmainCaller;
unsafe.putLong(JPLISAgent + 0x50, 0);//jmethodID mTransform;
unsafe.putLong(JPLISAgent + 0x58, 0x0072007400010001l);
/* jboolean mRedefineAvailable; /* cached answer to "does this agent support redefine"
jboolean mRedefineAdded; /* indicates if can_redefine_classes capability has been added
jboolean mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing"
jboolean mNativeMethodPrefixAdded; /* indicates if can_set_native_method_prefix capability has been added */
unsafe.putLong(JPLISAgent + 0x60, JPLISAgent + 0x68);// char const * mAgentClassName; /* agent class name */
unsafe.putLong(JPLISAgent + 0x68, 0x0041414141414141l);// char const * mOptionsString; /* -javaagent options string */
Class<?> instrument_clazz = Class.forName("sun.instrument.InstrumentationImpl");
Constructor<?> constructor = instrument_clazz.getDeclaredConstructor(long.class, boolean.class, boolean.class);
constructor.setAccessible(true);
Object insn = constructor.newInstance(JPLISAgent, true, false);
return insn;//返回对象
}
以上过程就能实现在server端直接构造Instrument,也就是所谓的NoAgent。
之后其实就是正常的Agent检测内存马思路了,不过可能是由于是自构造的instrument,有些函数调用时会发生报错,比如retransform,因此就没有这么方便去直接还原被agent型内存马修改的类了。因此,此类内存马的删除方式还在构思中。
由此NoAgent内存马检测的思路也就诞生了。
检测程序主要包含五个文件
- NoAgent.jar 用于生成instrument,对agent型内存马进行检测
- NoAgent.dll 用于获取jvm的地址等数据提供给NoAgent.jar
- detect.jsp 对NoAgent.jar进行外部调用,对filter等框架中的组件进行内存马检测,提供用户交互界面
- dumpclass.jar 将内存中的class导出到磁盘,用于后续的反编译代码检测(使用cfr进行反编译)
- sa-jdi.jar 作为dumpclass.jar的必要组件,放在%JAVA_HOME%/lib中
该程序的优点
-
可以绕过 对attach的阻断,因为没有使用attach,由于没有使用attach,对一些大型web应用的性能应该没什么影响。
-
使用dumpclass,配合cfr 基本上可以方便的显示所有class的java 代码。
缺点
-
dumpclass使用环境限制,导致只能在java8的环境使用,java11使用dump功能时会出现报错(待解决)
-
java.lang.RuntimeException: can't determine target's VM version : field "_reserve_for_allocation_prefetch" not found in type Abstract_VM_Version sun.jvm.hotspot.debugger.DebuggerException: java.lang.RuntimeException: can't determine target's VM version : field "_reserve_for_allocation_prefetch" not found in type Abstract_VM_Version at ...
-
交互界面过于简陋,待优化
-
涉及到复杂代码的检测仍然需要人工去查看
-
白名单中的类未经过仔细考察,不知道是否能被利用
-
反编译后的代码检测过于简单,容易产生误报
-
目前只做了windows端的dll,linux端的so文件以后会更新
测试
1.godzilla
在instrument检测处一个恶意class
在servlet检测处 恶意servlet
2.javaagent型的内存马
写一个agent attach到tomcat,修改javax.servlet.http.HttpServlet类
通过risk_implement检测,列出有风险的类,在使用dumpclass,可以看到代码中含有刚刚添加的代码
3.attach阻断绕过
在开启阻断代码后,其他agent无法attach
但是该程序仍能正常检测。