CSAPP:二進制炸彈實驗

二進制炸彈是作爲一個目標代碼文件提供給學生們的程序,運行時,它提示用戶輸入6個不同的字符串。如果其中任何一個不正確,炸彈就會“爆炸”:打印出一條錯誤信息。學生通過反彙編和逆向工程來確定是哪六個字符串,從而解除他們各自炸彈的雷管。該實驗教會學生理解彙編語言,並強制他們學習怎樣使用調試器。

對這個實驗慕名已久,做了一下不禁感嘆:果然牛x,不愧是從美國進口的!

因爲提供的二進制炸彈是根據特定的平臺而生成的,所以必須在”官方“提供的服務器上進行拆彈,我們一般用putty登錄。在這裏,我沒有研究怎麼搞putty,而是直接找別人的炸彈研究一下,理解彙編指令即可,後面有空,在慢慢學習gdb調試!

首先來看phase1 的代碼:

分析解答:

注意strings_not_equal 這個函數,從字面上可以猜想地理解爲:把輸入的字符串和內存某處字符串相比較,不相等的時候函數返回值的爲1。再看接下來兩句:

test %eax,%eax
je 8048bb2 <phase_1+0x22>

test 是把兩個操作數進行與運算,而通常這兩個操作數是一樣的,此操作的意義就在於影響標誌位,當%eax 爲0 時,零標誌位置1,否則零標誌爲0。而從je指令可以知道當%eax 爲0 時程序會跳過接下來一句對爆炸函數的引用,所以我們的目標就是要是%eax 即strings_not_equal 的返回值爲0,即要是輸入的字符串與內存某處的存放的字符串相等。於是現在的關鍵就是找出那個字符串是放在內存的哪個地方。其實非常明顯,$0x80498c0 這個地址在程序裏實在太顯眼,用x/s 0x80498c0 命令一查,果然那裏存放有一句話:"I am not part of the problem.I am a Republican."於是run ,輸入,果然:Phase 1 defused. How about the next one?


然後來看phase2:

分析解答:

注意到read_six_numbers 這個函數,同樣故名思義我先猜他爲輸入6 個數字。暫且不看read_six_numbers 函數的內容,先注意8048bcf 這句,cmpl 顯然是要引起我高度注意的,因爲比較的結果往往直接關係到爆不爆的問題。這條指令的內容很明確,就是看0xffffffd8(%ebp)處的數字是否爲1,不是的話就會調用爆炸函數。於是我非常堅定地認爲1 便是我該輸入的第一個數字,而輸入的數字是存放在0xffffffd8(%ebp)開始的地址處的。

接着看下去,lea 0x1(%ebx),%eax 這句將%eax 置爲2。8048be2 這句中的0xffffffd4(%ebp,%ebx,4)這個存儲器操作數稍作思考便可以知道其實就是0xffffffd8(%ebp),即是我們要輸入的第一個數字,因爲此時%ebx 爲1。所以這句是將第一個數字乘以2 放入%eax。8048be7 又來cmp 了,很容易看出0xffffffd8(%ebp,%ebx,4)是我們輸入第2 個數字的地方,還是因爲此時%ebx 爲1,所以這裏告訴我們,要輸入的第2 個數字就是第一個乘以2 的結果。

8048bf2 和8048bf3 兩句告訴我們從%ebx 等於1 到5 分別進行以上兩段的操作,即後面一個數字由前面一個數字乘以%eax 得到,而每次的乘數%eax 爲%ebx 加1,即乘數分別爲2,3,4,5,6,所以可以確定這六個數字分別爲1,2,3,4,5,6 的階乘,即1 2 6 24 120 720。於是輸入,果然:That's number 2. Keep going!


來看第3 個bomb:

分析解答:

Bomb3 的代碼就比較長了,先抓住重點:jmp *0x8049938(,%eax,4)這句話讓我想起了switch 語句。再翻翻書,發現這段代碼就是一個跳轉表結構。從語句上便可以看出備選的跳轉地址存放在0x8049938 開始的地址處,通過%eax 的值來選擇。通過打印0x8049938 處的16 進制數可以確認:
(gdb) print /x *0x8049938
$3 = 0x8048c49
(gdb) print /x *0x804993c
$4 = 0x8048c54
(gdb) print /x *0x8049940
$5 = 0x8048c5f
(gdb) print /x *0x8049944
$6 = 0x8048c67
(gdb) print /x *0x8049948
$7 = 0x8048c72
(gdb) print /x *0x804994c
$8 = 0x8048c7d
(gdb) print /x *0x8049950
$9 = 0x8048c88
(gdb) print /x *0x8049954
$10 = 0x8048c93

