记录黑客技术中优秀的内容,传播黑客文化,分享黑客技术精华

NoAgent内存马检测工具

2022-01-04 12:17

0x00 内存马简单介绍

首先,内存马分为两类,一类是利用web中间件组件或框架的特性在web执行流程中嵌入恶意代码来执行命令,例如tomcat的filter,servlet,springmvc的controller等,这类内存马在检测时也可以直接检测相应的组件。

另一类就是java-agent型的内存马,通过上传jar包,attach web应用,调用instrument,使用redefine或者retransform直接修改其关键类的代码,如冰蝎修改的就是 javax.servlet.http.HttpServlet类,在一般的web访问流程中都会调用该类,这类内存马检测时同样也需要java-agent来进行检测。

0x01 内存马防检测

以上为内存马简单的介绍,现在来看一下其进阶的技术,在rebeyond师傅的Java 内存攻击技术漫谈一文中,谈到了如何阻断java-agent的attach过程,这就为使用agent的检测制造了困难。

具体的实现可以看一下

议题解析与复现--《Java内存攻击技术漫谈》(一)

我们来看一下大致的实现

instrument机制实现类agent内存马的注入,但是也可以实现对内存马进行检测。

这里给出的方法就是注入内存马后将instrument机制破坏的,使其无法检测进程的类字节码等。

以下为instrument的工作流程

2414837-20211102153919433-348603466.png

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整个流程

2414837-20211102153914752-44573663.png

现在看来只要将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来修改关键类的效果。

0x02 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类入手构造恶意代码。

image-20211230190257907.png

这样就要先构造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是一个宏或函数,搜索一下可以发现这是个宏

2414837-20211105210245553-573235734.png

可以确定redefineclass需要mNormalEnvironment参数。

2414837-20211105210245264-2015072586.png

来看一下这个参数的结构。

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内存马检测的思路也就诞生了。

image-20211227114355955.png

检测程序主要包含五个文件

  • 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文件以后会更新

0x03 测试

1.godzilla

在instrument处检测出一个恶意class

image-20211216151956326.png

在servlet处 检测出恶意servlet
image-20211227150755631.png

2.javaagent型的内存马

写一个agent attach到tomcat,修改javax.servlet.http.HttpServlet类

image-20211216154254716.png

通过risk_implement检测,列出有风险的类,在使用dumpclass,可以看到代码中含有刚刚添加的代码

image-20211216154050721.png
image-20211216154235802.png

3.attach阻断绕过

在开启阻断代码后,其他agent无法attach
image-20211227155407767.png
但是该程序仍能正常检测。

工具链接:
https://github.com/xyy-ws/NoAgent-memshell-scanner(还在更新中,现在只能检测部分内存马,内存马的删除有待更新)


知识来源: https://tttang.com/archive/1390/

阅读:55444 | 评论:0 | 标签:内存

想收藏或者和大家分享这篇好文章→复制链接地址

“NoAgent内存马检测工具”共有0条留言

发表评论

姓名:

邮箱:

网址:

验证码:

黑帝公告 📢

永久免费持续更新精选优质黑客技术文章Hackdig,帮你成为掌握黑客技术的英雄

↓赞助商 🙇🧎

标签云 ☁