天天看點

《深入了解計算機系統》CSAPP_BombLab

作業系統:linux

調試工具:gdb

文章分為實驗前準備、複習、拆炸彈三個子產品。讀者可以自取所需。

目錄

Bomb Lab

實驗前準備

複習

拆炸彈

phase_1

phase_2

phase_3

phase_4

phase_5

phase_6

secret_phase

結語

實驗由<code>phase_1 ~ phase_6</code>組成,共6個階段,

我們需要分别輸入6個字元串密碼,以便解除Dr. Evil的炸彈,

但隻要有1個字元串輸錯,炸彈就會爆炸。

那麼如何知道6個正确的字元串呢?

我們需要分别檢視<code>phase_1 ~ phase_6</code>的彙編代碼,

而炸彈就存放在其中,我們要根據彙編代碼小心地避開炸彈,分析出正确的字元串即可。

而分析需要用到學習的彙編知識、常用的gdb指令以及linux終端指令。

需要用到的linux終端指令:

别忘了先進入檔案位址中

<code>objdump -d bomb &gt; assembly.s</code>

反編譯出<code>bomb.c</code>的彙編代碼,并存放在<code>assembly.s</code>中(assembly.s是自己命名的)

<code>gdb 檔案名</code>

用gdb調試工具打開某個檔案

eg. <code>gdb bomb</code> 用gdb調試工具打開bomb.c檔案

而我們就是要用gdb調試工具打開bomb.c檔案後做實驗

常用gdb指令:

gdb指令必須用gdb調試工具打開檔案後才能調用

<code>run</code> (可簡寫為:<code>r</code>)

運作檔案。

<code>quit</code> (可簡寫為:<code>q</code>)

停止運作,跳出gdb調試工具

<code>break 函數名</code> (可簡寫為:<code>b 函數名</code>)

在某個函數處打斷點,以便之後用disas展示這個函數的彙編代碼。

eg. <code>b phase_1</code> 在phase_1函數打斷點

<code>disas 函數名</code>

展示這個函數的彙編代碼(但必須先打斷點)

eg. <code>disas phase_1</code> 展示phase_1的彙編代碼

<code>x/NFU address or register</code>

用指定格式(FU)、指定數量(N)輸出位址或者寄存器對應内容

eg1. <code>x/s 0x40139b</code> 輸出0x40139b對應内容

eg2. <code>x/s $rsi</code> 輸出%rsi對應内容

eg3.<code>x/4xw 0x6032d0</code> 按24個十六進制4位元組格式輸出0x6032d0對應内容

<code>p/F *address or *register</code>

用指定格式(F)列印位址或者寄存器對應内容(效果上,<code>x/F = p/F</code>)

eg1. <code>p/x *(0x6032f0)</code> 按十六進制列印0x6032f0對應内容

格式F、機關U都與c語言類似,參考連結gdb:Format、Variables and Memory部分

測試和比較指令

- <code>ZF、OF、SF、CF</code>(條件碼)

zero flag 零标志位;overflow flag 溢出标志位;sign flag 符号标志位;carry flag 進位标志位。

- <code>test S1,S2</code>即 <code>S1 &amp; S2</code>

測試指令,進行位級與操作,若結果為0,傳回<code>ZF = 1</code>;若結果非0,傳回<code>ZF = 0</code>,但不會改變兩個數的值。同時,需要特殊注意的是,可以對同一個寄存器使用test指令,例如<code>test %eax,%eax</code>。

- <code>cmp S1,S2</code>即<code>S2 - S1</code>

比較指令,進行減法操作,但不會改變兩個數的值。

部分跳轉指令

不區分unsiged(無符号)、siged(有符号)跳轉

<code>jne</code>

根據ZF的值判斷是否跳轉,當<code>ZF = 0</code>才跳轉。

<code>je</code>

根據ZF的值判斷是否跳轉,當<code>ZF = 1</code>才跳轉。

siged jump 指令

<code>jle</code> (<code>jump&lt;=</code>)

j:jump;l:less(小于);e:equal

<code>jge</code> (<code>jump&lt;=</code>)

j:jump;g:great(大于);e:equal

<code>jg</code> (<code>jump&gt;</code>)

j:jump;g:great(大于)

