CSAPP實驗-二進制炸彈writeup

0x01 題目概述

二進制炸彈是《深入理解計算機系統》的一個課程實驗。

給定一個二進制文件bomb,及其主程序bomb.c文件,運行二進制文件bomb,一共有6關,用戶需要通過6個輸入來避免炸彈的爆炸。

我們需要通過對二進制文件進行逆向分析,得到能避開炸彈爆炸的合理的輸入。

0x02 涉及知識點

彙編語言的基礎,逆向分析工具的使用。

0x03 實驗環境

Ubuntu16.04LTS,IDA Pro 7.0,gdb

0x04 解題思路

phase_1

  1. 首先讀取了用戶輸入的第一個字符串,保存到rax寄存器中,並通過”mov rdi, rax”將字符串的值賦值給rdi寄存器,作爲後面調用函數phase_1()的第一個參數.

在這裏插入圖片描述

  1. 用gdb運行bomb: gdb bomb

  2. 爲了解出第一個字符串,在phase_1()函數處打斷點: b phase_1, 然後運行程序 r. 接下來隨便輸入一個字符串用於調試, 比如字符串”da”;然後此時進入了phase_1()函數,輸入disass查看其彙編代碼:

在這裏插入圖片描述

  1. 從<strings_not_equal>的名字可知,該函數用於比較兩字符串的值,需要兩個字符串作爲輸入. 兩個字符串不相等的話則返回1,相等返回0,結果保存在eax中. “test eax,eax”用於檢查eax寄存器的值是否爲0:如果eax爲0,則由於zf標誌位爲0,因此執行 “je 0x400ef7 <phase_1+23>”,跳過了調用<explode_bomb>的指令代碼.

因此, 在這裏需要輸入的字符串需要與代碼中用於比較的字符串相同.

  1. 觀察phase_1()函數的彙編代碼,在調用<strings_not_equal>之前事先通過指令”mov esi,0x402400”, 將內存地址爲”0x402400”的值賦值給了esi寄存器,作爲函數<strings_not_equal>的參數.

  2. 查看內存地址爲”0x402400”的值:

在這裏插入圖片描述

  1. 進行測試, 解決phase_1:

在這裏插入圖片描述

Answer1: Border relations with Canada have never been better.


string_length解析

  1. 比較rdi存放的字符串地址的內容是不是’\0’, 是的話則跳轉到0x401332: 將eax賦值爲0; 否則的話:

  2. 把rdi 也就是用戶的輸入字符串賦值給rdx, 然後將寄存器rdx中存放的字符串地址+1, 原來是0x6037800,對應的字符串內容爲”da”(也就是我一開始輸入的測試值),現在變成了0x6037801,對應的字符串內容爲”a”.

  3. “mov eax, edx” 和 “mov eax, rdx”一樣,不過rdx是64位的寄存器

  4. “sub eax, edi” 也就是將eax(地址加了1之後的rdx, 即地址加了1之後的rdi)減去edi, 結果爲1並賦值給eax

  5. 比較此時rdx對應的是不是’\0’,是的話結束循環,否則繼續循環; 下一次循環中eax就會爲2,以此類推

phase_2

  1. 使用gdb進行動態調試, 由phase_2調用的函數<read_six_numbers>可知需要用戶輸入6個數字. 因此, 先隨便輸入6個數字, 比如:”1 2 3 4 5 6”.

執行到下圖的位置時發現有個內存地址, 打印出其中的內容發現是scanf的格式字符串.

可以知道, 需要輸入6個數字, 並且是以空格分隔開的:

在這裏插入圖片描述

  1. 執行完scanf函數以後會有一個判斷, 只要輸入按要求來就不會引爆炸彈. 這裏eax是讀取的數據個數, 如果比5大,則跳轉到地址0x401499, 即不執行<explode_bomb>函數.

在這裏插入圖片描述

  1. 繼續執行phase_2()函數. rsp爲phase_2() 函數的棧頂指針, 由函數調用棧可知, 指向的是第一個參數.

在這裏插入圖片描述

在這裏插入圖片描述

因此, [rsp] 至 [rsp+0x14] 就是用戶傳入的數字參數.

