NoAgent内存马检测
2022-7-16 00:5:12 Author: LemonSec(查看原文) 阅读量:8 收藏

内存马是国内目前比较流行的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内存攻击技术漫谈》(一):https://www.cnblogs.com/xyylll/p/15499293.html

我们来看一下大致的实现

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

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

以下为instrument的工作流程

1.检测工具作为Client,根据指定的PID,向目标JVM发起attach请求;2.JVM收到请求后,做一些校验(比如上文提到的jdk.attach.allowAttachSelf的校验),校验通过后,会打开一个IPC通道。3.接下来Client会封装一个名为AttachOperationC++对象,发送给Server端;4.Server端会把Client发过来的AttachOperation对象放入一个队列;5.Server端另外一个线程会从队列中取出AttachOperation对象并解析,然后执行对应的操作,并把执行结果通过IPC通道返回Client

以下是windows端的防检测

我们来梳理一下loadagent整个流程

现在看来只要将jvmLib导出的两个函数JVM_EnqueueOperation和[email protected] NOP掉即可完成instrument流程的破坏。

来看一下rebeyond师傅的处理方法

用JNI,核心代码如下:
unsigned char buf[]="\xc2\x14\x00"; //32,direct return enqueue functionHINSTANCE hModule = LoadLibrary(L"jvm.dll");//LPVOID dst=GetProcAddress(hModule,"ConnectNamedPipe");LPVOID dst=GetProcAddress(hModule,"[email protected]");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 functionHINSTANCE 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型内存马

https://www.cnblogs.com/xyylll/p/15515254.html

这里讲一下大致原理

首先来看一下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方法redefineClasses08. 执行流继续走入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入手,看一下哪些参数需要。

voidredefineClasses(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 + 0x180x00730065006c0000l);//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 + 0x680x0041414141414141l);// 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, truefalse);                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

但是该程序仍能正常检测。

原文地址:https://www.cnblogs.com/xyylll/p/15818997.html

侵权请私聊公众号删文

 热文推荐  

欢迎关注LemonSec

觉得不错点个“赞”、“在看”


文章来源: http://mp.weixin.qq.com/s?__biz=MzUyMTA0MjQ4NA==&mid=2247529333&idx=3&sn=f5a4e7724fe563c766981ba0c7e2031e&chksm=f9e31c2ece9495382d036c2b8e78b629a3de501fa2dc55a7279edf5b1453a61f010a189178d8#rd
如有侵权请联系:admin#unsafe.sh