unsiged jump 指令

<code>jb</code>(<code>jump&lt;</code>)

j:jump;b:below(小于)

<code>jbe</code> (<code>jump&lt;=</code>)

j:jump;b:below(小于);e:equal

<code>ja</code> (<code>jump&gt;</code>)

j:jump;a:above(大于)

<code>cmp</code>與signed jump、unsigned jump的聯系(原理與條件碼有關,在此不解釋了)

當<code>cmp S1,S2</code>伴随signed jump指令使用時,S1置前,S2置後,例如:

當<code>cmp S1,S2</code>伴随unsigned jump指令使用時,S2置前,S1置後,例如:

部分二進制操作

<code>lea S,D</code> (即<code>D &lt;-- S</code>,等價于指派算式<code>D = S</code>)

不是讀取資料,是加載有效位址,但和<code>mov</code>指令效果相同,常用于執行簡潔的算術操作。

<code>sub S,D</code> (即<code>D &lt;-- D - S</code>,等價于指派算式<code>D -= S</code>)

減法,D - S後指派給D。

<code>sar S,D</code> (即<code>D &lt;-- D &gt;&gt; S</code>,等價于指派算式<code>D &gt;&gt;= S</code>)

算術右移,補符号位(sign),D &gt;&gt; S後指派給D。注意:若<code>sar D</code>,則預設是算術右移<code>1</code>位。

<code>shr S,D</code> (即<code>D &lt;-- D &gt;&gt; S</code>,等價于指派算式<code>D &gt;&gt;= S</code>)

算術右移,補<code>0</code>,D &gt;&gt; S後指派給D。

當寄存器為空時,其值為<code>0</code>

<code>%rdi</code>

系統往往要把一個值賦給%rdi後,才能當作入參(即輸入的參數)

<code>register</code>

注意:彙編代碼為了優化常常使用同一個系列的<code>register</code>表示同一個變量

例如:

register(64bits)

作用

%rax

存放傳回值

%rbx

被調用者儲存(存放被調用者的位址,可以是函數位址,也可以是其它位址,callee save)

%rcx

第4個參數(存儲參數,可以是數字字元參數,也可以是位址參數)

%rdx

第3個參數

%rsi

第2個參數

%rdi

第1個參數(經常作入參使用)

%rbp

被調用者儲存

%rsp

棧指針(用以開辟棧空間,注意入參的先進後出)

%r8

第5個參數

%r9

第6個參數

%10

調用者儲存(存放調用者函數的位址,可以是函數位址,也可以是其它位址,caller save)

%r11

調用者儲存

%r12

%r13

%r14

%r15

優化

有些看似毫無作用的彙編代碼,其實是在做優化。

在正式拆炸彈之前需要注意到一點,我們輸入的字元串都是放入到<code>%rdi</code>當中的。

(其實一般情況下,輸入也是放到<code>%rdi</code>的)

也就是說,在每個<code>phase</code>調用時,預設<code>%rdi</code>這個寄存器存放着輸入的字元串。

如何得到這點的呢?是通過分析<code>main</code>函數的彙編代碼得出的。

分析:

我們可以看到,在每個<code>phase</code>調用前,都<code>callq 0x40149e &lt;read_line&gt;</code>,然後<code>mov %rax,%rdi</code>,這樣子會将我們輸入的字元串最終存到<code>%rdi</code>中。(我們是把字元串輸入到<code>read_line</code>的,而<code>read_line</code>會傳回到<code>%rax</code>中,<code>mov</code>操作将<code>%rax</code>存到<code>%rdi</code>。注意,<code>%rax</code>往往用來放傳回值)

至于<code>read_line</code>的彙編代碼我就不分析了,通過名字也可以看出:它是用來傳回我們的輸入值的

下面開始分析<code>phase_1 ~ phase_6</code>的彙編代碼,

得出認為正确的字元串,并逐個解除炸彈

檢視<code>phase_1</code>的彙編代碼:

重點就是<code>&lt;+14&gt;</code>的test指令,可以看出當<code>%eax</code>為0時才不會爆炸。也就是說函數<code>strings_not_equal</code>的傳回值必須為0才行,根據名字我們可以猜測,字元串不相等時傳回1,字元串相等時傳回0。下面檢視<code>string_not_equal</code>的彙編代碼驗證我們的猜測。

