3.2 定位shellcode
3.2.1 棧幀移位與jmp esp
回憶2.4節中的代碼植入實驗,當我們可以用越界的字符完全控制返回地址后,需要將返回地址改寫成shellcode在內存中的起始地址。在實際的漏洞利用過程中,由于動態鏈接庫的裝入和卸載等原因,Windows進程的函數棧幀很有可能會產生“移位”,即shellcode在內存中的地址是會動態變化的,因此像2.4節中那樣將返回地址簡單地覆蓋成一個定值的做法往往不能讓exploit奏效,如圖3.2.1所示。
要想使exploit不至于10次中只有2次能成功地運行shellcode,我們必須想出一種方法能夠在程序運行時動態定位棧中的shellcode。
回顧2.4節代碼植入實驗中在verify_password函數返回后棧中的情況,如圖3.2.2所示。
(1)實線體現了代碼植入的流程:將返回地址淹沒為我們手工查出的shellcode起始地址0x0012FAF0,函數返回時,這個地址被彈入EIP寄存器,處理器按照EIP寄存器中的地址取指令,最后棧中的數據被處理器當成指令得以執行。
(2)虛線則點出了這樣一個細節:在函數返回的時候,ESP恰好指向棧幀中返回地址的后一個位置!

圖3.2.1 棧幀移位示意圖

圖3.2.2 溢出發生時棧、寄存器與代碼之間的關系
一般情況下,ESP寄存器中的地址總是指向系統棧中且不會被溢出的數據破壞。函數返回時,ESP所指的位置恰好是我們所淹沒的返回地址的下一個位置,如圖3.2.3所示。
提示:函數返回時,ESP所指位置還與函數調用約定、返回指令等有關。例如,retn3與retn4在返回后,ESP所指的位置都會有所差異。

圖3.2.3 使用“跳板”的溢出利用流程
由于ESP寄存器在函數返回后不被溢出數據干擾,且始終指向返回地址之后的位置,我們可以使用圖3.2.3所示的這種定位shellcode的方法來進行動態定位。
(1)用內存中任意一個jmp esp指令的地址覆蓋函數返回地址,而不是原來用手工查出的shellcode起始地址直接覆蓋。
(2)函數返回后被重定向去執行內存中的這條jmp esp指令,而不是直接開始執行shellcode。
(3)由于esp在函數返回時仍指向棧區(函數返回地址之后),jmp esp指令被執行后,處理器會到棧區函數返回地址之后的地方取指令執行。
(4)重新布置shellcode。在淹沒函數返回地址后,繼續淹沒一片??臻g。將緩沖區前邊一段地方用任意數據填充,把shellcode恰好擺放在函數返回地址之后。這樣,jmp esp指令執行過后會恰好跳進shellcode。
這種定位shellcode的方法使用進程空間里一條jmp esp指令作為“跳板”,不論棧幀怎么“移位”,都能夠精確地跳回棧區,從而適應程序運行中shellcode內存地址的動態變化。
本節實驗將把4.4節代碼植入實驗中的password.txt文件改造成上述思路的exploit,并加入安全退出的代碼避免點擊消息框后程序的崩潰。
題外話:1998年,黑客組織“Cult of the Dead Cow”的Dildog在Bugtrq郵件列表中以Microsoft Netmeeting為例首次提出了利用jmp esp完成對shellcode的動態定位,從而解決了Windows下棧幀移位問題給開發穩定的exploit帶來的重重困難。毫不夸張地講,跳板技術應該算得上是Windows棧溢出利用技術的一個里程碑。
3.2.2 獲取“跳板”的地址
我們必須首先獲得進程空間內一條jmp esp指令的地址作為“跳板”。通過第1章對PE文件和Win_32平臺下進程4GB的虛擬內存空間的學習,我們應當明白除了PE文件的代碼被讀入內存空間,一些經常被用到的動態鏈接庫也將會一同被映射到內存。其中,諸如kernel.32.dll、user32.dll之類的動態鏈接庫會被幾乎所有的進程加載,且加載基址始終相同。
2.4節實驗中的有漏洞的密碼驗證程序已經加載了user32.dll,所以我們準備使用user32.dll中的jmp esp作為跳板。獲得user32.dll內跳轉指令地址最直觀的方法就是編程序搜索內存。
#include <windows.h> #include <stdio.h> #define DLL_NAME "user32.dll" main() { BYTE* ptr; int position,address; HINSTANCE handle; BOOL done_flag = FALSE; handle=LoadLibrary(DLL_NAME); if(!handle) { printf(" load dll erro !"); exit(0); } ptr = (BYTE*)handle; for(position = 0; !done_flag; position++) { try { if(ptr[position] == 0xFF && ptr[position+1] == 0xE4) { //0xFFE4 is the opcode of jmp esp int address = (int)ptr + position; printf("OPCODE found at 0x%x\n",address); } } catch(...) { int address = (int)ptr + position; printf("END OF 0x%x\n", address); done_flag = true; } } }
jmp esp對應的機器碼是0xFFE4,上述程序的作用就是從user32.dll在內存中的基地址開始向后搜索0xFFE4,如果找到就返回其內存地址(指針值)。
如果您想使用別的動態鏈接庫中的地址(如“kernel32.dll”、“mfc42.dll”等),或者使用其他類型的跳轉地址(如call esp、jmp ebp等),也可以通過對上述程序稍加修改而輕易獲得。
除此以外,還可以通過OllyDbg的插件輕易地獲得整個進程空間中的各類跳轉地址。您可以到看雪論壇的相關版面下載到這個插件(OllyUni.dll),并把它放在OllyDbg目錄下的Plugins文件夾內,重新啟動OllyDbg進行調試,在代碼框內單擊右鍵,就可以使用這個插件了,如圖3.2.4所示。

