很明顯可以看到eax內容爲來自標準輸入的參數的地址,而直接入棧的地址很有可能就是答案。直接kill掉並重新加載bomb,在0x8048b3a處設置斷點,由圖4所示代碼可以看出如果eax爲0則通過。運行並輸入測試字符串:
![]()
圖4 phase_1檢測部分代碼
![]()
圖5 測試字符串運行結果
如圖 $eax==0 , 所以第一個關卡的答案就是
”Public speaking is very easy.” 了。
3. phase_2
觀察phase_2,可以看到如下片段:
![]()
圖6 phase_2部分代碼
入棧eax和edx可以看出是兩個地址,而read_six_numbers可以看出這個關卡的答案是6個數字。所以就用1 2 3 4 5 6 來進行測試,並read_six_number()函數運行前後對參數進行追蹤和對比:
![]()
圖7 進行phase_2測試
圖8 read_six_number()函數運行前
圖8 read_six_number()函數運行後
對比可以看到輸入的6個數字參數被存在以0xffffd0e0開始的24個字節中。接下來有如下代碼:
圖9 phase_2的第一個炸彈點
可以看到-18($ebp)是第一個數字的地址,而判斷語句就是判斷第一個數字是不是1,不是則爆炸。幸運的是測試的數字第一個數字是1,所以可以繼續運行:
圖10 運行到0x8048b73
圖11 對phase_2核心代碼的初步解釋
如圖11所示,由於ebp的值爲0xffffd0f8,把輸入的6個數字看作數組num[6],則可以將所示代碼總結爲以下C代碼:
圖12 phase_2的等價形式
根據圖10 和11推斷出第一個數字一定是1,根據循環算出後面的數字分別是1*2,2*3,6*4,24*5,120*6,即1 2 6 24 120 720。按照推斷出的答案進行測試,在bomb_explode函數處設置斷點,如果錯誤就可以直接Kill重新運行而不會爆炸:
圖13 測試phase_2
可以看到並未觸發斷點,則結果正確。
4. phase_3
首先當然還是先觀察phase_3的彙編代碼,看是不是調用了有關輸入的參數的函數:
圖14 phase_3參數的讀取部分
首先,將三個地址參數入棧,其中的數據大小爲32位,4位,32位,則中間一個參數爲單個字符型。在sscanf函數調用後檢查$eax,因爲sscanf在參數匹配成功後會將匹配成功的參數的個數放入eax中返回,所以檢查eax是否大於2,即至少應匹配三個參數才能過第一個爆炸點。
圖15 有關其餘兩個參數的調用
從圖15可以看出第一和第三個參數全部都與一個立即數做比較,所以可以推測這兩個32位的參數不是指針,而是數字。且可以看出第一個參數如果大於7會跳轉絕對尋址到bomb。所以可以得出第三個炸彈的答案格式是
小於8的整數,一個字符,一個數字。此外由於sscanf需要一個格式串來作爲匹配,所以通過查看入棧的立即數地址可以得到格式串,即參數的格式:
圖16 查找sscanf()的參數串
此外從整體看phase_3可以看出有9個分段,每個分段都類似於一下片段:
![]()
圖17 phase_3重複片段
且都由一處語句控制跳轉內存:
![]()
圖18 控制跳轉語句
控制語句通過將第一個參數的值放入eax中,通過第一個值控制跳轉,進一步控制第三個參數與哪個數字比較。如果第三個參數匹配失敗就會跳到末尾的explode_bomb()中。所以可以看出是一個switch語句。整理後可以得到以下結構:
圖19 phase_3等價的switch語句
所以可以算出第三個關卡共有8個答案,即從後三行中對應每一列爲一個答案。任選一個答案,在explode_bomb處設下斷點進行測試:
圖20 測試phase_3的答案
未觸發斷點,即成功通過。提示已經過了一半啦。
5. phase_4
觀察phase_4的彙編代碼首部,也調用了sscanf函數。由彙編代碼可以看出參數是一個4字節的參數,通過調出sscanf的格式串可以看出是一個整數:
圖21 phase_4中的參數調用
圖22 sscanf的格式串
圖23 對參數的判斷
此外由圖23可以看出參數(存放在 -0x4($ebp)的內存中 )需要大於0。而對$eax的判斷是爲了查看是否成功匹配到一個整數。( $eax是默認sscanf()函數返回值的存儲位置,sscanf()的返回值是匹配成功的參數個數
) 。接下來可以看到將整數入棧後進入func4函數中:
圖24 參數入棧並進入func4()中
接下來就轉戰func4函數,可以看出它是一個遞歸函數。對於遞歸函數首先要找到特殊出口:
圖25 特殊出口
圖26 phase_4對func4()返回值的判斷
可以看到當$ebx也就是輸入的參數小於1時直接返回1。觀察phase_4函數的部分( 圖26 )可以看出返回值應該等於55(0x37)。所以還是要研究一下遞歸部分。根據以下代碼可以看到遞歸部分有兩層,且都要進行:
![]()
圖27 func4的遞歸部分
不過代碼較短又只涉及到輸入的參數,所以可以直接看出等價以下C代碼:
圖28 模擬func4的遞歸代碼
經過計算,想要返回值爲55,則i應該爲9。測試一下結果的正確性( 還是要在explode_bomb()處設置斷點,否則錯誤就會bomb,然後就會扣分啦。。) :
圖29 phase_6測試結果
因爲沒有觸發explode_bomb斷點,所以phase_4就這樣被KO啦。
6. phase_5
根據上面四關總結出的套路,首先還是觀察phase_5首部:
![]()
圖30 phase_5首部
由入棧參數可以看出是一個4字節變量,因爲調用了string_length,所以推斷是一個字符串的地址。而$eax應該是string_length的返回值,因爲phase_5並未給$eax賦值。根據判斷語句可以得出參數應該是一個長度爲6的字符串。接下來是第一個循環:
圖31 phase_5循環體
首先還是要用gdb看一下所涉及到的參數都有什麼信息,在explode_bomb() 和 phase_5() 處設置斷點,用字符串"abcdef" 來測試一下:
圖32 phase_5第一個循環體的參數及內存觀察
如圖,在mov $0x804b220, %esi 之後進入循環,而在立即數地址參數中存放着一個字符串,在循環中的參數$ebx則是輸入的字符串參數。由於循環過程中沒有explode_bomb的入口,不怕直接被炸掉,所以先不探究循環過程,繼續運行,看一下什麼時候會進入explode_bomb()函數:
圖33 在進入strings_not_equal()後的參數變化
可以看到strings_not_equal() 函數,此時已經跳出循環。這個函數在第一個關卡已經出現過,作用是判斷兩個參數字符串是否相等,檢查兩個參數,其中0x804980b中存放的應該是不會變的標準串,而$eax存放地址中存放的串是循環後得出的串,很明顯不對。所以kill掉。由於圖32看到了第一個循環前準備的參數,所以直接來分析彙編代碼:
圖34 分析phase_5的循環代碼
如圖,在循環中,取輸入串的每個字符的ascii碼轉換成二進制的低四位,拓展成32位整數後作爲偏移量在模式串中取對應位置的字符,存放在0xffffd0e0中。當最後的字符串爲”giants”時即可。根據計算,opukma符合此條件。同事可以看出答案不止一個,只要符合就可。接下來用計算出來的字符串進行測試:
圖35 phase_5測試
可以看出並沒有觸發explode_bomb斷點,即搞定啦。
7. phase_6
接下來問題來了,這麼長的phase_6,什麼套路都要崩潰了。。因爲比較長,所以先總結一下:參數讀取,一個雙層循環,三個單層循環,且炸彈入口只出現在雙層循環前的參數準備和最後一個循環中。分了這幾部分後就不會有無法入手的感覺了。接下來就來挑戰一下吧:
首先是讀取參數:
圖36 phase_6讀取參數部分
可以看到給read_six_numbers的是兩個參數,根據mov和lea指令可以看出$edx是一個四字節數值,$eax是一個地址,而讀入的應該是6個數字,所以用1 2 3 4 5 6進行測試如下:
圖37 測試phase_6並查看參數變化
可以看到$eax由地址變成了數字6,所以應該是在read_six_numbers時被當作參數返回的值。可以看到原來$eax保存的地址的內容已經變成了輸入的6個數字。繼續觀察接下來的彙編代碼,就到了雙層循環部分,即如下圖所示代碼段:
圖38 雙層循環部分代碼
根據圖37觀察到的內存等信息,再結合判斷條件和調用方式等順序可以做出等效C代碼如下圖,其中一層解釋更像源代碼,二層更加簡單粗暴加直接:
圖39 雙層循環的遞進分析
可以看出這段循環的作用就是規範輸入的6個數字:最大爲6各不相等。又因爲圖中所表示的比較爲無符號比較,所以這6個數字也不能有0。綜上即1-6。數字是確定的,所以唯一做評判標準的應該就是順序了。觀察接下來的一部分彙編代碼並用gdb觀察變量和內存的變化:
圖40 下一個循環前的準備
圖41 準備階段的相關內存的分析
可以看到$eax $ecx 和 0xffffd0bc 處的內容被更改了。接下來進入下一個循環:
圖42 第一個單循環的代碼
可以看到涉及到的最小的內存是($ebp-0x3c),由於多爲內存變化,根據彙編代碼無法看出內存變化,所以用gdb查看一下循環前後的該處內存變化:
圖43 相關內存的變化
可以發現在0xffffd0c8 – 0xffffd0dc的24個內存發生了變化。而0xffffd0e0 – 0xffffd0f4是輸入的6個數字所在的內存。根據變化結果和彙編代碼,通過計算可以得到如下總結:
圖44 第一個單層循環解析
根據對整個單層循環的單步調試,對過程中所操作的內存的信息進行查看:
圖45 46 相關內存的查詢
根據以上信息,可以大概總結出如下內存和尋址的模式:
圖47 尋址方式總結和內存變化
這個循環基本就搞清楚了,接下來是下一個單層循環,同樣由於多爲內存操作,只能先用gdb單步運行來查看內存的變化:
圖48 49 循環中內寄存器和內存的變化
經過觀察發現此次循環過程的內存與上一個循環中被改變的內存有關,經過整理後可以發現如下內存變化:
圖50 第三次循環執行完畢相關內存變化
可以看到這次循環是根據上一次循環更改的相關內存進行第二次的修改,間接與輸入的6個數字有關。接下來是phase_6的最後一個循環:
![]()
圖51 phase_6最後一個循環
炸彈現在纔出現,可以看出來是內存間的訪問和比較,根據圖47 和圖50可以得到下面的流程模擬圖:
![]()
圖52 最後一個循環的運行模擬和參數推算
可以看到這個循環是在測試根據輸入的6個數字排好序的地址中存放的數字,輸入的6個數字應該使 0x804b230– 0x804b26c 這6個地址中的數字按照降序排列,而輸入的6個數字是在圖47所示的循環過程中控制6個地址在
0xffffd0c8 – 0xffffd0dc 中的順序 進而通過圖50所示過程控制6個地址存放在以0x804b開始的地址。所以正確輸入順序的推算應是
4 2 6 3 1 5 。如下圖所示:
圖53 推算正確結果
接下來就要測試一下推斷出的結果了,如下圖把phase_6的所有explode_bomb()的入口語句設置斷點,並在每個循環階段設置斷點:
圖54 設置斷點並輸入推算答案測試
圖55 第二個循環階段完成
如上圖,第一階段未觸發explode_bomb()函數,二階段後檢查內存發現在0xffffd0c8 -- 0xffffd0dc段的地址排列正像推算的一樣。
![]()
圖57 第三階段運行後檢測其他相關內存
第三階段後,根據圖57和58可以得出下圖所示的圖示:
![]()
圖58 內存圖示和比較順序推算
最後運行後顯示成功拆除炸彈並退出程序:
![]()
圖59 提示成功拆除炸彈並退出
8. phase_defused()
在研究phase_1 - phase_6時發現,在彙編代碼中每個關卡通過後都會進入phase_defused()中,且在此函數中還有一個secret_phase() 函數的入口。所以來分析一下phase_defused()函數:
圖60 phase_defused()入口語句
首先還是要觀察一下0x804b480這裏存的是什麼。所以在phase_defused()函數入口加斷點運行一遍:
圖61 觀察0x804b480的變化
可以看到隨着關卡的通過,0x804b480存儲的值在不斷增1,到第六個關卡時爲6,但並未進入secret_phase(),所以單步運行看一下哪裏出了問題:
![]()
圖62 異常跳過secret_phase()入口
可以看到是因爲$eax的值不對導致跳過入口,由於$eax作爲sscanf()函數的返回值,所以推算需要匹配兩個部分,但在sscanf()運行過程中並未手動輸入新的字符串且sscanf()的參數不一定來自標準輸入,所以需要查看之前入棧的參數來推算sscanf()的參數有哪些。
![]()
圖63 查找sscanf()的參數
可以看到模式串是數字+字符串,而收到的匹配串是一個數字9,由於並未手動輸入新串,所以排除是程序自帶的字符串,所以觀察6個關卡的答案,發現第四關的答案是9,而且第四個關卡的參數串是%d,所以有多餘的字符串也不會影響匹配的個數,所以推測需要改變第四個答案。在第四關時多輸入任意一個字符串:
![]()
圖64 測試任意字符串
![]()
圖65 通過參數匹配測試
![]()
圖66 再次異常跳轉
可以看到在輸入任意字符串後,在參數匹配個數部分已經成功,但在單步執行到strings_not_equal()時由於返回的參數$eax不爲0,所以再次異常跳轉,所以觀察入棧的兩個參數,發現一個字符串是標準輸入的字符串,另一個是絕對地址存放的字符串,所以推定應該在第四關輸入的字符串就是該處地址顯示的字符串,即
“9 austinpowers”。用推算的答案進行測試:
![]()
圖67 測試推算的答案
![]()
圖68 成功進入secret_phase()入口
可以看到已經成功運行到了secret_phase()入口處。
9. secret_phase()
.首先觀察入口部分的代碼:
![]()
圖69 入口部分代碼
可以看到有兩次調用函數,第一次是read_line(),從而可以推斷出此關卡應該是一個字符串作爲參數。用gdb單步調試觀察第二個調用函數的參數:
![]()
圖70 第二個調用的函數參數
可見是剛纔輸入的字符串,繼續運行:
![]()
圖71 發現錯誤
由於在調用第二個函數後$eax已經發生改變,所以$eax的值應該是該函數的返回值,由 cmp語句可知返回值應該爲0x3e8+1,爲1001。再觀察第二個函數的解析符號,爲<__strtol_internal@plt>,所以推測應該爲 strtol()函數。strtol()函數的參數原型爲long
int strtol(const char *nptr,char **endptr,int base) ,作用是將一個字符串轉換成一個長整數,又因爲參數 0xa 被入棧,所以推測這個函數是將輸入的字符串轉換成十進制長整數賦給$eax作爲返回值,且jbe 8048f14<secret_phase+0x2c> 表明 1000>=$eax-1>=0 ,所以初步推定輸入的字符串應該是1-1001的任意一個數字。
以數字1作爲字符串進行測試:
圖72 以 1 做參數進行測試
![]()
圖73 成功通過並進入fun7()
可以看到已經成功通過,並運行到fun7()函數入口,對參數進行分析發現是輸入的數字和0x24。由於除了參數匹配的檢測fun7()中沒有其他的爆炸帶你,所以先直接完成fun7()函數回到secert_phase()函數,發現fun7()返回的值未通過測試:
![]()
圖74 fun7()函數返回值錯誤
觀察fun7()的彙編代碼:
![]()
圖75 一種完成fun7()的方式
由圖75可看到如果傳入的參數$edx爲0,則直接完成函數並返回$eax,此時返回值$eax爲0xffffffff = -1 ,gdb測試觀察$eax和$edx的值:
圖76 查看fun7()參數
可以看到$edx的值是傳入的地址,所以首次進入時不會發生edx=0的情況。所以繼續觀察fun7()其他代碼發現又是一個雙層遞歸,但屬於分支情況,分析可得如下圖所示C模式:
![]()
圖77 fun7()的三分支模式
根據三分支模式在分支處設置斷點並運行測試:
![]()
圖78 設置斷點並測試
![]()
圖79 根據三分支模式尋找遞歸返回點
如圖,由於fun7()是三分支模式,
除相等時直接返回0外,只有在傳入地址參數爲0時纔會返回-1。所以
查找所有可以遞歸的地址點,直到某個地址點的左右分支均爲0則爲結束點。由上圖可知該遞歸模式是四層二叉樹遞歸模式,則有如下的二叉樹形態:
![]()
圖80 內存的二叉樹形態
由於secret_phase()判斷返回值應爲7,所以根據三分支的返回值公式,有且僅有7 = ((0*2+1)*2+1)*2+1。所以應該爲
$eax > 0x24 且$eax>0x6b 且 $eax==0x3e9,則輸入的數字應該爲0x3e9,即1001。strtol()函數在轉換時會捨棄無法轉換的部分,所以1001後可接第一個字符不爲0-9的其他字符串。
根據推算的結果輸入字符串進行測試:
![]()
圖81 測試secret_phase()
由圖81可以看到並未觸發secret_phase()和fun7()內的explode_bomb()入口,即破解成功。
結尾
從破解過程來看,需要至少要 "還可以" 的水平的彙編代碼閱讀能力,還要不怕麻煩的對許多地址進行遞歸的查詢,很多時候對於 “查詢哪個地址可以看到需要的東西” 的問題只有在大致的範圍上不斷尋找和計算才能準確找到能完美表現變化的地址。所以還是需要很大的耐心的。由於不需要大量的指令,只要懂得gdb的基本使用和查看內存的指令並且耐心調試就可以,所以這篇文章並沒有大量的代碼段,一切都可以在親自實踐中搞定。