switch轉換表是這樣的:


可以看到這8 個16 進制數正好是程序中的8 個地址(都用黑體標出),對應於%eax爲0 到7 時的跳轉地址。由於

cmp $0x2,%eax
jg 8048c39 <phase_3+0x3b>


兩句限定了%eax 大於2,所以我取%eax 爲3,然後查表到8048c67 處。

mov$0x6e,%bl 和cmp 0xfffffff7(%ebp),%bl 告訴了我們0xfffffff7(%ebp)處應該輸入的值,cmpl $0xcb,0xfffffff8(%ebp)與je 8048ca7 <phase_3+0xa9>告訴了我們0xfffffff8(%ebp)處應該輸入的值,然後我很自然的把0x6e 和0xcb 換算成了10 進制數110 和203,然後迫不及待地輸入3 110 203,結果很失望的到了break point1。
這裏我困惑了挺久,我還試了16 進制的輸入,甚至懷疑自己整個的理解是否有問題。終於我發現了一個問題:第2 個“數字”的地址爲0xfffffff7(%ebp)而第三個爲0xfffffff8(%ebp),顯然這是不合常理的,不可能只佔一個字節。而只佔一個字節的東西,我就只能想到字符了,但明明分析程序看到的是一個“數字”,要把數字與字符聯繫起來,就是ASCII 碼了!查表,6e 果然對應着一個字母:n於是迫不及待地輸入:3 n 203,果然Halfway there!
當然對應於不同的第一個數字,有不同的答案。

事後我還發現, 語句8048c1c: c7 44 24 04 26 99 04 movl$0x8049926,0x4(%esp)中的$0x8049926 處的字符串:

(gdb) x/s 0x8049926
0x8049926 <_IO_stdin_used+514>: "%d %c %d"


原來對輸入的格式早有說明!

 

第4 個bomb:

分析解答:

 

首先要研究下func4 的功能。
容易看出是一個遞歸函數。lea 0xffffffff(%esi),%eax 是得到%esi-1 的值然後調用func4,同樣lea 0xfffffffe(%esi),%eax 是得到%esi-2 的值然後調用func4,lea (%eax,%ebx,1),%eax 即是將f(%esi-1)的返回值(在%ebx 裏面)與f(%esi-2)的返回值相加放在%eax 中作爲func4 的返回值,很明顯這是一個斐波那契數列的函數。

 

細節研究可知:

func4(0) = 1,func4(1) = 1;

func4(2) = func4(1) + func4(0) = 2;

func4(3) = func4(2) + func4(1) = 3;

func4(4) = func4(3) + func4(2) = 5;

...

func4(10) = func4(9) + func4(8) = 89;


明白了func4 的意思,我直插phase4 的心臟:

cmp $0x59,%eax je 8048d3b <phase_4+0x45>
很清楚,當%eax 等於0x59 的時候就可以過關了。%eax 是什麼?是call 8048cb8 <func4>後的返回值!那此次調用的函數參數是什麼呢?就是我們輸入的東西了。8048d11 和8048d16 兩句也說明了輸入的是一個數字。然後把斐波那契數列一排,0x59 對應的序號爲10,輸入,果然:So you got that one. Try this one.


看第5 個:

分析解答:

 

首先,call 8048ff9 <string_length>和cmp $0x6,%eax 兩句告訴我們要輸入的是6 個字符。在指令movsbl (%edx,%ebx,1),%eax 中,%ebx 爲6 個字符的起始地址,通過循環增加%edx 的值來依次將這6 個字符的ASCII 碼傳給%eax進行下一步操作。

 

說明一下movsbl (%edx,%ebx,1),%eax具體含義:我們知道,movsbl S ,D的意思是將S(不論寄存器還是內存地址)裏面存放的值的一個最低位字節拷貝出來,設置其高24位爲此字節最高有效位,然後傳送到D中。簡單描述就是傳送符號擴展的字節。因此,此指令的含義爲將%ebx+%edx裏面的內容(其實應該爲一個地址),把這個地址中的值得最低位字節符號擴展,然後傳送到%eax中。

