?? 通用shellcode深入剖析.txt
字號:
通用ShellCode深入剖析
--------------------------------------------------------------------------------
第八軍團 時間:2004-2-22 13:01:28
前言:
在網上關于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),進行
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -