内存马是国内目前比较流行的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的检测制造了困难。
具体的实现可以看一下
议题解析与复现--《Java内存攻击技术漫谈》(一)
我们来看一下大致的实现
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来修改关键类的效果。
如何在服务端构造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内存马检测的思路也就诞生了。
检测程序主要包含五个文件
该程序的优点
可以绕过 对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
但是该程序仍能正常检测。