NoAgent内存马检测

内存马是国内目前比较流行的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的工作流程

NoAgent内存马检测

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

NoAgent内存马检测

现在看来只要将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. 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类入手构造恶意代码。

NoAgent内存马检测

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

NoAgent内存马检测

可以确定redefineclass需要mNormalEnvironment参数。

NoAgent内存马检测

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

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内存马检测

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

  • 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

NoAgent内存马检测

在servlet检测处 恶意servlet

NoAgent内存马检测

2.javaagent型的内存马

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

NoAgent内存马检测

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

NoAgent内存马检测

NoAgent内存马检测

3.attach阻断绕过

在开启阻断代码后,其他agent无法attach

NoAgent内存马检测

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

上一篇:Sigar java 服务器信息探针、监控


下一篇:设计模式【8】-- 手工耿教我写装饰器模式