一個數字爲int=4字節. 0-3爲第一個參數, 4-7爲第二個參數, 8-11爲第三個參數,12-15爲第四個參數, 16-20爲第五個參數,21-23爲第六個參數

由於我輸入的是”1 2 3 4 5 6”, 畫出來的參數在函數棧中的表示如下:

地址以及存儲的值
rbp rsp+24
rsp+20 6
rsp+16 5
rsp+12 4
rsp+8 3
rbx rsp+4 2
rsp 1
  1. 接下來在IDA中查看會比較清晰.

rbx-0x4的位置就是rsp的位置,也就是第一個參數的位置; 將[rbx-0x4]指向的數值賦值給了eax後另eax乘以2, 再與第二個參數的值,也就是[rbx]進行比較, 只有相等纔不會觸發函數<explode_bomb>. 因此, 第二個參數的值是第一個的兩倍.

在這裏插入圖片描述

在該次判斷中, 由於rbx+4以後變成rsp+8, 顯然不等於rbp=rsp+24,因此跳到loc_400F17處. 由於此時rbx爲rsp+8, rbx-4爲rsp+4, 因此第三個數爲第二個數的兩倍.

以此類推即可知,每個數都是前一個數的兩倍,這是一個等比數列.

隨便輸入一個等比數列, phase_2解決:

在這裏插入圖片描述

Answer2(答案不唯一): 1 2 4 8 16 32

phase_3

  1. 同樣的方法打斷點, 進行調試.
    可以看到, 輸入應該爲兩個數字, 並且以空格隔開:

在這裏插入圖片描述

  1. 在phase_3()函數中, 先將兩個參數的值分別賦值給寄存器rdx和rcx, 然後調用scanf函數.

  2. 接下來需要注意的地方是, 在箭頭處, 先將 [rsp+18h+var_10] 處的值, 也就是rdx處的值, 即參數1與7比較. 如果比7大, 則跳轉到地址0x400FAD處, 從地址0x400FAD處的代碼可以看到走這個分支的話必然引起炸彈爆炸, 因此第一個參數的值必然小於或等於7.

在這裏插入圖片描述

地址0x400FAD處的代碼:

在這裏插入圖片描述

  1. 接下來, 將[rsp+18h+var_10] 處的值, 即參數1的值賦值給eax, 並跳轉到 “jmp QWORD PTR [rax*8+0x402470] ”, 先假設輸入的參數1值爲2, 那麼跳轉的地址就是0x402480, 接下來是跳轉到0x400F83處:

在這裏插入圖片描述

對eax賦值爲 0x2C3, 跳轉到0x400FBE處

在這裏插入圖片描述

可以看出,參數2的值必須等於eax的值

在這裏插入圖片描述

可以觀察到, 對於參數1的不同取值, 參數2的值也應該與程序中最終對eax賦的值相同纔行.

因此, phase_3 的答案可以測試小於7非負數作爲參數1, 並走通程序來判斷參數2的值.

這裏選擇輸入數據爲: 2 707 , 其中 707 即 0x2C3 可以看到測試通過

在這裏插入圖片描述

phase_4

  1. 同樣的方法打斷點, 進行調試. 和phase_3一樣, 輸入也爲兩個數字, 空格隔開

在這裏插入圖片描述

  1. 可以看到, 參數1必須 ≤ 14纔不會引爆炸彈

在這裏插入圖片描述

  1. 再往下看, 先將參數1的值保存在寄存器edi中.

然後phase_4()函數先調用了func4()函數, 然後判斷func4()函數的返回值(保存在寄存器eax中), 如果eax不是0的話, 則會跳轉到引爆炸彈的地方. 因此函數func4()返回值eax必須是0.

接下來則判斷第二個參數是否爲0, 如果不是0的話則引爆炸彈, 因此第二個參數已經可以確定爲0.

在這裏插入圖片描述

  1. 接下來要做的就是分析func4()函數

在這裏插入圖片描述

框框框住的爲兩個關鍵的條件判斷. ecx<=edi則跳轉, 以及ecx>=edi則跳轉.
接下來有兩種方法

