or
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up
Syntax | Example | Reference | |
---|---|---|---|
# Header | Header | 基本排版 | |
- Unordered List |
|
||
1. Ordered List |
|
||
- [ ] Todo List |
|
||
> Blockquote | Blockquote |
||
**Bold font** | Bold font | ||
*Italics font* | Italics font | ||
~~Strikethrough~~ | |||
19^th^ | 19th | ||
H~2~O | H2O | ||
++Inserted text++ | Inserted text | ||
==Marked text== | Marked text | ||
[link text](https:// "title") | Link | ||
 | Image | ||
`Code` | Code |
在筆記中貼入程式碼 | |
```javascript var i = 0; ``` |
|
||
:smile: | ![]() |
Emoji list | |
{%youtube youtube_id %} | Externals | ||
$L^aT_eX$ | LaTeX | ||
:::info This is a alert area. ::: |
This is a alert area. |
On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?
Please give us some advice and help us improve HackMD.
Do you want to remove this version name and description?
Syncing
xxxxxxxxxx
CS:APP Bomb Lab
背景知識
環境安裝
從這裡安裝好後,進行解壓縮:
文件結構
bomb.c
: 主程式的 C 文件bomb
: 可執行檔,要使用gdb
對其調適以及反彙編READMED.md
: 說明題目文件實驗原理
原始的
bomb.c
的主函式如下:可以看到在每個題裡面都會先讀取一行字串,將這個字串傳入
phase_n
函式裡面經過一些比較,若字串輸入正確,就代表拆彈成功,若是輸入錯誤則炸彈就會爆炸。而
phase_n
函式裡面的程式我們是看不到的,因此只能透過反彙編工具以及gdb
來取得他的組合語言,進而分析要拆彈的字串是什麼。一共有phase_1
到phase_6
,6 個題目。實驗準備
先透過以下命令來獲取整個
bomb
的反彙編:GDB 命令
這邊列舉一些
gdb
的命令方便查閱:- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →x86_64 組語
$
開頭%rsp
,%rax
$
開頭$416
,$0x20
()
刮起($rdi)
,0x20(%r8)
OP A B
mov A B
cmp A B
test A B
call adress
ret
lea 4(%rdx, %rbx, 2), %rdx
phase_1
輸入
gdb bomb
進入 gdb,接著在第一題設置斷點break phase_1
,透過disas
來獲得函式的組合語言,如下:0x402400
傳入%esi
strings_not_equal
函式所以我們可以知道重點是
strings_not_equal
函式的比較,所以將他的組合語言抓出來看:已經知道前面將 常數
0x402400
傳入%esi
,所以我們要關心的地方就是有關 rsi 暫存器的操作。%rbp = %rsi
(%rbp)
和%al
來判斷是不是一樣的字串所以最終知道記憶體位置
0x402400
所儲存的字串就是要比較的字串,可以透過以下兩種方式來獲取其資訊:最後重新取啟動 gdb,設置新斷點
break phase_2
,並輸入上面字串後,phase_1 就通過啦。獲得一個題目的答案後,必須要設置下個題目的斷點,否則炸彈會爆炸。
嘗試將 phase_1 寫成原始的 C 語言程式:
phase_2
和上面一樣的操作,這邊不重寫一遍了。
試著跟著程式的流程走一遍,並將其寫成 C 語言風格:
可是我們不知道
read_six_numbers
函式到底對暫存器做了什麼,將其組合語言抓出來看:它在第 13 行呼叫
__isoc99_sscanf@plt
,從名子可以看出他是掃描的函式,有了上一題的經驗,我們知道第 11 行的常數記憶體地址是關鍵,所以使用以下:所以推斷說它將我們的輸入字串放入
%rsi
,然後利用sscanf
將上面格式的數字存放到堆疊中,一個整數為 4 bytes ,所以從 sp 0 ~ 24 ,十六進制則是 sp 0 ~ 0x18。而這也代表我們要輸入的字串應該是上面字串格式,也就是要輸入 6 個數字,中間要有空白。read_six_numbers
函式結束後,馬上就是判斷堆疊指標指向的第一個數字是否為 1 ,若不是則馬上爆炸。因此得知第一個數字為 1。接著設置了
%rbx
堆疊指標加 4,也就是輸入的第二個數字,%rbp
堆疊指標加 24,也就是輸入的最後一個數字。進入
do_while
迴圈,會先將rbx
減 4 的記憶體資料取出,第一次執行就是將第一個數字取出,在將其乘以 2,接著就是比較這兩個數有沒有相等。若不相等則爆炸,相等則將rbx
加四,直到和rbp
相等。這邊的操作其實就是判斷數組是不是等比級數,而公比為 2。透過
rbx
作為記憶體位置的走訪變數,每次移動一個數字就是加 4,eax
為前一個數組的數值,比較前後是否是兩倍差,結束條件就是當rbx
走訪到最後一個數組。那結果其實就很清楚了,條件如下。
要輸入的字串必為
"1 2 4 8 16 32"
一樣我們嘗試將其翻譯成 C 語言型態:
phase_3
掃描數字
印出組語來看發現和上題架構類似,馬上檢查第 3 行的
0x4025cf
記憶體位置,第 5 行就呼叫了sscanf
函式說明其掃描輸入的兩個字串。再根據第 1, 2 得知,sscanf 會將掃描的兩個數字分別存放在堆疊指標rsp + 8
和rsp + 12
。第一個數字範圍
第 11 行將輸入的第一個數字取出比較,若大於 7 ,則炸彈爆炸。
switch 比較
第一行將第一個數字放入
%rax
,第二行使用間接跳轉,根據%rax
的值選擇跳轉到哪裡。這實際上就是 switch 條件分支操作,編譯器會生成一個跳轉表(jump table),而我們可以使用 gdb 來取得這個跳轉表。從第 2 行知道基底位置是
0x402470
,每 8 個 byte 是一個地址長度,又知道第一個數字的範圍為 0 ~ 7 一共 8 個數。所以我們一共要看 \(8\times 8=64\) 個 bytes。0x400f7c
0x400fb9
0x400f83
0x400f8a
0x400f91
0x400f98
0x400f9f
0x400fa6
最終比較
根據這些地址可以看到,都是將特定數字放入
$rax
,第 1 行和第二個數字比較是否相等,不相等則會爆炸。因此本題要輸入的值就是上表中對應到的數字 1 和數字 2的字串。
C 語言反彙編
phase_4
前半部的架構和上一題一樣,知道是要掃描兩個數字。
第一個數字範圍
若是第一個數字大於 14 ,而這邊使用的是
jbe
也就是無號數操作,因此第一個數的範圍應該是 \(0\leq n_{1}\leq 14\)。func4
進入
func4
函式前設置 3 個參數,將這三個暫存器先取個變數:%edx
(j)%esi
(i)%edi
(n)%eax = j
j = j - i
%ecx = j - i
(j - i)
右移 31 位,取出符號數%ecx
= \(\frac{j-i}{2}\)%ecx
= \(\frac{j-i}{2}+i=\frac{j+i}{2}\)接下來的分支判斷用以下 C 語言呈現:
這是基於二分搜尋所設計的,判斷 i, j 平均值和 n 的大小,來做遞迴操作。但這邊不會引爆炸彈。
回到
phase_4
函式,第 17,18 行用來測試回傳數是否為 0test %eax,%eax
: 相同暫存器做 AND,若為 0,ZF 置 1,若不為 0 ,ZF 置 0jne
: 不相等跳轉,實際上就是取~ZF
我們知道
i = 0, j = 14, 0 <= n <= 14
,所以有 15 種組合,只要測試這 15 個數怎樣會回傳 0 就可以了。所以第一個數字必須是 0, 1, 3, 7 。
而第二個數字則是由上面所示,必須是 0 ,因此本題的答案為
phase_5
輸入字串長度
呼叫
string_length
函式來取得輸入字串的長度。若是不等於 6 則炸彈爆炸。迴圈
可以看到第 10, 11 行是自加 1 在判斷是否等於 6 跳回原本位置,代表這是一個從 0 到 6 執行 6 次的迴圈。
而回圈裡做的事就是本題的精隨。
rax
為迴圈計數器,rbx
存放輸入字串的記憶體位置。abcdef
為例,存放在基底為0x6038c0
的位置。0x6038c0 + 0
: 抓出0x64636261
0x6038c0 + 1
: 抓出0x65646362
0x6038c0 + 2
: 抓出0x66656463
rdx
中。0xf
做 AND,就是取出最低的 byte 。0x4024b0
的記憶體位置放入rdx
。先來看看這個位置有什麼rsp + 0x10 + rax
的記憶體位置會發現這一系列操作非常的繞,但仔細想一下就會發現其實這就是陣列的操作而已,我們可以寫成 C 語言風格:
因為組合語言不支持記憶體對記憶體的訪存,因此都需要先抓出來給暫存器再放進去記憶體裡。
而這一段的意思就是每一次迴圈都會抓出輸入字串的 ASCII 碼的低 4 位元,若輸入是
output = "abcdef"
,則每次取出的索引值就是sys_str[index]
a
0x61
0x1
a
b
0x62
0x2
d
c
0x63
0x3
u
d
0x64
0x4
i
e
0x65
0x5
e
f
0x66
0x6
r
也就是說最後
my_str = "aduier"
。字串檢查
觀察後面發現呼叫
strings_not_equal
,因此一如既往的取檢查第 1 行0x40245e
記憶體位置存放的字串。因此我們知道剛剛的字串就是要和 "flyers" 是一樣的,所以可以將剛剛那個表反過來推:
<–––––––––––––––––––––––––––––––––––––––––––––
sys_str[index]
i
0x69
0x9
f
o
0x6F
0xF
l
n
0x6E
0xE
y
e
0x65
0x5
e
f
0x66
0x6
r
g
0x67
0x7
s
當然因為 ASCII code 的特性,答案也可以是大寫的:
phase_6
這題是最難的一題,我們慢慢看。
可以看到整個函式非常地長,因此要拆成幾個部份來看。在第 10 行讀取 6 個數字已經在前面出現過了,就不多講了。
Part 1
讀取六個數字出來後(
0x40110b <+23>
),可以看到是對讀取的數字做某些操作,然後迴圈回去。因此我們嘗試寫出簡單的程式邏輯:因此可以看出這出迴圈的樣子,所以改寫成以下樣子。其中
i
就是r12d
,j
是rbx
。觀察上面程式會發現,每個輸入的數字
N - 1
不能大於 5,且輸入的 6 個數字不能有重複的。編譯器通常會盡可能的重複利用暫存器,和程式語言的變數想法常常會有出入,因此需要記得暫存器的前因後果會比較好的理解組合語言。
Part 2
我一樣將其寫成以下型態,可以看到它會把輸入的 6 個數字全部變成
7 - N
。Part 3
我們還是一樣把它轉成好看的樣子:
rsi
作為計數器,迴圈執行 6 次ecx
為第 n 個數字7 - n
,因此只有當輸入是 6 時會進入第一個分支,其他則進入第二個分支。0x6032d0
給edx
0x6032d0
給edx
。但會多跑一個迴圈,我們知道這個分支只有輸入為 \(1\leq n\leq 5\) 的情況才會進入,換算成ecx
就是 \(6\leq ecx\leq 2\) ,而這也作為這個迴圈的計數次數。迴圈裡面執行的是以這個地址基底每次加 8 取出值,但根本看不出個所以來,因此需要將這個地址相關的值抓出來看。print *(int *)0x6032d0
:0x6032d0
0x6032d8
0x6032e0
0x6032e0
0x6032e8
0x6032f0
0x6032f0
0x6032f8
0x603300
0x603300
0x603308
0x603310
0x603310
0x603318
0x603320
0x603320
仔細觀察就發現這就是鍊結串列的操作,
0x6032d8
存放著0x6032e0
地址,也就是168
的地址,嘗試寫成以下程式碼:所以現在可以來窮舉所有的可能了。因為
eax
初始化為 1,因此每次迴圈只會跑ecx - 1
次。ecx
rdx
最後把值放入堆疊裡面,繼續下一個輸入數字。注意到它儲存在堆疊的
rsp + 32 + 2 * rsi
,而rsi
為 4 的倍數,所以存放的是以 8 為倍數,原因當然是因為這個是一個結構體。Part 4
一樣變成好看的樣子:
ebp
作為迴圈計數器,一共執行 4 次rbx
每次取出目前 index 的值 ,對應到先輸入的值eax
每次取出下一個 index 的值rbx
指向下一個值因此可以理解成我們要的是一個從大到小的數列。我們再根據上面表格:
ecx
rdx
可以得到,若是想讓最後一列是以大到小的方式排列,輸入值要是
4 3 2 1 6 5
。這也就是本題最後的答案了。完整答案
全部輸入成功後,就會得到成功訊息。