通過檢視<code>string_not_equal</code>函數的彙編代碼,可以找到,<code>string_not_equal</code>函數先是比較了輸入字元串和位址<code>0x402400</code>對應的字元長度,再逐個比較字元,最終,字元相同傳回<code>0</code>,字元不同傳回<code>1</code>。我們可以看到,彙編進行不少優化。例如,如果第一個字元已經是不同的,就不用循環進行更多的操作。

key:由此可以知道,隻要我們輸入的字元和位址<code>0x402400</code>對應的字元相同即可。那麼,我們通過gdb指令<code>x/s 0x402400</code>即可通路<code>0x402400</code>對應的字元。代碼如下:

那麼,隻要在運作界面輸入<code>Border relations with Canada have never been better.</code>即可。(譯為:與加拿大的邊境關系從來沒有這麼好過。)

我們先檢視<code>phase_2</code>的彙編代碼:

<code>callq 0x40145c &lt;read_six_numbers&gt;</code>,我們檢視這個函數的彙編代碼:

(根據名字,我們猜測其作用是讀取6個數字)

注意到<code>phase_2</code>中<code>&lt;+6&gt;</code>已經将%rsp賦給%rsi,那麼,對%rsi進行操作也就是之後對<code>phase_2</code>中的%rsp操作。

以下表格是對%rsi操作的展示:

0x4(%rsi)

0x8(%rsi)

0xc(%rsi)

0x10(%rsi)

0x14(%rsi)

(%rsp)

0x8(%rsp)

注意,<code>0x10 = 16D</code>,<code>0x14 = 20D</code>。十進制字尾<code>D</code>,Decimal.

可以看到,這樣操作傳遞了6個位址參數,其位址間隔為<code>4</code>,最後兩個因為參數寄存器不夠用了,隻能用棧幀%rsp。(注意,此處的%rsp是<code>read_six_numbers</code>自己開辟的,原先<code>phase_2</code>的%rsp已經中斷)

<code>&lt;+36&gt;</code>處,<code>mov $0x4025c3,%esi</code>,通路0x4025c3内容:

可以知道%eax中存放着格式字元串

<code>&lt;+46&gt;</code>處,<code>callq 0x400bf0 &lt;__isoc99_sscanf@plt&gt;</code>,函數<code>sscanf</code>是c語言自帶的函數,同<code>scanf</code>類似,作用是讀取輸入的字元串,并根據指定格式存儲到指定位址。這告訴了我們的輸入的字元串,應該是6個整型數字,而且之間用空格隔開。同時,它們存放在上述的6個參數位址中,也就是之後<code>phase_2</code>的%rsp按<code>4</code>遞增位址的棧幀中。符合我們的猜測。

我們回到<code>phase_2</code>繼續分析: 可以知道第一個參數數值<code>(%rsp)</code>必須是<code>1</code>,之後會進入一個循環。我們把這個循環單獨拿出來看:

我們已經知道<code>(%rsp) = 1</code>,那麼第一次循環中,<code>&lt;+32&gt;</code>處的<code>%eax = 2</code>,故隻有<code>(%rbx) = 0x4(%rsp) = 2</code>,才不爆炸;第二次循環中,<code>&lt;+32&gt;</code>處的<code>%eax = 4</code>,故<code>(%rbx) = 0x8(%rsp) = 4</code>;第三次循環中,<code>&lt;+32&gt;</code>處的<code>%eax = 8</code>,故<code>(%rbx) = 0xc(%rsp) = 8</code>,可以看出是以1為首項,後一項是前一項兩倍的遞增數列,共6項,以此類推,有下表。

0x4(%rsp)

0xc(%rsp)

0x10(%rsp)

0x14(%rsp)

1

2

4

8

16

32

key: 由此得知,phase_2該輸入的字元串為<code>1 2 4 8 16 32</code>

<code>phase_3</code>的彙編代碼如下:

通路<code>0x4025cf</code>,可知字元串格式為<code>"%d %d"</code>,進而得知輸入字元串是兩個整數。根據<code>&lt;+39&gt;: cmpl $0x7,0x8(%rsp)</code>和<code>&lt;+44&gt;: ja 0x400fad &lt;phase_3+106&gt;</code>可以知道<code>0x8(%rsp) &lt;= 7</code>。那麼,第一個參數%rdx取值要小于等于7。

根據<code>&lt;+50&gt;: jmpq *0x402470(,%rax,8)</code>可以知道,跳轉的位址是指針<code>*0x402470</code>的變換,增量為<code>8</code>,此時為<code>0x8(%rsp)</code>即為%eax。

下面我們用<code>x/s</code>指令逐個通路,指針位址是遞增的,但對應存放的<code>phase_3</code>的位址并不是遞增的。到這時,答案已經呼之欲出了。

(由此可以知道指針<code>*0x402470</code>指向一個存放<code>phase_3</code>部分位址的字元串。一開始我想的是對<code>phase_3</code>的部分位址直接進行增<code>8</code>操作,沒有想到是對指針<code>*0x402470</code>進行增<code>8</code>操作,進而通路不同的<code>phase_3</code>中的位址,耽誤了許久)

對<code>phase_3</code>繼續分析:

根據<code>&lt;+123&gt;: cmp 0xc(%rsp),%eax</code>和<code>&lt;+127&gt;: je 0x400fc9 &lt;phase_3+134&gt;</code>知道,第二個參數<code>0xc(%rsp)</code>必須和%eax相同,此時的%eax根據<code>0x8(%rsp)</code>不同而不同。

key:

整理上述分析可以知道,共有<code>8</code>組答案如下:

3

x/s

*0x402470

*(~ +8)

*(~ +16)

*(~ +24)

&lt;phase_3+n&gt;

+57

+118

+64

+71

0xc(%rsp) H

0xcf

0x137

0x2c3

0x100

0xc(%rsp) D

207

311

707

256

5

6

7

*(~ +32)

*(~ +40)

*(~ +48)

*(~ +56)

+78

+85

+92

+99

0x185

0xce

0x2aa

0x147

389

206

682

327

(十六進制字尾,H,hexadecimal;十進制字尾,D,Decimal)

我們選擇第8組答案<code>7 327</code>作為輸入:

<code>phase_4</code>的彙編代碼如下:

分析:仍舊是輸入兩個<code>int</code>參數,根據<code>&lt;+34&gt;</code>處可知,第一個參數<code>0x8(%rsp)</code>滿足<code>0xe &gt; 0x8(%rsp)</code>,第二個參數<code>0xc(%rsp)</code>必須為0。但是第一個參數需要根據<code>&lt;func4&gt;</code>函數來确定。

<code>&lt;func4&gt;</code>的彙編代碼如下:

第一次進入func4函數:

分析:其實這個時候答案已經出來了,隻要第一個參數為<code>7</code>和<code>%ecx=7</code>相同即可。但顯然<code>func</code>是一個遞歸函數,我們進入遞歸看看,還有什麼答案。

第一次遞歸:(記為<code>R1</code>)

第一個參數為<code>3</code>和<code>%ecx=3</code>相同即可。

第二次遞歸:(記為<code>R2</code>)

第一個參數為<code>1</code>和<code>%ecx=1</code>相同即可。

第三次遞歸:(記為<code>R3</code>)

第一個參數為<code>0</code>和<code>%ecx=0</code>相同即可。

第四次遞歸:(記為<code>R4</code>)

第五次遞歸:(記為<code>R5</code>,此時<code>R5 = R3</code>)

由此可以知道,<code>R4</code>之後會一直在<code>R3、R4</code>之間無限循環,直到程式崩潰。那麼,第一個參數的取值隻能為:<code>0 1 3 7</code>

得出<code>4</code>組答案為:

0x8(%rsp) 第一個輸入參數

0xc(%rsp) 第二個輸入參數

我們取<code>0 0</code>這組輸入。

<code>phase_5</code>的彙編代碼如下:

很顯然,輸入的字元串長度得為<code>6</code>。利用x/s指令,在<code>&lt;+55&gt;</code>處我們檢視<code>x/s 0x4024b0</code>,給出一句嘲諷我們的話,<code>"maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"</code>,很明顯不是答案,也許是答案,也許不是答案,也許在暗示答案不能單單copy,我們無視他的嘲諷,對<code>&lt;+41&gt; ~ &lt;+74&gt;</code>處的循環進行專門分析。