a) 第一種是將彙編代碼寫成高級語言,直接執行. 因爲參數1的範圍已知. 最終結果爲, 參數1的取值可以有:0, 1, 3, 7


# -*- coding:utf-8 -*-

edx = 14
esi = 0
edi = 8  # param 1
eax = 0
ecx = 0


def func4():
    global edx, esi, edi, eax, ecx
    eax = edx
    eax = eax - esi
    ecx = eax
    ecx = ecx >> 31
    eax = eax + ecx
    eax = eax >> 1
    ecx = eax + esi 

    if ecx <= edi:
        eax = 0
        if ecx >= edi:  # ecx == edi
            return eax
        else:           # ecx < edi
            esi = ecx + 1
            func4()
            eax = eax*2 + 1
            return eax
    else:               # ecx > edi
        edx = ecx - 1
        func4()
        eax = eax*2
        return eax


if __name__ == "__main__":
    # for edi in range(7, 15):
    edi = 7
    res = func4()
    print(edi, ": ", res)


b) 第二種方法, 分析程序的邏輯.

可以看到, func4()中有三個條件判斷很關鍵:

從地址0x400fd2到0x 400fdf:

從高地址往低地址逆着推:

ecx = eax + esi
= eax>>1 + esi = eax / 2 + esi
= (eax + ecx) / 2 + esi = eax / 2 + esi # ecx是eax的符號, 這裏都是正數ecx爲0
= (eax - esi) / 2 + esi = (eax + esi) / 2
= (edx + esi) / 2

a) 情況1: 如果ecx > edi,則 400fe6 處代碼將 ecx-1 賦給edx,接着遞歸調用func4函數。eax = eax + eax

b) 情況2: 如果ecx == edi,則將eax賦值爲0並返回。

c) 情況3: 如果ecx < edi,則 400ffb 處代碼將 ecx+1 賦給esi,接着遞歸調用func4函數。 eax = eax + eax + 1

這樣一分析的話, 可以看到這個過程像二分查找. esi初始爲0, 爲左邊界, edx爲右邊界, ecx爲區間的中間值, edi爲參數1.

情況2能保證最後eax爲0;

情況1中會將eax= eax+ eax. 在情況3中, 會將eax = eax + eax + 1. 因此在遞歸過程中不能出現ecx<edi的情況, 如果出現了, 那麼eax =eax * 2 + 1, eax必不等於0.

由於ecx爲區間的中間值, 那麼, 爲了能到達情況2另eax=0, 則參數1也就是edi的值必須是ecx在區間變化過程中的值.

按照程序的邏輯來走的話, 區間變化如下:

[esi, edx] = [0, 14], ecx=7; edx = ecx-1=6; 如果edi==7, 則此時已經滿足情況2的條件;

[esi, edx] = [0, 6], ecx=3; edx = ecx-1=2; 如果edi==3, 則此時已經滿足情況2的條件;

[esi, edx] = [0, 2], ecx=1; edx = ecx-1=0; 如果edi==1, 則此時已經滿足情況2的條件;

[esi, edx] = [0, 0], ecx=0; edx = ecx-1=-1; 如果edi==0, 則此時已經滿足情況2的條件;

因此, 參數1的取值爲: 0, 1, 3, 7, 參數2的取值爲0. 測試通過

在這裏插入圖片描述

phase_5

  1. 由string_length()函數對輸入判斷的返回值可知, 應該輸入長度爲6的字符串

在這裏插入圖片描述

  1. 輸入數據”abcdef”進行測試. 此時地址0x61處也就是寄存器ecx存儲的是97, 也就是”a”, 依次打印0x62爲”b”, 0x63爲”c”.

在這裏插入圖片描述

  1. 從地址0x40108B開始到0x4010AC是一個循環, 當寄存器rax的值不等於6 的時候, 會在地址0x4010AC處跳轉到0x40108B.

  2. 執行完循環以後, 將0x40245e地址處的內容放入esi寄存器中, 打印出來發現是字符串”flyers”. 然後將用戶輸入的字符串放入rdi寄存器, 打印發現此時已經跟我一開始輸入的”abcdef”不是同一個了. 因此, phase_5應該是要構造一個字符串, 使得經過循環以後rdi的值爲”flyers”. 最終, 在函數strings_not_equal()中對esi的內容和rdi的內容進行比較

