CobaltStrike ShellCode詳解
一、前言
CobaltStrike大家應該知道,最近剛好遇上了一個CS的分段的Beacon樣本,詳細分析了下ShellCode,看它ShellCode是如何實現,給大家提供些混淆思路或者檢測思路,如有錯誤歡迎指出。
二、目標樣本
樣本基本信息如下:
MD5: ad26a8c74596c32909923330eee2f6f7
SHA1: 775275f0debf7dce1fd86cc067f986d86884e1e0
SHA256: 627b6a48dbd4cd0c15a7cdd6ed125c2999ac0e6c6bbf8273481e5964a348cd28
未加殼

三、樣本情況說明
這個樣本主要是一個loader負責加載起shellcode,其主要功能就在函數sub_1400111D0其中。

該函數內的主要部分如下,申請內存寫入shellcode并在EnumObjects的回調函數中運行shellcode,加載方式也是比較經典了,這里可以多說兩句,此函數原來的功能是什么不重要,重要的是它可以指定一個回調函數就可以把shellcode的地址作為回調函數加載起來,還可以引申出一種免殺方式,那就是可以將shellcode轉化為包括但不限于UUID/MAC地址后硬編碼進樣本,之后調用對應的轉換函數轉為二進制后調用有回調函數的API將其運行起來,Lazarus的一次攻擊就用的這種方式具體的可以參考這篇文章:
https://research.nccgroup.com/2021/01/23/rift-analysing-a-lazarus-shellcode-execution-method/


接下來就是重點了,加載起來的這段shellcode開頭先將DF標志位置0,這里為什么這樣做后面會提到。

第一個call就是先將返回地址pop到rbp中將wininet的十六進制賦值給R14,之后壓棧在取出字符,將后面要獲取的LoadLibraryA的特征值賦給r10d,最后調用call rbp返回。

之后就是從PEB中獲取文件名和文件名最大長度,具體解析見下:

GS+60h中存儲的PEB

PEB+18h處存儲的是_PEB_LDR_DATA

_PEB_LDR_DATA中+20h的地方就是LIST_ENTRY,這里有一個要注意的點,那就是這里的InMemoryOrderModuleList指向的其實是_LDR_DATA_TABLE_ENTRY中的InMemoryOrderLinks,所以在后面用這個地址的時候直接從InMemoryOrderLinks的地方開始取才是正確的,這里的知識點在下面就會有所用到。


LIST_ENTRY是一個雙向鏈表,其內每個節點都存儲著一個_LDR_DATA_TABLE_ENTRY結構。

InMemoryOrderLinks+48h處儲存的模塊名稱信息的結構體。
注意:此處[rdx+50]并不是取_LDR_DATA_TABLE_ENTRY+50h而是InMemoryOrderLinks+50h,也就是_LDR_DATA_TABLE_ENTRY+60h的地方。


_UNICODE_STRING內+2h的地方存儲著最大長度+8h的地方存儲著模塊名稱,也就是[rdx+50]和[rdx+4A]。

之后就是我前面賣的關子了,shellcode在這塊會調用lodsb指令讀取模塊名,這個指令就是根據DF標志位控制向前還是后讀,這段shellcode功能就是循環讀取模塊名,判斷是否大于61也就是小寫的a,如果大于的話就減20,這里其實是在把小寫的字母轉化為大寫然后用ror和add指令求一個特征值。

接下來就是保存指向InMemoryOrderLinks的rdx和先前求的特征值,在用InMemoryOrderLinks+20h獲取到DllBase,也就是加載的模塊基址,然后用這個基址+3Ch取到NT頭的偏移,在判斷NT頭+18h的地方是否為20B,以此判斷是否為64位文件,之后判斷導出表是否為空,為空則直接跳轉。

這里放上一張PE結構圖供大家參考,也可直接010中對照。

在跳轉處設置堆棧并獲取下一個_LDR_DATA_TABLE_ENTRY,然后跳轉回前面獲取BaseDllName的地址開始下一個循環,直到找到有導出表的模塊。

如果判斷存在導出表的話就先用rax+rdx也就是導出表的RVA+基址獲取到內存中導出表的地址,之后用該地址分別+18h和20h依次獲取到名稱導出函數個數和函數名稱表,然后用函數名地址+函數名導出個數*4獲取到具體函數名,這里*4是因為函數名是dword類型。
接下來就是對獲取到的函數名取特征值,用存著LoadLibraryA特征值的r10d與r9d對比是否為LoadLibraryA,如果遍歷完一個模塊都沒找到就跳轉獲取下一個模塊地址然后跳轉到獲取BaseDllName的地址在下一個模塊中開始下一個循環,直到找到LoadLibraryA。


在找到LoadLibraryA后先彈出導出表地址然后+24h獲取到導出序號表地址,接著獲取序號表內存中位置,用名稱表內的排序去序號表內找對應的序號,因為在導出表中名稱表是與序號表一一對應的,獲取到函數序號后我們可以直接用這個序號去函數地址表中找到對應的地址即可。
一切準備完成后設置堆棧push+jmp直接調用LoadladLibraryA,因為是x64的所以用x64調用約定也就是依次用rcx,rdx,r8,r9,多出部分使用棧進行傳遞,這里的rcx就是之前push的wininet。