第二次循環:

以此類推至第六次循環後,應當有<code>&lt;+41&gt;: %ecx = %rbx+5</code>和<code>&lt;+62&gt;: %rsp+0x10+5 = %dl</code>。此時,<code>%rbx</code>是輸入,會有: <code>%rsp+0x10+5 = %dl = %edx = 0x4024b0+(%rdx &amp; 0xf)</code>

上式等價于:<code>= 0x4024b0+(%cl &amp; 0xf) = 0x4024b0+((%rbx+5) &amp; 0xf)</code>

所有情況見下表。

%rsp+0x10+0

0x4024b0+%rbx+0(0x4024b0 + (第一個輸入參數 &amp; 0xf))

%rsp+0x10+1

0x4024b0+%rbx+1(0x4024b0 + (第二個輸入參數 &amp; 0xf))

%rsp+0x10+2

0x4024b0+%rbx+2(0x4024b0 + (第三個輸入參數 &amp; 0xf))

%rsp+0x10+3

0x4024b0+%rbx+3(0x4024b0 + (第四個輸入參數 &amp; 0xf))

%rsp+0x10+4

0x4024b0+%rbx+4(0x4024b0 + (第五個輸入參數 &amp; 0xf))

%rsp+0x10+5

0x4024b0+%rbx+5(0x4024b0 + (第六個輸入參數 &amp; 0xf))

可以看出<code>%rsp+0x10</code>的指派是<code>0x4024b0</code>的偏移,偏移量範圍為<code>0 ~ 15</code>,也就是嘲諷句的前16個字元,即<code>maduiersnfotvbyl</code>。

我們回到<code>phase_5</code>函數繼續分析,利用x/s指令,在<code>&lt;+81&gt;</code>處我們檢視<code>x/s 0x40245e</code>,給出一個單詞字元串,<code>"flyers"</code>(譯為飛翔者們或者考熟的鴨子飛走了)。也就是說,<code>&lt;+86&gt;</code>處的入參必須與<code>flyers</code>相同。

根據<code>maduiersnfotvbyl</code>,取得<code>flyers</code>對應<code>0x4024b0</code>的偏移量是<code>9、15、14、5、6、7</code>,但是隻能輸入<code>6</code>個字元,如果直接輸入<code>91514567</code>,超過<code>6</code>個字元會爆炸。

要解決這個問題,可以先把偏移量轉換為二進制<code>1001、1111、1110、0101、0110、0111</code>。因為有<code>%rbx &amp; 0xf</code>的位級操作,那麼我們隻要保證尾4位相同即可。這時可以聯想到用來定義字元的ASCII表,那麼答案有許多種,我比較懶,就隻找尾4位為<code>1111</code>和<code>1110</code>的字元。分别為<code>/</code>和<code>.</code>,進而得到輸入<code>9/.567</code>。

檢視<code>phase_6</code>的彙編代碼:

好家夥,一個電腦螢幕根本裝不下全部代碼。我們分成幾個部分來看。

注意:在循環開始前會有一些變量準備;<code>0x10 = 16D,0x14 = 20D</code>;<code>flag</code>這個變量命名常用來當作循環是否結束的标志。

Part1,讀入了6個整數。

根據<code>phase_2</code>的<code>&lt;read_six_numbers&gt;</code>分析可以知道,這段讀入了6個整數,對棧幀<code>%rsp</code>偏移(偏移量為<code>4</code>)即是輸入<code>input[i],i取0 ~ 5</code>。如下表所示:

input[0]

input[1]

input[2]

input[3]

input[4]

input[5]

Part2,保證6個輸入整數(<code>input[i],i取0 ~ 5</code>)都不相同且範圍為1 ~ 6。

loop1确定了<code>input[i]</code>的取值範圍為:<code>1 ~ 6</code>。loop2保證6個輸入都不相等。