與movsbl相對的是movzbl,它是0擴展,其餘與 movsbl 一樣。


研究下這句:movzbl 0x804a5c0(%eax),%eax。

說明一下這個格式0x804a5c0(%eax),它的原型爲Imm(Eb),操作爲M[Imm+R[Eb]],表示爲(基址+偏移量)尋址,它很容易讓我們聯想到數組的操作。
剛纔已經說了%eax 是我們輸入字符的ASCII 碼,經過and $0xf,%eax 處理後也就是相應ASCII 碼的低4 位。查看0x804a5c0 處存放的東西:
(gdb) x/s 0x804a5c0
0x804a5c0 <array.0>: "isrveawhobpnutfgs/001"
0x804a5c0(%eax) 的尋址方式說明了這條指令是將字符串"isrveawhobpnutfgs/001"中的第%eax 個字符傳送給%eax,%eax 起到了一個索引的作用。循環6 次以後,以輸入的6 個字符的的ASCII 碼低4 位爲索引得到的字符串"isrveawhobpnutfgs/001"中的6 個字符,被裝入0xffffffe8(%ebp)爲起始地址的連續存儲空間中。
接着可以看到8048d8a 處調用了strings_not_equal,8048d7c 處清楚地說明了比較的對象,查看0x804992f 處的字符串:
(gdb) x/s 0x804992f
0x804992f <_IO_stdin_used+523>: "giants"
說明我們只要使索引得到的字符串爲"giants"就可以了!
反推回去,g、i、a、n、t、s 對應的索引值爲15、0、5、11、13、1。所以只要使我們輸入的6 個字符的ASCII 碼低4 位依次爲15 0 5 11 13 1 就可以了。
我取爲o0ekma,於是:Good work! On to the next...

 

當然,這裏的答案不是固定的,只要滿足要求即可!

 

第六個:

第六個炸彈代碼太長,應該步步調試來獲取答案,下面簡述一下gdb調試

gdb調試:
作爲調試,我覺得最重要的就是要搞清楚如何單步調試,接下來區分step、stepi和next、nexti
1,step和next是對c源程序進行調試,每步一行。而stepi和nexti主要對彙編指令進行調試,每步一個指令語句。
2,step和stepi是遇到調用函數則會進入調用函數裏面單步執行,而next和nexti遇到調用函數則不進入,跳過繼續執行本函數的下一行或下一指令語句。
3,對彙編進行調試時,若需要到達調試點有三種方式:step、next以及continue,以coninue使用最多。

GBD常用命令