圖3.2.4 用OllyDbg的插件搜索“跳板”的地址
搜索結束后,單擊OllyDbg中的“L”按鈕,就可以在日志窗口中查看搜索結果了。
3.2.3 使用“跳板”定位的exploit
仍然使用2.4節中的代碼作為攻擊目標,實驗環境如表3-2-1所示
表3-2-1 實驗環境
說明:函數調用地址和跳轉地址依賴于系統補丁,需要在實驗時重新確定。確定的方法在實驗指導中有詳細說明。
運行我們自己編寫程序搜索跳轉地址得到的結果和OllyDbg插件搜到的結果基本相同,如圖3.2.5所示。

圖3.2.5 OllyDbg搜出的“跳板”與程序搜出的“跳板”地址
題外話:跳轉指令的地址將直接關系到exploit的通用性。事實上,kernel32.dll與user32.dll在不同的操作系統版本和補丁版本中也是有所差異的。最佳的跳轉地址位于那些“千年不變”且被幾乎所有進程都加載的模塊中。
這里不妨采用位于內存0x77DC14CC處的跳轉地址jmp esp作為定位shellcode的“跳板”。
在制作exploit的時候,還應當修復2.4節中shellcode無法正常退出的缺陷。為此,我們在調用MessageBox之后,通過調用exit函數讓程序干凈利落地退出。
這里仍然用dependency walker獲得這個函數的入口地址。如圖3.2.6所示,ExitProcess是kernel32.dll的導出函數,故首先查出kernel32.dll的加載基址0x7C800000,然后加上函數的偏移地址0x0001CDDA,得到函數入口最終的內存地址0x7C81CDDA。

圖3.2.6 計算ExitProcess函數的入口地址
寫出的shellcode的源代碼如下所示。
#include <windows.h> int main() { HINSTANCE LibHandle; char dllbuf[11] = "user32.dll"; LibHandle = LoadLibrary(dllbuf); _asm{ sub sp,0x440 xor ebx,ebx push ebx // cut string push 0x74736577 push 0x6C696166//push failwest mov eax,esp //load address of failwest push ebx push eax push eax push ebx mov eax,0x77D804EA // address should be reset in different OS call eax //call MessageboxA push ebx mov eax,0x7C81CDDA call eax //call exit(0) } }
為了提取出匯編代碼對應的機器碼,我們將上述代碼用VC6.0編譯運行通過后,再用OllyDbg加載可執行文件,選中所需的代碼后可直接將其dump到文件中,如圖3.2.7所示。
通過IDA Pro等其他反匯編工具也可以從PE文件中得到對應的機器碼。當然,如果熟悉intel指令集,也可以為自己編寫專用的由匯編指令到機器指令的轉換工具。
現在我們已經具備了制作新exploit需要的所有信息。

圖3.2.7 從PE文件中提取shellcode的機器碼
(1)搜索到的jmp esp地址,用作重定位shellcode的“跳板”:0x77DC14CC。
(2)修改后并重新提取得到的shellcode,如表3-2-2所示。
表3-2-2 shellcode及注釋
按照2.4節中對棧內情況的分析,我們將password.txt制作成如圖3.2.8所示的形式。

圖3.2.8 在輸入文件中部署shellcode
現在再運行密碼驗證程序,怎么樣,程序退出的時候不會報內存錯誤了吧。雖然還是同樣的消息框,但是這次植入代碼的流程和2.4節中已有很大不同了,最核心的地方就是使用了跳轉地址定位shellcode,進程被劫持的過程如圖3.2.3中我們設計的那樣。