在這裏插入圖片描述

  1. 接下來來具體看循環裏面的內容. 地址0x4024b0打印出來, 發現是一個字符串, 通過rdx的低四位值來對字符串中的數據進行讀取, 最後存放到edx寄存器中.

那麼rdx值怎麼來的? 溯源上去可以看到是用戶輸入的字符

在這裏插入圖片描述

  1. 然後再保存到[rsp+rax*1+0x10], 因爲rax是從0–5, 因此存放地址就是[rsp+0x10]–[rsp+0x15]

  2. 地址0x4010ae處: 退出循環以後,將[rsp+0x16] 置爲0, 作爲循環生成後字符串的結束標誌:’\0’.

然後地址0x4010b3處將字符串”flyers”放入esi寄存器中,用於後續strings_not_equal()函數的字符串比較

  1. 然後地址0x4010b8處將[rsp+0x10]放入寄存器rdi中, 用於後續strings_not_equal()函數的字符串比較

所以phase_5的解決方案就是:

從字符串”maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?”中, 根據ASCII碼與0xF邏輯與操作得到的後四位, 取出”flayers”.

參考: http://ascii.911cha.com/

目標字符 字符串中的索引 索引對應的二進制 可能的取值 取值對應的二進制
f 9 1001 i 0110
l 15 1111 o 0110
y 14 1110 n 0110
e 5 0101 e 0110
r 6 0110 f 0110
s 7 0111 g 0110

所以, 最終可能的一種答案爲: ionefg

phase_6

  1. 從地址0x401106可以看到, phase_6的輸入爲6個數字, 輸入”1 2 3 4 5 6” 進行測試

在這裏插入圖片描述

在這裏插入圖片描述

則數據從rsp開始存放

地址以及存儲的值
rsp+0x14 6
rsp+0x10 5
rsp+0xc 4
rsp+0x8 3
rsp+0x4 2
rsp 1
  1. 走完第一遍循環, 發現輸入的數字滿足兩個條件: a. 參數1≤6; b.參數1和參數2,3,4,5,6都不相等

  2. 在該循環結束之後, 地址0x40114D處, 對r13中保存的地址+4, 即此時r13保存的爲參數2的地址, 在地址0x401151處跳回地址0x401114.

則可以推斷出:

a) 輸入的6個數字都要≤6

b) 每個數字和後面的數字均不相等

在這裏插入圖片描述

r12寄存器用於控制循環, 當r12寄存器中的值爲6時,跳轉到地址0x401153處

  1. 地址0x40115-0x401174

在這裏插入圖片描述

0x401153處將rsp+18h的地址給rsi, 也就是參數6的地址再加4.

0x401158處將r14中存放的地址, 也就是參數1的地址(往上溯源發現是rsp中存放的值)賦值給rax寄存器. rax存放的地址在0x401166處自增4,也就是存放下一個參數的地址. 然後rsi寄存器在地址0x40116A處與rax進行比較, 控制循環的次數.

接下來, 在0x40115B處將7賦值給ecx寄存器, 在每一層循環中都用ecx寄存器對edx寄存器重新賦值. 接下來用edx減去當前參數, 並對當前參數重新覆蓋.

參數1 = 7 - 參數1; 參數2=7-參數2; 參數3=7-參數3… 以此類推

再令esi爲0, 跳轉到0x401197.

  1. 接下來, 注意到gdb調試中的0x401183和0x4011A4的指令中, 都有個地址: 0x6032d0. 在IDA中, 顯示爲node1. 猜測爲鏈表.

可以看到是在數據段, 因此這個鏈表應該是個全局變量. 注意到node1中, 0x6032D8-0x6032DF爲0x6032E0(高位放在高地址,低位放在低地址), 也就是node2的地址. 因此, 該節點應該是個結構體, 最後一個成員爲指針, 指向下一個節點, 並且佔了八個字節. 一個節點佔了16個字節.

因此可以猜測結構體爲:

struct node{

          …,

          node *next;

};

