逆向工程【二進位制炸彈】
二進位制炸彈任務描述
"二進位制炸彈包含若干個階段,每個階段需要輸入特定的字串,所有輸入正確則炸彈被排除,否則….."
拆彈的任務也就是找出這些字串將字串記錄到solution.txt檔案中,用換行區別不同階段的字串,
Linux環境下可按下列方式驗證拆彈結果:
主要方法
objdump反彙編與gdb除錯。
分析流程
已知資料有編譯好的二進位制可執行檔案bomb,也就是反彙編目標檔案,以及bomb.c檔案,用於輔助理解程式碼。
檢視bomb.c可知程式利用phase_*函式(*為1~6) 檢查輸入字串是否合法,不合法就引爆炸彈。
bomb.c沒有給出phase的原始碼,我們實際的任務就是逆向出每個phase的檢查規則,構造出合法字串。
首先使用 反彙編得到bomb的彙編檔案,
以下將逐個分析phase1-phase6。
phase1【字串對比】
<phase_1>彙編段前三行申請了8位元組棧空間,
movl指令在高4位元組存入了0x80499a8處的一份資料,隨後的兩條mov指令將我們提供的phase1的引數存入了棧的低4位元組,
緊跟著一條call指令,跳轉到<strings_not_equal>處,顯然是在進行字串對比,
通過分析我們可以得到,phase_1實際上是將輸入引數與0x80499a8處儲存的資料做對比,
在彙編檔案中,資料段的內容並沒有包含,所以我們需要通過gdb斷點輸出該處的儲存資料,
斷點處輸出可以得到0x80499a8處的資料,即phase_1的字串答案I turned the moon into something I like to call a Death Star.
phase2【迴圈數字】
從8048bbe行可以看出,phase_2的執行與讀取6個數字有關,那麼輸入也應該是6個數字。
從8048bc3、8048bf0、8048bf3行可以得到phase_2的執行,進行了3次迴圈,也就是6個輸入數字進行了3次對比。
有一個誤區是,一開始去分析了read_six_numbers段的彙編碼,沒有得到什麼有用資訊,
由此可知後面對phase的分析,主要基於phase段的程式碼,其中涉及到的一些子操作只要按照字面意思理解即可。
分析迴圈語句中的8048bcc到8048bdd行,可以看到edx暫存器先從棧中讀取了一個數字,
而後eax暫存器在索引+3(8048bd6行)的情況下,也從棧中讀取了一個數字,
之後edx暫存器與eax暫存器進行了對比。
從已經分析到的資訊可知,phase_2輸入6個數字,對比只迴圈了3次,而對比的兩個數字都在棧中(與phase_1不同)且下標相差3,可以猜測phase_2是將使用者輸入的6個數字中的前3個數字與後3個數字做了對比,下面通過gdb進行除錯:
為了避免巧合,測試資料將數字設定成兩組五位數,phase_2通過,證明phase_2確實是在對輸入的數字進行索引加3的對比檢查。
phase3【case分支】
有了phase1和phase2的經驗,直接從8048c36行開始分析,根據<sscanf@plt>可以得到,phase_3的引數應該是呼叫了C語言中sscanf函式來輸入,sscanf讀取格式化的字串中的資料。隨後的mov、cmpl、jg、call四條指令中出現了對比操作與<explode_bomb>的呼叫,顯然是在對輸入資料進行分析判斷。cmpl語句將暫存器eax與常數1進行對比,若不大於1則爆炸,而eax暫存器中存放的是上一次操作的返回值,檢視sscanf函式定義可以得知, 返回值為int型別,返回的是讀取到的有效資料個數。
接下來,測試phase_3需要的引數個數,首先給兩個輸入引數:
得到sscanf函式返回值為2,接下來我們再給三個輸入引數:
給了三個輸入引數,但sscanf函式返回值仍然為2,由此可知,sscanf函式只讀取兩個格式化字串,也就是phase_3需要兩個輸入引數。
8048c49行將輸入的引數存入eax暫存器,用gdb除錯可以得到存入的是第一個輸入引數,引數1與常數7進行對比跳轉,若大於7則跳轉到爆炸指令,可知引數1取值範圍為0-7。8048c5f行根據eax暫存器的內容進行直接定址跳轉,8048c58行在計算跳轉地址時,用到了地址0x80499ec,且以4*%edx作為偏移量,檢視反彙編得到的彙編碼,並沒有0x80499ec行,gdb除錯輸出可以得到:
顯然該段是資料段,儲存了case跳轉表,而跳轉地址正是phase_3程式碼段的8個地址入口。
不同的入口地址,分別給-0x8(%ebp)賦予了不同的常數值。
再讀取引數2,將引數2與case段中的賦值進行比較,相等則通過。
0x3d4=980;0x2a3=675;0x23e=574;0x14a=330;0x1b3=435;0x2e5=741;0x154=340;0x9c=156;
所以有6組可用輸入:0 980; 1 675; 2 574; 3 330; 4 435; 5 741; 6 340; 7 156;
phase4【遞迴求冪】
有了phase3的經驗,可以看出phase_4需要一個輸入引數。
test指令判斷輸入引數是否為0,可知輸入引數>0,隨後將輸入引數傳遞給func4函式並執行。
func4執行的返回值與常數$0x1cb91(117649D)對比,相等則通過,可以得知func4是對輸入引數進行了運算,且運算結果應該等於111649。接下來分析func4函式,反推輸出111649的輸入值應該是多少。
快速分析func4的功能:func4彙編段中出現了呼叫自身的call指令(0x8048cd9),顯然func4函式是在進行遞迴運算。
再尋找遞迴結束條件,發現遞迴出口在0x48cd0行。每層遞迴引數值-1,最後一層遞迴返回1後,彙編程式碼執行到0x8048cde,該處彙編程式碼對下一層遞迴的返回值進行7倍乘,然後將值返回給上一層,由此可以推匯出func4的C程式碼——7的n次冪。
log(7,117649)=6,phase_4輸入6通過。
phase5【靜態連結串列】
彙編碼前一部分已經很熟悉了,可以看出phase_5的輸入大於1個引數,下面用3個引數去測試,若sscanf返回值不等於3,則需要2個引數。
phase_5的輸入引數確實是兩個。
通過gdb除錯,可以得到phase_5還是對引數1進行操作。
直接找到彙編碼的主體,jne返回呼叫8048d98明顯是迴圈,迴圈體之前是在做一些初始化操作。
迴圈判斷條件是用返回值eax(也就是-0x14(%ebp))與常數15對比,而incl指令每次迴圈自增,在迴圈體內並沒有其他地方使用該計數變數。
所以此處邏輯應該是類似while迴圈的迴圈體,當滿足eax等於15則跳出迴圈。
先不分析迴圈內部,當跳出迴圈之後,cmpl指令將迴圈計數變數與10對比,等於10則通過,說明前面的迴圈體應該在第10次時使eax第一次滿足等於15的條件跳出。
繼續往下觀察彙編碼可以發現,當-0xc(%ebp)與-0x18(%ebp)相等時則phase_5完全通過,-0x18(%ebp)是我們輸入的第二個引數(通過讀之前的彙編碼或gdb可以看出來),而-0xc(%ebp)在0x8048dab處出現過,在迴圈的每次都做了+=操作。
綜合以上分析,得以得到,迴圈主體做的工作應該是,每次迴圈都對-0xc(%ebp)處引數進行+=操作,當-0x14(%ebp)等於15則跳出迴圈。而我們需要滿足的條件是,迴圈應該進行10次,且最終累加得到的-0xc(%ebp)應該等於我們輸入的第二個引數。那麼,我們輸入的第一個引數應該是什麼範圍呢?
往回分析彙編碼,可以看到0x8048d7c行有一個and指令,效果是%16,而運算元正是-0x14(%ebp),這個運算元很眼熟,正是迴圈跳出需要等於15的那個數。說明,我們輸入的第一個引數,在迴圈體中會被賦予新的值。(直接分析最上面幾行初始化的彙編碼也可以得到第一第二引數,這裡沒有使用該方法。phase_6也沒有分析,事實證明分析粒度太細會讓事情變得很複雜,沒有直接分析邏輯並代入來的快)
到這裡我們已經得到了除迴圈內部以外所有資訊,接下來再分析迴圈內部會簡單很多。
在0x8048d9e行出現了 ,地址結構是線性儲存,首地址是0x804a5c0,偏移4*eax。而eax又是等於一直在變化的引數一,結構又與連結串列有關(以上一結點的值作為當前結點的索引),所以迴圈體內部應該是在進行靜態連結串列的遍歷操作,列印0x804a5c0地址為首的16個數(因為引數一%16有16個值)驗證猜測:
輸出值的範圍正好遍佈0-15,且從第17個數開始明顯超出了合理數值,證實了靜態連結串列的猜想。
假設引數一%16等於6,那麼迴圈一次引數一就變成了15,若引數一%16等於14,則迴圈兩次變成15。
那麼拆除phase_5的任務就變成了,從誰開始索引,需要索引10次才使引數一變成15,且引數二等於迴圈過程中(不包括初始值)所有引數一的累加和。列表推導一下,可以得到初始引數的低八位應該等於13,迴圈十次累加和為69。
phase6【尋找靜態連結串列的降序索引】
phase6的分析踩了不少坑,一開始去直接分析彙編碼,但是彙編碼很長而且到處跳轉,實在進行不下去。之後又去gdb除錯,過程中出現了不少變數,也越查越亂。最後去看了下之前幾次拆除炸彈一直忽略的初始化部分,發現phase_6設定了72位元組的棧,說明phase_6中用來操作的變數比較多。最後索性代入引數逐步除錯,直接將棧打印出來,這樣有了輸入和輸出分析中間的過程會簡單一點。
程式碼主要分為四段,下面逐步分析:
【第一部分】
根據圖示,大體結構是進行了兩重迴圈,迴圈操作的物件是線性結構儲存的一系列引數,通過gdb除錯或者程式碼分析可以得出實際上就是我們輸入的六個數值。每層迴圈下標只索引到5,迴圈體的內層迴圈是在將外層迴圈索引的值與後面索引的值逐個對比。
再看一下第一部分需要我們滿足的條件,首先外層迴圈要求 0<輸入引數<=6,記憶體迴圈要求從引數1開始,每個引數都要與其後的引數不同。也就是6個輸入引數要兩兩不同,且在1-6之間,那麼只能是1,2,3,4,5,6的某一種排列。
【第二部分】
還是兩層迴圈,這裡外層遍歷每一個輸入引數,輸入6 5 4 3 2 1引數,通過gdb除錯,發現經過第二部分後棧內資料發生了以下變化:
多出了六個地址,每個距離相差0xc,gdb輸出一下這六個地址對應的值:
彙編碼0x8048e68行與0x8048e74行表示,每次迴圈都在通過-0xc(%ebp)對那六個地址賦值,最後gdb輸出的-0xc(%ebp)也確實與六個地址值的最後一個相等。對彙編碼繼續分析,或者改動六個數字的排列方式按照上面的方式將棧列印,最終可以得到那六個地址值與1-6是一一對應的:
6—0x0804a600=274,5—0x0804a60c=920,4—0x0804a618=100,
3—0x0804a624=313,2—0x0804a630=586,1—0x0804a63c=125。
所以,第二部分的作用就是根據輸入的六個引數,給出一組對應順序的地址,地址指向6個數字。
【第三部分】
程式碼執行過第三部分之後,棧內資料沒有任何改變,而且第三段也不存在指向爆炸的跳轉,直接去分析第四部分。
【第四部分】
迴圈執行5次,1處彙編碼是在對比兩個線性儲存的數字,也就是之前第二部分新增的那六個中的數字。
2處的操作和phase5的靜態連結串列索引很像,每次加0x8向後索引。
8048ee0行的跳轉指令又顯示,每次迴圈需要當前索引值大於等於後一個索引值。
到這一步,我們已經有了足夠的資訊,可以猜測第三部分應該是在對第二部分獲得的六個數初始化索引連結,而第四部分驗證的要求是,前一個索引數要大於後一個索引數,也就是125、586、313、100、920、274降序排列。而我們要給出的,則是降序的索引。總結下來,phase_6的功能為,當程式按照我們輸入的六個引數去生成六個對應的地址時,這六個地址對應的值要滿足降序排列。所以答案為:5 2 3 6 1 4。
至此程式需要的六個輸入我們都已經分析出來,炸彈也成功拆除。
----------------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------------