這里貼上一段我用PEB找到Kernel32的基址后遍歷導出表找到LoadladLibraryA的地址后調用的代碼,這段代碼最后加載的dll是我寫的一個測試dll,功能是彈個窗。
#include #include #include typedef void* (WINAPI* FnLoadLibraryA)(char*);FnLoadLibraryA MyLoadLibraryA; int main(){ UINT_PTR uiBaseAddress = 0; UINT_PTR uiExportDir = 0; UINT_PTR uiNameArray = 0; UINT_PTR uiAddressArray = 0; UINT_PTR uiNameOrdinals = 0; DWORD dwCounter = 0; void* hKernel32 = NULL; //直接通過PEB獲取到Kernel32的基址 __asm { mov rdx, gs: [60h] mov rdx, [rdx + 18h] mov rdx, [rdx + 20h] mov rdx, [rdx] mov rdx, [rdx] mov rdx, [rdx + 20h] mov hKernel32, rdx } uiBaseAddress = (UINT_PTR)hKernel32; //獲取NT頭 uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew; //獲取數據目錄表中的導出表RVA uiNameArray = (UINT_PTR) & ((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; //獲取導出表 uiExportDir = uiBaseAddress +((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress; //獲取名稱表 uiNameArray = uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNames; //獲取導出地址表 uiAddressArray = uiBaseAddress +((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions; //獲取導出序號表 uiNameOrdinals = uiBaseAddress +((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNameOrdinals; //獲取名稱導出的個數 dwCounter = ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->NumberOfNames; while (dwCounter--) { char* cpExportedFunctionName = (char*)(uiBaseAddress + *(DWORD*)(uiNameArray)); //找LoadLibrary if (strstr(cpExportedFunctionName, "LoadLibraryA") != NULL) { // 用導出序號*dword是在獲取函數在地址表內的位置+地址表獲取到內存中的位置 uiAddressArray += (*(WORD*)(uiNameOrdinals) * sizeof(DWORD)); printf(" LoadLibraryA RVA: % d", *(DWORD*)(uiAddressArray)); // 返回函數地址RVA+基址 MyLoadLibraryA = (FnLoadLibraryA)( uiBaseAddress+* (DWORD*)uiAddressArray); //調用后加載 HMODULE hUser32 = (HMODULE)MyLoadLibraryA((char*)"testdll.dll"); //返回LoadLibraryA的rva return *(DWORD*)(uiAddressArray); } // 名稱表++ uiNameArray += sizeof(DWORD); // 序號表++ uiNameOrdinals += sizeof(WORD); } return 0;}

OK,我們接著說,shellcode在調用LoadLibrary加載wininet后返回shellcode起始處,開始遍歷加載模塊找加載的wininet遍歷其導出表依次找到并調用InternetOpenUrlA,InternetOpenA, InternetConnectA,HttpOpenRequestA, HttpSendRequestA,在調用HttpSendRequestA之后會判斷返回值是否為NULL,如果返回失敗的話就不停的嘗試10次。


在嘗試10次都失敗后會跳轉賦值r14而不是賦值r10,因為R10在這段shellcode中是存儲函數特征值的,所以這樣就會導致r10一直為0從而沒有匹配到的函數,最后遍歷完所有模塊后lodsb指令讀取模塊名時導致異常。


如果返回成功的話就會調用VirtualAllocEx申請空間。

之后會調用InternetReadFile從c2分段讀取文件到申請的空間內,InternetReadFile在文件讀取完成后會將接收讀取字節數變量的指針置為0,shellcode借此判斷文件是否讀取完成,讀取完成后跳轉執行。

跳轉會有一個小循環解密后續的payload。

其連接c2在VT評論如下:

解密出一個dll文件并加載,可以將其dump下來。

其dll文件為CS的后門運行后會調用自身的反射加載函數,在內存中實現反射注入,這里反射如何實現與分析我們留待下次細說。

之后會回連c2執行c2指令。

根據獲取指令執行后續操作。

通過上面的分析可以發現這是一個cs的分段Beacon,其特征就是會從C2下載并加載后續的payload,相對的也就是還有不分段的Beacon可以直接加載shellcode執行,這里我用cs4.4生成了一個默認的不分段的Beacon快速分析一下。
不分段的Beacon
樣本會在解密出shellcode后在線程回調中調用。


其解密出的shellcdoe就是類似于前面分析的分段Beacon連接cc獲取的CS的dll文件。

將其dump下來后會發現其用于接受指令執行的流程圖基本一致。


可以看出這兩種cs的樣本最終的目的都是用外層的loader加載起來內層的shellcode,后面的后滲透操作都是由加載起來的dll所執行。分析下來后看這段ShellCode其實也是比較經典的,PEB找系統模塊,遍歷導出表調用,最后獲取payload解密調用。