在這裏插入圖片描述

  1. 地址0x401197開始, 有兩個循環, 外層循環由rsi寄存器控制, 當rsi寄存器值爲0x18時退出, 內層循環由eax控制, 當eax值和ecx寄存器值一樣時退出.

a) 如果當前的參數值(也就是被7減過的)大於1, 則進入一層子循環0x401176-0x40117F, 用寄存器eax的值來控制, 只有噹噹前的參數與eax的值相同時纔會退出.

並且將rdx寄存器中的值賦值爲每個節點的起始地址 :

0x401176處: mov rdx, QWORD ptr [rdx+8] 這裏比較奇怪的是IDA中沒有顯示QWORD ptr

從rdx+8的地址開始, 取8個字節放進rdx寄存器, rdx是node1的 起始地址, +8是剛好到指向下個節點的指針的起始地址, 再取8字節,剛好就是下一個節點的起始地址

當eax的值與當前參數相同時, 跳出循環,並且跳轉至0x401188處, 把node1的地址賦值給[rsp+rsi*2+20h], 然後rsi自增4, 當rsi值爲24時退出循環, 剛好賦值了6次. 當rsi值不爲24時, 繼續回到0x401197, 處理下一個參數.

在這裏插入圖片描述

b) 如果當前參數≤1, 則跳轉到0x401183處, 先將node1地址給edx寄存器, 然後在0x401188處對地址[rsp+rsi*2+20h]進行賦值.

c) 因此, 從0x401176-0x401197的意義是, 根據被7減過的參數, 對地址[rsp+rsi2+0x20]進行賦值.
比如當前參數1的值爲2, 此時rsi值爲0, 那麼會將地址[rsp+0
2+0x20]的內容賦值爲 node參數1 也就是node2 的地址. 可以猜想[rsp+0+20h]開始分配了一個數組, 數組起始地址從[rsp+0x20]開始, 最後一個元素起始地址是[rsp+0x48]:

struct node *Array[6];

其中, Arrayi是一個指針, 指向了節點p(nodep, p=1,2…,6)的地址, p爲參數的值. 並且, 由於指針是八個字節, 因此地址[rsp+rsi*2+20h]中rsi需要乘以2.

哭了,這部分終於理清楚了.

  1. 從地址0x4011AB開始, 這裏需要注意的是, 數組本身的地址以及數組中元素存儲的節點的起始地址的區別:

在這裏插入圖片描述

分成兩部分來看:

a) 首先是初始化的部分: 0x4011AB至0x4011BA

0x4011AB mov rbx, [rsp+78h+var_58]

[rsp+0x20] 是Array[0]的起始地址, mov指令將該起始地址保存的地址, 也就是 node參數1 的地址賦值給了rbx寄存器.


0x4011B0 lea rax, [rsp+78h+var_50]

[rsp+0x28] 是Array[1]的起始地址, lea指令將Array[1]的起始地址賦值給rax寄存器.


4011B5 lea rsi, [rsp+78h+var_28]

[rsp+0x50]可以看作是Array[6]的起始地址, 把該地址直接賦值給rsi寄存器. 但是Array數組元素下標從0到5, 所以猜測這個地址應該是後續用來判斷循環邊界的. 這點在地址0x4011C8處得到驗證.


4011BA mov rcx, rbx

這裏將rbx保存的值, 也就是 node參數1 的起始地址賦值給rcx寄存器. rcx寄存器在這裏的作用是保存當前的 node的起始地址.

b) 地址0x4011BD至0x4011D0 爲循環, 由rax寄存器控制, rsi寄存器爲邊界:

這裏就不在一行一行解讀, 詳細步驟看圖片中的註釋即可.

rcx寄存器保存了當前遍歷到的Array元素Array[i] 保存的地址, 也就是node參數i+1 的地址, 其中i∈[0,5] . 因爲數組元素Array[i]保存的是節點 nodei+1 的起始地址.

rax寄存器保存了當前Array元素的下一個元素 Array[i+1] 的地址, 再通過 [rax]就能得到該元素保存的 node參數i+2 的地址.


0x4011C0 mov [rcx+8], rdx