取值範圍如何确定的?由<code>+39 ~ +45</code>處得出,<code>input[i] &lt;= 6</code>。同時,因為<code>+45</code>的 <code>jbe</code> 是對無符号數操作,若<code>input[i]</code>取 <code>0</code> ,執行<code>0 - 1 = -1</code>在比較時會出錯(溢出),是以不能取<code>0</code>。故隻能取<code>1 ~ 6</code>。

Part3,執行算式:<code>input[i] = 7 - input[i]</code>

是以為了友善,我們重新命名:<code>sub_input[i] = 7 - input[i]</code>

Part4,引傳入連結表并改變連結清單順序。

首先我們檢視<code>0x6032d0</code>,一開始用<code>x/s</code>指令都是亂碼,如圖所示。

但是注意到<code>node1</code>,說明其很有可能是連結清單,而不是字元串。

我們用<code>x/24xw</code>指令,按xw格式(十六進制4位元組格式)輸出24個資料。

如圖所示,符合我們的猜測,就是連結清單,<code>0x6032d0</code>是head指針。

其中,node是節點位址,val是節點值,order是它的序列(一共6個),next是下個節點的位址(能夠發現它剛好就是按照序列排列的,最後一個節點的next是<code>0x0即NULL</code>)。

那麼,該段循環是怎麼改變連結清單順序的呢?首先引入棧幀<code>%rsp + 8*i + 0x20,i取0 ~ 5</code>,然後在<code>+130 ~ +137</code>,<code>0x6032d0</code>将随<code>sub_input[i]</code>的改變而改變,最後将改變後的連結清單逐個賦給棧幀。

即執行執行指派算式:<code>%rsp + 8*i + 0x20 = $0x6032d0 + (8*(sub_input[i]-1))</code>

這樣下來就能夠重排連結清單了。但此處還沒有給出改變連結清單順序的标準是什麼。

同時注意到一個重要的細節問題:改變時0x6032d0的間隔為8D,但我們需要的間隔為0x10 = 16D,如何解決?

事實上,間隔為8也沒關系。隻要部分+8的位址所存數值恰好為+16的位址即可。(不是所有位址都需要這樣,另一部分位址執行兩次+8就行了)

上述表述等價于三個等式

<code>*(0x6032d0+8) = 0x6032e0</code>、<code>*(0x6032f0+8) = 0x603200</code>、<code>*(0x603310+8)= 0x603320</code>成立。

我們用p/x 指令列印數值看三個等式是否成立。(這個成立是<code>phase_6</code>已經設定好的,但不檢視之前完全不知道)

執行part4後,可以得出如下表格:

rec_node0

rec_node1

rec_node2

rec_node3

rec_node4

rec_node5

%rsp + 0x20

%rsp + 0x28

%rsp + 0x30

%rsp + 0x38

%rsp + 0x40

%rsp + 0x48

說明<code>rec_node1 ~ rec_node6</code>将逐個放入棧幀<code>%rsp + 0x20 ~ %rsp + 0x48</code>中。

注意:因為node0 ~ node5已經随sub_input而重新排列了,但我們還不知道是如何排列的。

是以,不妨命名為:rec_node0 ~ rec_node5,表明連結清單重組,也就是所謂的改變連結清單順序。

(rec:recombination)

同時也可以得出<code>sub_input</code>對應<code>rec_node</code>的表格:

sub_input[0]

sub_input[1]

sub_input[2]

sub_input[3]

sub_input[4]

sub_input[5]

0x6032d0

0x6032e0

0x6032f0

0x603300

0x603310

0x603320

part5,重新連接配接next。

注意:

<code>0x50(%rsp) = 0 = NULL</code>是next連接配接完才指派的。

這裡利用棧幀<code>%0x28(%rsp) ~ 0x50(%rsp)</code>逐個賦給<code>%0x20(%rsp) ~ 0x48(%rsp)</code>

改變的是rec_node -&gt;next指向的位址,而不是rec_ndoe本身。也就是所謂的重新連接配接next。

之是以要重連next,也是因為node已經重排為rec_ndoe了。

part6,說明改變連結清單順序的标準:按<code>val</code>降序排列。(從大到小)

之是以按<code>val</code>降序排列,是因為必須滿足<code>(0x8+0x20+%rsp+k*8) &gt;= (0x20+%rsp+k*8),k取0~5</code>