1.啓動GDB
你可以輸入GDB來啓動GDB程序。GDB程序有許多參數,在此沒有必要詳細介紹,但一個最爲常用的還是要介紹的:如果你已經編譯好一個程序,我們假設文件名爲hello,你想用GDB調試它,可以輸入gdb hello來啓動GDB並載入你的程序。如果你僅僅啓動了GDB,你必須在啓動後,在GDB中再載入你的程序。
2.載入程序 === file
在GDB內,載入程序很簡單,使用file命令。如file hello。當然,程序的路徑名要正確。
退出GDB === quit
在GDB的命令方式下,輸入quit,你就可以退出GDB。你也可以輸入'C-d'來退出GDB。
3.運行程序 === run
當你在GDB中已將要調試的程序載入後,你可以用run命令來執行。如果你的程序需要參數,你可以在run指令後接着輸入參數,就象你在SHELL下執行一個需要參數的命令一樣。
4.查看程序信息 === info
info指令用來查看程序的信息,當你用help info查看幫助的話,info指令的參數足足佔了兩個屏幕,它的參數非常多,但大部分不常用。我用info指令最多的是用它來查看斷點信息。
4.1查看斷點信息
info br
br是斷點break的縮寫,記得GDB的補齊功能吧。用這條指令,你可以得到你所設置的所有斷點的詳細信息。包括斷點號,類型,狀態,內存地址,斷點在源程序中的位置等。
4.2查看當前源程序
info source
4.3查看堆棧信息
info stack
用這條指令你可以看清楚程序的調用層次關係。
4.4查看當前的各寄存器值
info registers
5.列出源一段源程序 === list
5.1列出某個函數
list FUNCTION
5.2以當前源文件的某行爲中間顯示一段源程序
list LINENUM
5.3接着前一次繼續顯示
list
5.4顯示前一次之前的源程序
list -
5.5顯示另一個文件的一段程序
list FILENAME:FUNCTION 或 list FILENAME:LINENUM
6.設置斷點 === break
現在我們將要介紹的也許是最常用和最重要的命令:設置斷點。無論何時,只要你的程序已被載入,並且當前沒有正在運行,你就能設置,修改,刪除斷點。設置斷點的命令是break。有許多種設置斷點的方法。如下:
6.1在函數入口設置斷點
break FUNCTION
6.2在當前源文件的某一行上設置斷點
break LINENUM
6.3在另一個源文件的某一行上設置斷點
break FILENAME:LINENUM
6.4在某個地址上設置斷點,當你調試的程序沒有源程序是,這很有用
break *ADDRESS
除此之外,設置一個斷點,讓它只有在某些特定的條件成立時程序纔會停下,我們可以稱其爲條件斷點。這個功能很有用,尤其是當你要在一個程序會很多次執行到的地方設置斷點時。如果沒有這個功能,你必須有極大的耐心,加上大量的時間,一次一次讓程序斷下,檢查一些值,接着再讓程序繼續執行。事實上,大部分的斷下並不是我們所希望的,我們只希望在某些條件下讓程序斷下。這時,條件斷點就可以大大提高你的效率,節省你的時間。條件斷點的命令如下,在後面的例子中會有示例。
當你設置一個斷點後,它的確省狀態是有效。你可以用enable和disable指令來設置斷點的狀態爲有效或禁止。例如,如果你想禁止2號斷點,可以用下面的指令:
disable 2
相應的,如果想刪除2號斷點,可以有下面的指令:
delete 2
7.檢查數據
最常用的檢查數據的方法是使用print命令。
print exp
print指令打印exp表達式的值。卻省情況下,表達式的值的打印格式依賴於它的數據類型。但你可以用一個參數/F來選擇輸出的打印格式。F是一個代表某種格式的字母,詳細可參考輸出格式一節。表達式可以是常量,變量,函數調用,條件表達式等。但不能打印宏定義的值。表達式exp中的變量必須是全局變量或當前堆棧區可見的變量。否則GDB會顯示象下面的一條信息:
No symbol "varible" in current context
8.修改變量值
在調試程序時,你可能想改變一個變量的值,看看在這種情況下會發生什麼。用set指令可以修改變量的值:
set varible=value
例如你想將一個變量tmp的值賦爲10,
set tmp=10
9.檢查內存值
檢查內存值的指令是x,x是examine的意思。用法如下:
x /NFU ADDR
其中N代表重複數,F代表輸出格式(見2.13),U代表每個數據單位的大小。U可以去如下值:
b :字節(byte)
h :雙字節數值
w :四字節數值
g :八字節數值
因此,上面的指令可以這樣解釋:從ADDR地址開始,以F格式顯示N個U數值。例如:
x/4ub 0x4000
會以無符號十進制整數格式(u)顯示四個字節(b),0x4000,0x4001,0x4002,0x4003。
10.輸出格式
缺省情況下,輸出格式依賴於它的數據類型。但你可以改變輸出格式。當你使用print命令時,可以用一個參數/F來選擇輸出的打印格式。F可以是以下的一些值:
'x' 16進制整數格式
'd' 有符號十進制整數格式
'u' 無符號十進制整數格式
'f' 浮點數格式
11.單步執行指令
單步執行指令有兩個step和next。Step可以讓你跟蹤進入一個函數,而next指令則不會進入函數。
12.繼續執行指令
當程序被斷下後,你查看了所需的信息後,你會希望程序執行下去,輸入 continue, 程序會繼續執行下去。
13.幫助指令help
在GDB中,如果想知道一條指令的用法,最方便的方法是使用help。使用方法很簡單,在help後跟上指令名。例如,想知道list指令用法,輸入help list。

發佈了259 篇原創文章 · 獲贊 7 · 訪問量 51萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章