rcx爲 node參數i+1 的地址 rcx+8 爲node參數i 節點的結構體中, 指向下個節點的指針的起始地址. 因此,這裏是將 node參數i 節點指向了node參數i+1.

然後在0x4011C4處讓rax寄存器的值自增8, 也就是將Array[i+1] 的地址自增8, 來到Array[i+2] 的地址. 依次類推.

當rax地址與rsi相同, 即rax從[rsp+0x28]變成[rsp+0x50]時, 一共經過5個循環, 修改了5個節點指向的節點地址. 然後跳轉到0x4011D2.

總結一下, 6)中是將 Array[i]保存了 node參數i+1 的地址, i∈[0, 5].

                7)中是遍歷數組Array, 對於每個元素Array[i]保存的節點地址, 讓保存的節點地址 node參數i+1 指向 node參數i+2.

比如,

Array 0 1 2 3 4 5
node 1 3 5 2 6 4

在這裏插入圖片描述

  1. 地址0x4011D2:

地址0x4011D2處, rdx此時保存的值爲 node參數6 的地址.

地址0x4011DF處, 由0x4011AB處可知, rbx爲[rsp+0x20], 是 node參數1 的地址. 則這裏是將[rbx+8]的值, 也就是node1結構體中指向下個節點的指針, 即 node參數2 的地址給了rax寄存器.

地址0x4011E3處將[rax]的值, 也就是node參數2的值賦值給eax寄存器.

結合地址0x4011E5和0x4011E7可知, [rbx]必須大於 eax寄存器的值, 也就是node參數1 的值 必須≥node參數2的值. 不滿足該條件就會引爆炸彈! 也就是說, 當前節點的值必須 > 它指向的節點的值. 也就是說, 用戶的輸入能對這個鏈表進行從大到小的排序.

同時注意到0x4011E5是將eax的值賦值給dword大小的內存空間[rbx], 也就是4個字節. 因此, 可以判斷結構體第一個成員是int類型. 還記得之前分析的0x4011C0(第7)點分析)中, 爲了將地址偏移到當前節點的結構體中指向下個節點的指針, 需要將rcx+8.回想起之前老師說的對齊機制, 這裏應該是在節點的值–4字節 與 指針–8字節之間填充了4個字節. 結構體可以初步判斷爲:

struct node{

          int val;

          int padding;

          node* next;

};

滿足這個條件判斷, 跳轉到0x4011EE.

0x4011EE mov rbx, [rbx+8]

[rbx+8] 爲node1結構體中指向下個節點的指針, 即 node參數2 的地址, 將該地址賦值給rbx. 也就是遍歷到下一個節點. 然後跳回地址0x4011DF.

  1. 那麼, 現在的問題就變成了需要知道鏈表中節點存儲的值. 然後對其從大到小排序, 再反推出輸入的數值.

在IDA中查看或是在gdb中打印, 這裏我直接在gdb中打印, 以16進制打印12個 8個字節的值:

在這裏插入圖片描述

根據高位放在高地址, 低位放在低地址的原則, 結合結構體的結構:

struct node{

          int val; // 低4字節

          int padding; // 高4字節

          node* next; // 高8字節

};

畫出節點的初始關係圖:

在這裏插入圖片描述

用python將其轉爲10進制:

在這裏插入圖片描述

對節點的值進行從大到小排序:

節點val 0x39c 0x2b3 0x1dd 0x1bb 0x14c 0xa8
節點編號 node 3 node 4 node 5 node 6 node 1 node 2

也就是說, 用戶的輸入在經由 7 減去每個數之後得到的是 3, 4, 5, 6, 1, 2. 這樣一來才能保證重新鏈接後的節點的值是從大到小排序的.

因此, 用戶的輸入爲4 3 2 1 6 5

測試成功!~

在這裏插入圖片描述

0x05 總結收穫

通過這次二進制炸彈的實驗,算是對彙編的基礎有了一點了解。並且對IDA的使用和gdb的使用也有了初步的瞭解。

不過這篇被diss惹,不能寫得太細,應該把整個題目的框架揪出來就好。 下次好好照着師兄的要求來!

0x06 參考資料

1.關於彙編跳轉指令的說明

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章