即<code>node-&gt;val &gt;= node-&gt;next-&gt;val</code>成立,也就是連結清單中目前節點值大于下一個節點值。

綜上part1 ~ part6可以得出答案:(<code>sub_input</code>模7得到<code>input</code>)

val

0x39c

0x2b3

0x1dd

0x1bb

0x14c

0x0a8

rec_node

sub_input

input

inuput為:<code>4 3 2 1 6 5</code>

ps:如果你仔細觀察,你會發現,按val降序排列之後,order其實恰好對應着sub_input。

可惡啊!竟然還有個彩蛋關。什麼?你說我怎麼發現的,當然是“口口相傳”啦。

(事實上可以在編譯檔案上查找<code>ctrl+f</code>到)

用到了二叉樹。

檢視<code>phase_defused</code>的彙編代碼:(可以知道在phase_4中多輸入個DrEvil即<code>0 0 DrEvil</code>才能進入彩蛋關)

我們對一些字元串進行通路,可以知道調用<code>sscanf</code>函數前得輸入,其格式為<code>"%d %d %s"</code>但輸入内容是空的,這說明我們還沒輸入。我們需要輸入,但,是什麼時候輸入呢?

要确定輸入是何時輸入的,我們可以先在<code>*0x4015f5</code>處,即輸入處打斷點,然後run bomb檔案,最後通路<code>0x603870</code>的内容。

此時有<code>0x603870 &lt;input_strings+240&gt;: "0 0 "</code>,這說明輸入是在<code>phase_4</code>處進行的。其輸入格式為<code>"%d %d %s"</code>,同時有<code>*(0x402622) == DrEvil</code>,之後要進行字元比對且相同才進入secret_phase。那麼我們大膽猜測,應該在<code>phase_4</code>處輸入<code>0 0 DrEvil</code>才能進入彩蛋關。

開始正式分析,首先檢視<code>secret_phase</code>的彙編代碼:(說明調用fun7之後,傳回值必須為2)

檢視<code>fun7</code>的彙編代碼:

這是一棵二叉樹,思路如圖所示:

《深入了解計算機系統》CSAPP_BombLab

方法一:實作eax = 2。(ps:遞歸調用順序與傳回順序相反)

下面描述調用過程:(ps:esi是input,每次調用完都會更新rid指向的位址)

B:第一次調用fun7,<code>(rdi) = (0x6030f0) = 0x24</code>。同時輸入要不滿足<code>(rdi) &lt;= esi</code>,也就說輸入小于0x24,才能走路徑B。

此次調用結束時更新rdi指向的位址,即:<code>rdi+0x8 = 0x603110</code>。

C:第二次調用fun7,<code>(rdi) = (0x603110) = 0x8</code>。同時輸入要滿足<code>(rdi) &lt;= esi</code>且不滿足<code>edx == esi</code>,也就說輸入大于0x8且不等于0x8,才能走路徑C。

此次調用結束時更新rdi指向的位址,即:<code>rdi+0x10 = 0x603150</code>。

D:第三次調用fun7,<code>(rdi) = (0x603150) = 0x16</code>。同時輸入要滿足<code>(rdi) &lt;= esi</code>且滿足<code>edx == esi</code>,也就說輸入等于0x16即20D,才能走路徑D。

三次調用結束,得出input為<code>20</code>。

方法二:實作eax = 2。

key:我們取<code>20</code>這個答案。

無聊地檢視一下二叉樹内容:(此處對解題不是很有必要)

整理後的圖狀展示:

But you know, Alan, sometimes it's the very people who no one imagines anything of who do the things no one can imagine. ---The Imitation Game

從9.27到10.11,總算完結了。期間有幾天是沒有寫的。總共是8天,最後兩關都用了2天。當然不是一整天,5、6個小時應該是有的。完全靠自己寫的是phase_4,奈何沒啥基礎。前幾個都在适應gdb是怎麼調試的,後幾個難度比較大,phase_5是沒聯系到ASCII,後兩個是連結清單、二叉樹都不熟悉。也參考了不少其它大佬的解答。

自己的彙編閱讀能力是提升了不少,後面看都是比較自然,但是聯系到資料結構就比較不了解,還是得繼續提高姿勢水準。

如有謬誤,敬請指正。