CS-Shellcode分析(一)
前言
本文是CS的shellcode分析的第一篇文章,該系列文章旨在幫助具有一定二進制基礎的朋友看懂cs的shellcode的生成方式,進而可以達到對shellcode進行二進制層面的改變與混淆,用于免殺相關的研究。
一、生成
首先設置一個監聽器并生成一個原始格式的shellcode(Attacks -> Packages -> Payload Generator)

這里我們就得到了一個原始的CS的shellcode:

buf中的其實是二進制的可執行代碼,然而該代碼是沒有辦法直接運行的,因為他到可執行文件(exe)之間還差了一些必要的文件結構,所以我們需要寫一個加載器去執行他,一個最簡單的加載器如下:
#include
int main(){ unsigned char Scode[] = "Your shellcode"; //申請內存(權限為rwx) void* exec = VirtualAlloc(0, 1024, MEM_COMMIT, PAGE_EXECUTE_READWRITE); //將shellcode復制進申請的內存中 RtlMoveMemory(exec, Scode, 1024); //執行shellcode ((void(*)())exec)(); return 0;}
用vs2019編譯該C代碼,選擇Debug-X64模式編譯,便于調試(這里的位數應與shellcode的位數保持一致)
生成exe后我們就可以對其進行靜態/動態分析了
二、靜態分析
加載器部分
使用IDA打開生成的exe

可以看到main函數其實非常的短,除了由編譯器自動生成的代碼如checkForDeguggerJustMyCode這種外,我們重點注意兩點,第一是圖中黃色的unk_140019c30,這個地方其實就是shellcode的儲存的位置

這里通過逐字節的復制將shellcode放入到了[rbp+550+Src]的位置其實也就是rbp+10的位置,跟進unk_140019c30可以看到十六進制形式的shellcode:

另外一處就是標注evilCode的位置,上面看到memmove將[rbp+550h+Src]位置的shellcode復制到了[rbp+550h+var_1A8],然后直接call這個地址執行了shellcode代碼,順便說一句,x64的函數調用可以不依賴于棧實現,如memmove的參數就是通過寄存器來傳遞的,關于MSVC的x64函數調用可以參考官方文檔
加載器部分基本就沒什么可說的了,我們來看shellcode的部分,他又做了什么
shellcode部分
我們可以使用IDApython來將shellcode導出出來
IDApython是一個IDA增強插件,允許用戶用python編寫IDA腳本,IDA7.0自帶IDApython,只需要確保本機裝有相應的python環境即可(py2/py3均可),但是需要注意IDA7.0已經切換到x64構架了,所以對應的python版本也需要是x64版本才能正常使用,如果沒有什么問題,那么當使用IDA打開任意exe時應會有類似的提示:
IDAPython Hex-Rays bindings initialized. --------------------------------------------------------------------------------------------- Python 3.7.9 (tags/v3.7.9:13c94747c7, Aug 17 2020, 18:58:18) [MSC v.1900 64 bit (AMD64)] IDAPython 64-bit v7.4.0 final (serial 0) (c) The IDAPython Team ---------------------------------------------------------------------------------------------
首先從File選項中找到script command選項,并輸入如下腳本:
import idaapistart_address = 0x140019C30 #替換成對應的shellcode的起始地址data_length = 1024data = idc.get_bytes(start_address , data_length)fp = open('D:\\project\\CS-shellcode-analysis\\shellcode\\shellcode.bin', 'wb') #替換成保存的路徑fp.write(data)fp.close()

執行后可以得到shellcode的原始文件(其實也就是剛剛生成的文件中buf部分按16進制寫入的文件)

然后我們再用IDA打開這個文件進行分析:

先看第一個call的內容

這里pop rbp 相當于將call的返回地址pop到了rbp中,然后把teniniw這個字符串放到了RCX中,這里由于是在棧上傳遞數據,所以其實是顛倒的wininet這個字符串,在動態調試時可以清晰的看到這點

然后后面call rbp就相當于返回剛剛代碼中繼續執行,其中的mov操作相當于是函數傳參了

接著看,將參數壓棧,然后取了gs段60h處的東西,這里gs段在用戶態是指向TEB(Thread Environment Block,線程環境塊)這個結構的
此結構體包含進程中運行的線程的各種信息,進程中的每個線程都對應一個 TEB 結構體,而TEB中又存儲著PEB的地址,通過遍歷PEB可以獲取到kernel32和ntdll的基址(關于PEB的簡介,可以參考這里),進而實現調用系統函數,對于shellcode來說這是一種很常見的在程序中定位動態鏈接庫的地址進而獲取函數地址的方法,得到函數地址后就可以調用相應函數了,借助這篇文章我們也可以看到他的遍歷方式
為了看到TEB的結構信息,建議使用windbg在全局模式下使用dt _TEB來查看TEB的結構(因為微軟的符號服務器被墻了= =所以說需要全局)我們來看怎么手動遍歷TEB并獲取函數地址的:在gs:[60h]的位置是ProcessEnvironmentBlock(也就是PEB)這個結構里面(如果60h在User32Reversed中的話,是windbg調試的不是64位程序,隨意調試一個64位程序就能看到如下的TEB表了)

也就是說現在取到了PEB的地址放入rdx,然后又接著取PEB的0x18h的內容,需要接著看PEB的結構:

同樣接著去看_PEB_LDR_DATA這個結構,又取了0x20這里:

在接著往下說之前,我們先插入講點PEB相關的東西。我們看到,在PEB_LDR_DATA結構中,又包含三個LIST_ENTRY結構體分別命名為:
InLoadOrderModuleList; 模塊加載順序 InMemoryOrderModuleList; 模塊在內存中的順序 InInitializationOrderModuleList; 模塊初始化裝載順序
LIST_ENTRY其結構定義如下:
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

微軟是怎么解釋LIST_ENTRY結構中成員作用的呢?來看看MSDN
The head of a doubly-linked list that contains the loaded modules for the process. Each item in the list is a pointer to an LDR_DATA_TABLE_ENTRY structure
這個雙鏈表指向進程裝載的模塊,結構中的每個指針,指向了一個LDR_DATA_TABLE_ENTRY 的結構,同樣可以通過dt _LDR_DATA_TABLE_ENTRY來查看該結構:
0:000> dt _LDR_DATA_TABLE_ENTRY ntdll!_LDR_DATA_TABLE_ENTRY +0x000 InLoadOrderLinks : _LIST_ENTRY +0x010 InMemoryOrderLinks : _LIST_ENTRY +0x020 InInitializationOrderLinks : _LIST_ENTRY +0x030 DllBase : Ptr64 Void +0x038 EntryPoint : Ptr64 Void +0x040 SizeOfImage : Uint4B +0x048 FullDllName : _UNICODE_STRING +0x058 BaseDllName : _UNICODE_STRING +0x068 FlagGroup : [4] UChar +0x068 Flags : Uint4B +0x068 PackagedBinary : Pos 0, 1 Bit +0x068 MarkedForRemoval : Pos 1, 1 Bit +0x068 ImageDll : Pos 2, 1 Bit +0x068 LoadNotificationsSent : Pos 3, 1 Bit +0x068 TelemetryEntryProcessed : Pos 4, 1 Bit +0x068 ProcessStaticImport : Pos 5, 1 Bit +0x068 InLegacyLists : Pos 6, 1 Bit +0x068 InIndexes : Pos 7, 1 Bit +0x068 ShimDll : Pos 8, 1 Bit +0x068 InExceptionTable : Pos 9, 1 Bit +0x068 ReservedFlags1 : Pos 10, 2 Bits +0x068 LoadInProgress : Pos 12, 1 Bit +0x068 LoadConfigProcessed : Pos 13, 1 Bit +0x068 EntryProcessed : Pos 14, 1 Bit +0x068 ProtectDelayLoad : Pos 15, 1 Bit +0x068 ReservedFlags3 : Pos 16, 2 Bits +0x068 DontCallForThreads : Pos 18, 1 Bit +0x068 ProcessAttachCalled : Pos 19, 1 Bit +0x068 ProcessAttachFailed : Pos 20, 1 Bit +0x068 CorDeferredValidate : Pos 21, 1 Bit +0x068 CorImage : Pos 22, 1 Bit +0x068 DontRelocate : Pos 23, 1 Bit +0x068 CorILOnly : Pos 24, 1 Bit +0x068 ChpeImage : Pos 25, 1 Bit +0x068 ReservedFlags5 : Pos 26, 2 Bits +0x068 Redirected : Pos 28, 1 Bit +0x068 ReservedFlags6 : Pos 29, 2 Bits +0x068 CompatDatabaseProcessed : Pos 31, 1 Bit +0x06c ObsoleteLoadCount : Uint2B +0x06e TlsIndex : Uint2B +0x070 HashLinks : _LIST_ENTRY +0x080 TimeDateStamp : Uint4B +0x088 EntryPointActivationContext : Ptr64 _ACTIVATION_CONTEXT +0x090 Lock : Ptr64 Void +0x098 DdagNode : Ptr64 _LDR_DDAG_NODE +0x0a0 NodeModuleLink : _LIST_ENTRY +0x0b0 LoadContext : Ptr64 _LDRP_LOAD_CONTEXT +0x0b8 ParentDllBase : Ptr64 Void +0x0c0 SwitchBackContext : Ptr64 Void +0x0c8 BaseAddressIndexNode : _RTL_BALANCED_NODE +0x0e0 MappingInfoIndexNode : _RTL_BALANCED_NODE +0x0f8 OriginalBase : Uint8B +0x100 LoadTime : _LARGE_INTEGER +0x108 BaseNameHashValue : Uint4B +0x10c LoadReason : _LDR_DLL_LOAD_REASON +0x110 ImplicitPathOptions : Uint4B +0x114 ReferenceCount : Uint4B +0x118 DependentLoadFlags : Uint4B +0x11c SigningLevel : UChar
所以[rdx+50h]就指向了FullDllName這個_UNICODE_STRING的結構體:
0:000> dt _UNICODE_STRING ntdll!_UNICODE_STRING +0x000 Length : Uint2B +0x002 MaximumLength : Uint2B +0x008 Buffer : Ptr64 Wchar
換算下應該是0x48+0x8=0x50,也就是Buffer的內容,這里存儲的就是Dll的全名,可以通過這個名字確定當前遍歷到的dll是不是自己想要的,默認第一個指向的就是自己這個程序,我們可以通過一個Demo來證實:
#include
int main(){ void* peb = 0; void* PEB_LDR_DATA = 0; void* InMemoryOrderModuleList = 0; wchar_t* DllName = 0; __asm { mov rdx, gs:[60h] mov peb, rdx mov rdx, [rdx+18h] mov PEB_LDR_DATA, rdx mov rdx, [rdx+20h] mov InMemoryOrderModuleList, rdx mov rdx, [rdx + 50h] mov DllName, rdx } printf("peb address:\t%p", peb); printf("PEB_LDR_DATA address:\t%p", PEB_LDR_DATA); printf("InMemoryOrderModuleList address:\t%p", InMemoryOrderModuleList); printf("Dllname address:\t%p", DllName); wprintf(L"DllName:\t%s", DllName);
return 0;}
注意,這里由于msvc是不支持64位的C++內嵌匯編的,所以想運行如上代碼需要做以下設置:

如果沒有llvm(clang-cl)這個選項可以通過visual studio 2019 installer來安裝:


然后運行就可以驗證我們剛剛說的:

那么再借助_LIST_ENTRY就可以遍歷這整個由_LIST_ENTRY 組織起來的雙向循環鏈表了。
我們接著看shellcode代碼:

這里將模塊名放入rsi,最大長度放入rcx,之后做了一步字符變大寫的操作(如果字符小于a,那么ASCII值減0x20,正好變成對應的大寫字符),便于后面進行比較,具體怎么比較的,師傅們可以先自己嘗試看能不能看懂。

OK,這篇文章就先到這里,有上述鋪墊后各位師傅可以自己嘗試手擼匯編來遍歷整個_LDR_DATA_TABLE_ENTRY結構來獲取ntdll和kernel32的地址來調用函數,這是木馬很常見的一種調用系統函數的方式,同時遍歷方法也比較多變,可以在這里做文章對shellcode進行混淆,也可以接著閱讀shellcode代碼。