標?題
:?
【技術專題】軟件漏洞分析入門_4_初級棧溢出C_修改程序流程
作?者
:?
failwest
時?間
:?
2007
-
12
-
15
,
01
:
01
鏈?接
:?
http
:
//bbs.pediy.com/showthread.php?t=56582
第
4
講??初級棧溢出C
To?be?the?apostrophe?which?changed?“Impossible”?into?“I’m?possible”
——?failwest
沒有星星的夜里,我用知識吸引你
上節課沒有操練滴東西,不少蠢蠢欲動的同學肯定已經坐不住了。悟空,不要猴急,下面的兩堂課都是實踐課,用來在實踐中深入體會上節課中的知識,并且很有趣味性哦
??信息安全技術是一個對技術性要求極高的領域,除了扎實的計算機理論基礎外、更重要的是優秀的動手實踐能力。在我看來,不懂二進制就無從談起安全技術。
??緩沖區溢出的概念我若干年前已經了然于胸,不就是淹個返回地址把CPU指到緩沖區的shellcode去么。然而當我開始動手實踐的時候,才發現實際中的情況遠遠比原理復雜。
??國內近年來對網絡安全的重視程度正在逐漸增加,許多高校相繼成立了“信息安全學院”或者設立“網絡安全專業”。科班出身的學生往往具有扎實的理論基礎,他們通曉密碼學知識、知道PKI體系架構,但要談到如何真刀實槍的分析病毒樣本、如何拿掉PE上復雜的保護殼、如何在二進制文件中定位漏洞、如何對軟件實施有效的攻擊測試……能夠做到的人并不多。
??雖然每年有大量的網絡安全技術人才從高校涌入人力市場,真正能夠滿足用人單位需求的卻聊聊無幾。捧著書本去做應急響應和風險評估是濫竽充數的作法,社會需要的是能夠為客戶切實解決安全風險的技術精英,而不是滿腹教條的闊論者。
??我所知道的很多資深安全專家都并非科班出身,他們有的學醫、有的學文、有的根本沒有學歷和文憑,但他們卻技術精湛,充滿自信。
??這個行業屬于有興趣、夠執著的人,屬于為了夢想能夠不懈努力的意志堅定者。如果你是這樣的人,請跟著我把這個系列的所有實驗全部完成,之后你會發現眼中的軟件,程序,語言,計算機都與以前看到的有所不同——因為以前使用肉眼來看問題,我會教你用心和調試器以及手指來重新體驗它們。
首先簡單復習上節課的內容:
高級語言經過編譯后,最終函數調用通過為其開辟棧幀來實現
開辟棧幀的動作是編譯器加進去的,高級語言程序員不用在意
函數棧幀中首先是函數的局部變量,局部變量后面存放著函數返回地址
當前被調用的子函數返回時,會從它的棧幀底部取出返回地址,并跳轉到那個位置(母函數中)繼續執行母函數
我們這節課的思路是,讓溢出數組的數據躍過authenticated,一直淹沒到返回地址,把這個地址從main函數中分支判斷的地方直接改到密碼驗證通過的分支!
這樣當verify_password函數返回時,就會返回到錯誤的指令區去執行(密碼驗證通過的地方)
由于用鍵盤輸入字符的ASCII表示范圍有限,很多值如
0x11
,
0x12
等符號無法直接用鍵盤輸入,所以我們把用于實驗的代碼在第二講的基礎上稍加改動,將程序的輸入由鍵盤改為從文件中讀取字符串。
#include?
<
stdio
.
h
>
#define?
PASSWORD?
"1234567"
int?
verify_password?
(
char?
*
password
)
{
??
int?
authenticated
;
??
char?
buffer
[
8
];
??
authenticated
=
strcmp
(
password
,
PASSWORD
);
??
strcpy
(
buffer
,
password
);
//over?flowed?here!??
??
return?
authenticated
;
}
main
()
{
??
int?
valid_flag
=
0
;
??
char?
password
[
1024
];
??
FILE?
*?
fp
;
??
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
);
}
??
程序的基本邏輯和第二講中的代碼大體相同,只是現在將從同目錄下的password
.
txt文件中讀取字符串而不是用鍵盤輸入。我們可以用十六進制的編輯器把我們想寫入的但不能直接鍵入的ASCII字符寫進這個password
.
txt文件。
??
??用VC6
.0
將上述代碼編譯鏈接。我這里使用默認編譯選項,BUILD成debug版本。鑒于有些同學反映自己的用的是VS2003和VS2005,我好人做到底,把我build出來的PE一并在附件中雙手奉上——沒話說了吧!不許不學,不許學不會,不許說難,不許不做實驗!呵呵。
要PE的點這里:?stack_overflow_ret
.
rar
在與PE文件同目錄下建立password
.
txt并寫入測試用的密碼之后,就可以用OllyDbg加載調試了。
停~~~啥是OllyDbg,開玩笑,在這里問啥是Ollydbg分明是不給看雪老大的面子么!如果沒有這個調試器的話,去工具版找吧,帖子附件要掛出個OD的話會給被人鄙視的。
??在開始動手之前,我們先理理思路,看看要達到實驗目的我們都需要做哪些工作。
??要摸清楚棧中的狀況,如函數地址距離緩沖區的偏移量,到底第幾個字節能淹到返回地址等。這雖然可以通過分析代碼得到,但我還是推薦從動態調試中獲得這些信息。
??要得到程序中密碼驗證通過的指令地址,以便程序直接跳去這個分支執行
??要在password
.
txt文件的相應偏移處填上這個地址
??這樣verify_password函數返回后就會直接跳轉到驗證通過的正確分支去執行了。
??首先用OllyDbg加載得到的可執行PE文件如圖:
圖
1??
??
閱讀上圖中顯示的反匯編代碼,可以知道通過驗證的程序分支的指令地址為
0x00401122
。
簡單解釋一下這段匯編與C語言的對應關系,其實憑著OD給出的注釋,就算你沒學過匯編語言,讀懂也應該沒啥問題。
0x00401102
處的函數調用就是verify_password函數,之后在
0x0040110A
處將EAX中的函數返回值取出?,在?
0x0040110D
處與
0
比較,然后決定跳轉到提示驗證錯誤的分支或提示驗證通過的分支。提示驗證通過的分支從
0x00401122
處的參數壓棧開始。
啥?用OllyDbg加載后找不到verify_password函數的位置?這個嘛,我這里只說一次啊。
OllyDbg在默認情況下將程序中斷在PE裝載器開始處,而不是main函數的開始。如果您有興趣的話可以按F8單步跟蹤一下看看在main函數被運行之前,裝載器都做了哪些準備工作。一般情況下main函數位于GetCommandLineA函數調用后不遠處,并且有明顯的特征:在調用之前有
3
次連續的壓棧操作,因為系統要給main傳入默認的argc、argv等參數。找到main函數調用后,按F7單步跟入就可以看到真正的代碼了。
我相信你,你一定行的,找到了嗎?什么?還找不到?好吧,按ctr
+
g后面輸入截圖中的地址
0x00401102
,這回看到了吧。建議你按F2下個斷點記住這個位置,別一會兒又在PE里邊迷路了。
這步完成后,您應該對這個PE的主要代碼有了一個把握了。這才牙長一點指令啊,真正的漏洞要對付的是軟件,那個難纏~~~好,不潑冷水了
如果我們把返回地址覆蓋成這個地址,那么在
0x00401102?
處的函數調用返回后,程序將跳轉到驗證通過的分支,而不是進入
0x00401107
處分支判斷代碼。這個過程如下圖所示:
??
圖
2
??
通過動態調試,發現棧幀中的變量分布情況基本沒變。這樣我們就可以按照如下方法構造password
.
txt中的數據:
??仍然出于字節對齊、容易辨認的目的,我們將“
4321
”作為一個輸入單元。
??buffer
[
8
]
共需要
2
個這樣的單元
??第
3
個輸入單元將authenticated覆蓋
??第
4
個輸入單元將前棧幀EBP值覆蓋
??第
5
個輸入單元將返回地址覆蓋
??為了把第
5
個輸入單元的ASCII碼值
0x34333231
修改成驗證通過分支的指令地址
0x00401122
,我們采取如下方式借助
16
進制編輯工具UltraEdit來完成(
0x40
,
0x11
等ASCII碼對應的符號很難用鍵盤輸入)。
??
??步驟
1
:創建一個名為password
.
txt的文件,并用記事本打開,在其中寫入
5
個“
4321
”后保存到與實驗程序同名的目錄下:
?
圖
3
步驟
2
:保存后用UltraEdit_32重新打開,如圖:
?
圖
4
啥?問啥是UltraEdit?去工具版找吧,多的不得了,這里是看雪!
步驟
3
:將UltraEdit_32切換到
16
進制編輯模式,如圖:
?
圖
5
步驟寫到這個份上了,您不會還跟不上吧。
??步驟
4
:將最后四個字節修改成新的返回地址,注意這里是按照“內存數據”排列的,由于“大頂機”的緣故,為了讓最終的“數值數據”為
0x00401122
,我們需要逆序輸入這四個字節。如圖:
?
圖
6
步驟
5
:這時我們可以切換回文本模式,最后這四個字節對應的字符顯示為亂碼:
?
圖
7
最終的password
.
txt我也給你附上。
要txt的點這里:??password
.
txt
將password
.
txt保存后,用OllyDbg加載程序并調試,可以看到最終的棧狀態如下表所示:
局部變量名??內存地址??偏移
3
處的值??偏移
2
處的值??偏移
1
處的值??偏移
0
處的值
buffer
[
0
~
3
]??
0x0012FB14??0x31?
(
‘
1
’
)??
0x32?
(
‘
2
’
)??
0x33?
(
‘
3
’
)??
0x34?
(
‘
4
’
)
buffer
[
4
~
7
]??
0x0012FB18??0x31?
(
‘
1
’
)??
0x32?
(
‘
2
’
)??
0x33?
(
‘
3
’
)??
0x34?
(
‘
4
’
)
authenticated
(被覆蓋前)??
0x0012FB1C??0x00??0x00??0x00??0x01
authenticated
(被覆蓋后)??
0x0012FB1C??0x31?
(
‘
1
’
)??
0x32?
(
‘
2
’
)??
0x33?
(
‘
3
’
)??
0x34?
(
‘
4
’
)
前棧幀EBP
(被覆蓋前)??
0x0012FB20??0x00??0x12??0xFF??0x80
前棧幀EBP
(被覆蓋后)??
0x0012FB20??0x31?
(
‘
1
’
)??
0x32?
(
‘
2
’
)??
0x33?
(
‘
3
’
)??
0x34?
(
‘
4
’
)
返回地址
(被覆蓋前)??
0x0012FB24??0x00??0x40??0x11??0x07
返回地址
(被覆蓋后)??
0x0012FB24??0x00??0x40??0x11??0x22
程序執行狀態如圖:
??由于棧內EBP等被覆蓋為無效值,使得程序在退出時堆棧無法平衡,導致崩潰。雖然如此,我們已經成功的淹沒了返回地址,并讓處理器如我們設想的那樣,在函數返回時直接跳轉到了提示驗證通過的分支。
同學們,你們成功了么?
最后再總結一下這個實驗的內容:
通過Ollydbg調試PE文件確定密碼驗證成功的分支的指令所處的內存地址為
0x00401122
通過調試確定buffer數組距離棧幀中函數返回地址的偏移量
在password
.
txt相應的偏移處準確的寫入
0x00401122
,當password
.
txt被讀入后會同樣準確的把verify_password函數的返回地址從分支判斷處修改到
0x00401122
(密碼正確分支)
函數返回時,笨笨的返回到密碼正確的地方
程序繼續執行,但由于棧被破壞,不再平衡,故出錯
試想一下,如果我們在buffer
[]
中填入一些可執行的機器碼,然后用溢出的數據把返回地址指向buffer
[]
,那么函數返回后這些代碼是不是就會執行了?
答案是肯定的,下一講我將用類似的敘述方式,同樣手把手的和您一起完成這段機器代碼的編寫,并把它們準確的布置在password
.
txt中,這樣原本用來讀取密碼文件的程序讀了這樣一個精心構造的“黑文件”之后,就會做出一些“出格”的事情了。
明天見,順便說一下,我一般會在凌晨
1
點左右發文,想坐沙發的同學注意了呦。