Bomb Lab 屬於《深入理解計算機系統》第三章 —— 程序的機器級表示,主要通過練習加深彙編知識的相關記憶。《CSAPP》- Bomb Lab (上) 中介紹了前三關的具體解答,本文繼續解答後三關和隱藏關。
繼續闖關獲取第四個字符串我們在通過前面的步驟成功獲取了前三個字符串,此時我們可以開始獲取第四個字符串。讓我們重新運行程序 (gdb bomb),在 phase_4 入口處打上斷點,然後運行程序使其進入函數 phase_4,並獲取 phase_4 彙編代碼:
...Breakpoint 1, 0x000000000040100c in phase_4 ()# 獲取函數 phase_4 的彙編代碼(gdb) disasDump of assembler code for function phase_4:=> 0x40100c <+0>: sub $0x18,%rsp 0x401010 <+4>: lea 0xc(%rsp),%rcx 0x401015 <+9>: lea 0x8(%rsp),%rdx 0x40101a <+14>: mov $0x4025cf,%esi 0x40101f <+19>: mov $0x0,%eax 0x401024 <+24>: callq 0x400bf0 <__isoc99_sscanf@plt> 0x401029 <+29>: cmp $0x2,%eax 0x40102c <+32>: jne 0x401035 <phase_4+41> 0x40102e <+34>: cmpl $0xe,0x8(%rsp) 0x401033 <+39>: jbe 0x40103a <phase_4+46> 0x401035 <+41>: callq 0x40143a <explode_bomb> 0x40103a <+46>: mov $0xe,%edx 0x40103f <+51>: mov $0x0,%esi 0x401044 <+56>: mov 0x8(%rsp),%edi 0x401048 <+60>: callq 0x400fce <func4> 0x40104d <+65>: test %eax,%eax 0x40104f <+67>: jne 0x401058 <phase_4+76> 0x401051 <+69>: cmpl $0x0,0xc(%rsp) 0x401056 <+74>: je 0x40105d <phase_4+81> 0x401058 <+76>: callq 0x40143a <explode_bomb> 0x40105d <+81>: add $0x18,%rsp 0x401061 <+85>: retqEnd of assembler dump.0x40100c <+0> ~ 0x40102c <+32>: 和前面類似,主要是通過 sscanf 獲取輸入字符串的兩個 32 位有符號整數,假設分別存儲在 a (0x8(%rsp)) 和 b (0xc(%rsp)) 中。如果沒有成功獲取兩個整數,那麼就會引爆炸彈,無法繼續運行。
0x40102e <+34> ~ 0x401033 <+39>: 主要是判斷 0 <= a <= 14 是否成立(注意跳轉使用的是 jbe 指令,所以是把 a 當作無符號數時必須小於等於 14 ,即 a 必須非負且小於等於 14),成立則跳轉至 0x40103a <+46> 繼續運行;不成立則會引爆炸彈,無法繼續運行。
0x40103a <+46> ~ 0x401048 <+60>: 主要是調用函數 func4 ,該函數籤名大致為 int func4(int i, int j, int k) 。我們調用語句大致為 int c = func4(a, 0, 14); 。
0x40104d <+65> ~ 0x40104f <+67>: 判斷 c == 0 是否成立 ,不成立則引爆炸彈,成立時繼續運行。此時我們確定 func4 函數的調用結果必須為 0 ,從而反推出第一個數 (a) 的值。
0x401051 <+69> ~ 0x401056 <+74>: 判斷 b == 0 是否成立,不成立則引爆炸彈,成立時繼續運行並可順利結束。此時我們已經確定了第二個數 (b) 為 0 。
此時我們已經確認了第二個數的值,還需確認第一個數的值。我們需要給 func4 入口處打上斷點,並運行至 func4 入口處,查看 func4 的彙編代碼:
# 在函數 func4 入口處打上斷點(gdb) break func4Breakpoint 2 at 0x400fce# 繼續運行至 func4 處打上斷點(gdb) continueContinuing.
Breakpoint 2, 0x0000000000400fce in func4 ()# 獲取函數 func4 的彙編代碼(gdb) disasDump of assembler code for function func4:=> 0x400fce <+0>: sub $0x8,%rsp 0x400fd2 <+4>: mov %edx,%eax 0x400fd4 <+6>: sub %esi,%eax 0x400fd6 <+8>: mov %eax,%ecx 0x400fd8 <+10>: shr $0x1f,%ecx 0x400fdb <+13>: add %ecx,%eax 0x400fdd <+15>: sar %eax 0x400fdf <+17>: lea (%rax,%rsi,1),%ecx 0x400fe2 <+20>: cmp %edi,%ecx 0x400fe4 <+22>: jle 0x400ff2 <func4+36> 0x400fe6 <+24>: lea -0x1(%rcx),%edx 0x400fe9 <+27>: callq 0x400fce <func4> 0x400fee <+32>: add %eax,%eax 0x400ff0 <+34>: jmp 0x401007 <func4+57> 0x400ff2 <+36>: mov $0x0,%eax 0x400ff7 <+41>: cmp %edi,%ecx 0x400ff9 <+43>: jge 0x401007 <func4+57> 0x400ffb <+45>: lea 0x1(%rcx),%esi 0x400ffe <+48>: callq 0x400fce <func4> 0x401003 <+53>: lea 0x1(%rax,%rax,1),%eax 0x401007 <+57>: add $0x8,%rsp 0x40100b <+61>: retqEnd of assembler dump.這一段代碼比較繁瑣,而且涉及到遞歸調用,直接根據每塊指令看不太容易理解,所以直接可以先直譯出相關的 C 代碼:
inf func4(int i, int j, int k) { int l = k - j;
int m = (l >> 31) & 0x1;
l = (l + m) >> 1;
m = l + j;
if (l <= i) {
if (l >= i) {
return 0; } else {
return 2 * func4(i, j + 1, k) + 1 } } else {
return 2 * func4(i, j, k - 1) }}直譯後的代碼還是有點難懂(難怪 main.c 裡要問數學好不好),但是我們知道這個函數需要返回 0 才行,找到對應的分支語句,可以反推出 i == l , i 就是我們需要確定的值,而 l 只與 j, k 有關,並且第一次調用 func4 時 j = 0, k = 14 ,根據函數最開始的語句可以算出 l = 7 ,所以 i 的值也為 7 ,並且滿足 0 <= i <= 14 ,所以 i = 7 是一個合法的解。(已經獲取了 func4 的函數,所以可以直接枚舉 i = 0 ~ 14 ,可得合法的解為: 0, 1, 3, 7)
至此我們已經推斷出了字符串中兩個數字的值分別為 7 和 0 ,那麼第四個字符串為: 7 0 。
獲取第五個字符串我們在通過前面的步驟成功獲取了前四個字符串,此時我們可以開始獲取第五個字符串。讓我們重新運行程序 (gdb bomb),在 phase_5 入口處打上斷點,然後運行程序使其進入函數 phase_5,並獲取 phase_5 彙編代碼:
...Breakpoint 1, 0x0000000000401062 in phase_5 ()# 獲取函數 phase_5 的彙編代碼(gdb) disasDump of assembler code for function phase_5:=> 0x401062 <+0>: push %rbx 0x401063 <+1>: sub $0x20,%rsp 0x401067 <+5>: mov %rdi,%rbx 0x40106a <+8>: mov %fs:0x28,%rax 0x401073 <+17>: mov %rax,0x18(%rsp) 0x401078 <+22>: xor %eax,%eax 0x40107a <+24>: callq 0x40131b <string_length> 0x40107f <+29>: cmp $0x6,%eax 0x401082 <+32>: je 0x4010d2 <phase_5+112> 0x401084 <+34>: callq 0x40143a <explode_bomb> 0x401089 <+39>: jmp 0x4010d2 <phase_5+112> 0x40108b <+41>: movzbl (%rbx,%rax,1),%ecx 0x40108f <+45>: mov %cl,(%rsp) 0x401092 <+48>: mov (%rsp),%rdx 0x401096 <+52>: and $0xf,%edx 0x401099 <+55>: movzbl 0x4024b0(%rdx),%edx 0x4010a0 <+62>: mov %dl,0x10(%rsp,%rax,1) 0x4010a4 <+66>: add $0x1,%rax 0x4010a8 <+70>: cmp $0x6,%rax 0x4010ac <+74>: jne 0x40108b <phase_5+41> 0x4010ae <+76>: movb $0x0,0x16(%rsp) 0x4010b3 <+81>: mov $0x40245e,%esi 0x4010b8 <+86>: lea 0x10(%rsp),%rdi 0x4010bd <+91>: callq 0x401338 <strings_not_equal> 0x4010c2 <+96>: test %eax,%eax 0x4010c4 <+98>: je 0x4010d9 <phase_5+119> 0x4010c6 <+100>: callq 0x40143a <explode_bomb> 0x4010cb <+105>: nopl 0x0(%rax,%rax,1) 0x4010d0 <+110>: jmp 0x4010d9 <phase_5+119> 0x4010d2 <+112>: mov $0x0,%eax 0x4010d7 <+117>: jmp 0x40108b <phase_5+41> 0x4010d9 <+119>: mov 0x18(%rsp),%rax 0x4010de <+124>: xor %fs:0x28,%rax 0x4010e7 <+133>: je 0x4010ee <phase_5+140> 0x4010e9 <+135>: callq 0x400b30 <__stack_chk_fail@plt> 0x4010ee <+140>: add $0x20,%rsp 0x4010f2 <+144>: pop %rbx 0x4010f3 <+145>: retqEnd of assembler dump.0x40106a <+8> ~ 0x401073 <+17>: 看到 mov %fs:0x28,%rax 就想起了這是棧破壞檢測的相關代碼 (P199) ,主要是檢測棧是否被破壞,也說明了我們剛剛定義了一個數組,並且這個數組在後面會作為參數傳入一個函數中
0x401078 <+22> ~ 0x401082 <+32>: 主要判斷我們輸入的字符串的長度是否為 6 ,不為 6 則引爆炸彈,否則繼續運行後續代碼
0x4010d2 <+112> ~ 0x4010d7 <+117>: 將寄存器 %eax 賦值為 0 (書中 P123 提到 movl 指令以寄存器作為目的時,它會把該寄存器的高位 4 字節設置為 0 ,因此沒必要使用 movq $0x0, %rax)
0x40108b <+41> ~ 0x4010ae <+76>: 這一段是循環,主要是遍歷輸入字符串的 6 個字符 ch ,並取該字符的低 4 位作為一個下標 index ,然後將相對地址 0x4024b0 偏移為 index 的一個字節賦值給我們定義的數組中的對應位置(假設輸入字符串為 s ,棧中數組的定義語句為 char d[7]; ,地址 0x4024b0 對應的字符數組為 chs ,那麼這一段循環的意思為 d[i] = chs[s[i] & 0xf];)
•0x40108b <+41>: 使用了 MOVZ 類指令,這類指令會把目的中剩餘的字節填充為 0 ;與其對應的 MOVS 類指令會通過符號擴展填充剩餘的字節(即將源操作數的最高位進行賦值填充到目的中剩餘的字節) P123•print * (char *) 0x4024b0@16: 查看地址 0x4024b0 開始的 8 個字符為 maduiersnfotvbyl•0x4010ae <+76>: d[6] = '\0' ,即把我們棧中定義的字符數組最後設置為 0 ,表示其代表的字符串長度為 6
0x4010b3 <+81> ~ 0x4010c4 <+98>: 判斷字符串 d 與以地址 0x40245e 開始的字符串是否相等,不相等則引爆炸彈,相等則繼續運行校驗棧是否被破壞,然後通過第五個字符串的驗證
•print * (char *) 0x40245e@7: 查看地址 0x40245e 開始的 7 個字符為 flyers (注意最後一個 \0 沒有顯示)
綜上可知:我們需要生成的字符串 d 為 flyers ,可用於生成目標字符串的字母表為 maduiersnfotvbyl ,然後我們可以反推出我們可輸入的字符的低 4 位,然後對照 man ascii 指令輸出的表找出對應哪些可輸入的字符。
id[i]s[i] & 0xf可輸入字符0f0x9) 9 I Y i y1l0xf/ ? O _ o2y0xe. > N ^ n ~3e0x5% 5 E U e u4r0x6& 6 F V f v5s0x7' 7 G W g w第五個字符串每個位置任意選取對應的可輸入字符即可通過第五個校驗。
獲取第六個字符串我們在通過前面的步驟成功獲取了前五個字符串,此時我們可以開始獲取第六個字符串。讓我們重新運行程序 (gdb bomb),在 phase_6 入口處打上斷點,然後運行程序使其進入函數 phase_6,並獲取 phase_6 彙編代碼:
...Breakpoint 1, 0x00000000004010f4 in phase_6 ()(gdb) disasDump of assembler code for function phase_6:=> 0x4010f4 <+0>: push %r14 0x4010f6 <+2>: push %r13 0x4010f8 <+4>: push %r12 0x4010fa <+6>: push %rbp 0x4010fb <+7>: push %rbx 0x4010fc <+8>: sub $0x50,%rsp 0x401100 <+12>: mov %rsp,%r13 0x401103 <+15>: mov %rsp,%rsi 0x401106 <+18>: callq 0x40145c <read_six_numbers> 0x40110b <+23>: mov %rsp,%r14 0x40110e <+26>: mov $0x0,%r12d 0x401114 <+32>: mov %r13,%rbp 0x401117 <+35>: mov 0x0(%r13),%eax 0x40111b <+39>: sub $0x1,%eax 0x40111e <+42>: cmp $0x5,%eax 0x401121 <+45>: jbe 0x401128 <phase_6+52> 0x401123 <+47>: callq 0x40143a <explode_bomb> 0x401128 <+52>: add $0x1,%r12d 0x40112c <+56>: cmp $0x6,%r12d 0x401130 <+60>: je 0x401153 <phase_6+95> 0x401132 <+62>: mov %r12d,%ebx 0x401135 <+65>: movslq %ebx,%rax 0x401138 <+68>: mov (%rsp,%rax,4),%eax 0x40113b <+71>: cmp %eax,0x0(%rbp) 0x40113e <+74>: jne 0x401145 <phase_6+81> 0x401140 <+76>: callq 0x40143a <explode_bomb> 0x401145 <+81>: add $0x1,%ebx 0x401148 <+84>: cmp $0x5,%ebx 0x40114b <+87>: jle 0x401135 <phase_6+65> 0x40114d <+89>: add $0x4,%r13 0x401151 <+93>: jmp 0x401114 <phase_6+32> 0x401153 <+95>: lea 0x18(%rsp),%rsi 0x401158 <+100>: mov %r14,%rax 0x40115b <+103>: mov $0x7,%ecx 0x401160 <+108>: mov %ecx,%edx 0x401162 <+110>: sub (%rax),%edx 0x401164 <+112>: mov %edx,(%rax) 0x401166 <+114>: add $0x4,%rax 0x40116a <+118>: cmp %rsi,%rax 0x40116d <+121>: jne 0x401160 <phase_6+108> 0x40116f <+123>: mov $0x0,%esi 0x401174 <+128>: jmp 0x401197 <phase_6+163> 0x401176 <+130>: mov 0x8(%rdx),%rdx 0x40117a <+134>: add $0x1,%eax 0x40117d <+137>: cmp %ecx,%eax 0x40117f <+139>: jne 0x401176 <phase_6+130> 0x401181 <+141>: jmp 0x401188 <phase_6+148> 0x401183 <+143>: mov $0x6032d0,%edx 0x401188 <+148>: mov %rdx,0x20(%rsp,%rsi,2) 0x40118d <+153>: add $0x4,%rsi 0x401191 <+157>: cmp $0x18,%rsi 0x401195 <+161>: je 0x4011ab <phase_6+183> 0x401197 <+163>: mov (%rsp,%rsi,1),%ecx 0x40119a <+166>: cmp $0x1,%ecx 0x40119d <+169>: jle 0x401183 <phase_6+143> 0x40119f <+171>: mov $0x1,%eax 0x4011a4 <+176>: mov $0x6032d0,%edx 0x4011a9 <+181>: jmp 0x401176 <phase_6+130> 0x4011ab <+183>: mov 0x20(%rsp),%rbx 0x4011b0 <+188>: lea 0x28(%rsp),%rax 0x4011b5 <+193>: lea 0x50(%rsp),%rsi 0x4011ba <+198>: mov %rbx,%rcx 0x4011bd <+201>: mov (%rax),%rdx 0x4011c0 <+204>: mov %rdx,0x8(%rcx) 0x4011c4 <+208>: add $0x8,%rax 0x4011c8 <+212>: cmp %rsi,%rax 0x4011cb <+215>: je 0x4011d2 <phase_6+222> 0x4011cd <+217>: mov %rdx,%rcx 0x4011d0 <+220>: jmp 0x4011bd <phase_6+201> 0x4011d2 <+222>: movq $0x0,0x8(%rdx) 0x4011da <+230>: mov $0x5,%ebp 0x4011df <+235>: mov 0x8(%rbx),%rax 0x4011e3 <+239>: mov (%rax),%eax 0x4011e5 <+241>: cmp %eax,(%rbx) 0x4011e7 <+243>: jge 0x4011ee <phase_6+250> 0x4011e9 <+245>: callq 0x40143a <explode_bomb> 0x4011ee <+250>: mov 0x8(%rbx),%rbx 0x4011f2 <+254>: sub $0x1,%ebp 0x4011f5 <+257>: jne 0x4011df <phase_6+235> 0x4011f7 <+259>: add $0x50,%rsp 0x4011fb <+263>: pop %rbx 0x4011fc <+264>: pop %rbp 0x4011fd <+265>: pop %r12 0x4011ff <+267>: pop %r13 0x401201 <+269>: pop %r14 0x401203 <+271>: retqEnd of assembler dump.第一眼看到這麼長的彙編代碼有點懵,但大概一看沒有太複雜的邏輯,還有前面遇到過的函數,所以還是冷靜下來繼續分析,由於拆開很難看懂,所以還是需要直譯成 C 代碼再研究(部分判斷轉換成等價的語句,減少縮進)。為了細化每一塊的功能,將把原始碼拆成多部分進行分析。
0x401100 <+12> ~ 0x401106 <+18>: 調用函數 read_six_numbers 從輸入的字符串讀入 6 個數字,並按順序存儲在 nums 中
直譯的 C 代碼如下:
void phase_6(char *s) { int *ap, *bp, *endp; int c, d, e; struct Node **nextpp, **endpp; struct Node *nodep, *curp; struct Node *arr[6]; int nums[6];
ap = nums;
read_six_numbers(s, nums); ...}0x40110b <+23> ~ 0x40114d <+89>: 主要是判斷 nums 中的數字是否全都滿足 0 <= nums[i] <= 6 且互不相等
直譯的 C 代碼如下:
... bp = nums; c = 0;
whilt (true) { if ((unsigned) (*ap - 1) > 5)) { explode_bomb(); }
c += 1; if (c == 6) { break; }
d = c; do { if (nums[d] == *ap) { explode_bomb(); }
d += 1;
} while(d <= 5);
ap += 1; } ...}0x401153 <+95> ~ 0x40116d <+121>: 重新計算每個數組中的數字: nums[i] = 7 - nums[i]
直譯的 C 代碼如下:
... endp = nums + 6; ap = bp; c = 7;
do { *ap = c - *ap;
ap += 1;
} while(ap != endp); ...0x40116f <+123> ~ 0x4011a9 <+181>: 這一段有點繞,第一次看的時候還丟了一段代碼,導致後面怎麼都對不上。主要還是缺少類型信息,不知道某些地址對應的數據類型,導致推理走了歧路。
這一段代碼中出現了一個地址 0x6032d0 ,我們可以使用 x /14g 0x6032d0 查看裡面存儲的數據:
(gdb) x /14g 0x6032d00x6032d0 <node1>: 0x000000010000014c 0x00000000006032e00x6032e0 <node2>: 0x00000002000000a8 0x00000000006032f00x6032f0 <node3>: 0x000000030000039c 0x00000000006033000x603300 <node4>: 0x00000004000002b3 0x00000000006033100x603310 <node5>: 0x00000005000001dd 0x00000000006033200x603320 <node6>: 0x00000006000001bb 0x00000000000000000x603330: 0x0000000000000000 0x0000000000000000這裡面存儲了 6 個 Node 類型的數據,可以發現後面的數字是一個地址,且為後面一個 Node 的地址,因此可以推導出 Node 類型及預先定義的一個 Node 鍊表:
struct Node { long val; struct Node *next;};
struct Node nodes[6];有了以上信息,我們就能懂得這部分彙編代碼的含義了:將鍊表的第 nums[c] 個 Node 賦值給 arr[c] ,即: arr[c] = nodes[nums[c]]
直譯的 C 代碼如下:
... c = 0;
do { d = nums[c];
if (d <= 1) { nodep = nodes[0]; } else { e = 1; nodep = nodes;
do { nodep = nodep -> next; e += 1;
} while(e != d); } arr[c] = nodep;
} while(c != 6); ...0x4011ab <+183> ~ 0x4011d2 <+222>:主要是將原鍊表中的 Node 按照目前在 arr 的順序重新連起來,即: arr[i] -> next = arr[i + 1]
直譯的 C 代碼如下:
... nodep = arr[0]; nextpp = arr + 1; endpp = arr + 6; curp = nextpp;
while(true) { curp -> next = *nextpp;
nextpp += 1;
if (nextpp == endpp) { break; }
curp = *nextpp; }
node_gp -> next = NULL; ...0x4011da <+230> ~ 0x4011f5 <+257>: 這一段主要是判斷新鍊表中的值 val 強轉成 32 位有符號整型後是不是嚴格遞減的
... c = 5; do { int d = (int) (nodep -> next -> val)
if (d >= (int) (nodep -> val)) { explode_bomb(); }
nodep = nodep -> next; c -= 1; } while(c != 0);}至此我們已經知道 phase_6 中每一部分的邏輯,串起來就是:先從輸入的字符串中讀入 6 個在範圍 [1, 6] 內且互不相同數字,按順序放入 nums 數組中;再對數組中的每個數字執行以下操作: nums[i] = 7 - nums[i] ;然後將預先定義的一個鍊表 nodes 按照新順序重新連起來,將原來的第 nums[i] 個節點放在第 i 個位置;最後判斷新順序下鍊表節點中的值強轉成 32 位有符號整型後是否為嚴格遞減,嚴格遞減的通過本關,否則引爆炸彈。
原鍊表中的數字順序為: 0x14c -> 0x0a8 -> 0x39c -> 0x2b3 -> 0x1dd -> 0x1bb
可以通過數字數組 nums = {3, 4, 5, 6, 1, 2} 將其轉換為降序順序,由於前面還執行了 nums[i] = 7 - nums[i] ,所以實際讀取的數字數組為: nums = {4, 3, 2, 1, 6, 5} ,對應的輸入字符串為: 4 3 2 1 6 5
進入隱藏關至此我們已經通過全部六關,但 main.c 最後還有一段注釋提示我們可能忽略了什麼:
Wow, they got it! But isn't something... missing? Perhaps something they overlooked? Mua ha ha ha ha!
我們再回看以下前面六關中 main 函數內的代碼,可以發現每一關都是先調用 read_line 函數讀取輸入字符串 ,然後傳入每關的校驗函數 phase_x ,再然後調用了同一個方法 phase_defused ,最後在提示通過了當前關。
回憶一下我們的校驗函數,裡面都直接對輸入的字符串進行了校驗,不需要後面再進行處理,也就是後面的 phase_defused 可能另有奧秘(最開始我還以為這個函數是進行後續校驗的),所以我們先看看這個函數究竟做了什麼事情。
Breakpoint 1, 0x00000000004015c4 in phase_defused ()# 查看 phase_defused 的彙編代碼(gdb) disasDump of assembler code for function phase_defused:=> 0x4015c4 <+0>: sub $0x78,%rsp 0x4015c8 <+4>: mov %fs:0x28,%rax 0x4015d1 <+13>: mov %rax,0x68(%rsp) 0x4015d6 <+18>: xor %eax,%eax 0x4015d8 <+20>: cmpl $0x6,0x202181(%rip) # 0x603760 <num_input_strings> 0x4015df <+27>: jne 0x40163f <phase_defused+123> 0x4015e1 <+29>: lea 0x10(%rsp),%r8 0x4015e6 <+34>: lea 0xc(%rsp),%rcx 0x4015eb <+39>: lea 0x8(%rsp),%rdx 0x4015f0 <+44>: mov $0x402619,%esi 0x4015f5 <+49>: mov $0x603870,%edi 0x4015fa <+54>: callq 0x400bf0 <__isoc99_sscanf@plt> 0x4015ff <+59>: cmp $0x3,%eax 0x401602 <+62>: jne 0x401635 <phase_defused+113> 0x401604 <+64>: mov $0x402622,%esi 0x401609 <+69>: lea 0x10(%rsp),%rdi 0x40160e <+74>: callq 0x401338 <strings_not_equal> 0x401613 <+79>: test %eax,%eax 0x401615 <+81>: jne 0x401635 <phase_defused+113> 0x401617 <+83>: mov $0x4024f8,%edi 0x40161c <+88>: callq 0x400b10 <puts@plt> 0x401621 <+93>: mov $0x402520,%edi 0x401626 <+98>: callq 0x400b10 <puts@plt> 0x40162b <+103>: mov $0x0,%eax 0x401630 <+108>: callq 0x401242 <secret_phase> 0x401635 <+113>: mov $0x402558,%edi 0x40163a <+118>: callq 0x400b10 <puts@plt> 0x40163f <+123>: mov 0x68(%rsp),%rax 0x401644 <+128>: xor %fs:0x28,%rax 0x40164d <+137>: je 0x401654 <phase_defused+144> 0x40164f <+139>: callq 0x400b30 <__stack_chk_fail@plt> 0x401654 <+144>: add $0x78,%rsp 0x401658 <+148>: retqEnd of assembler dump.0x401630 <+108>: callq 0x401242 <secret_phase> 這一句看起來會調用一個隱藏關的函數,所以我們需要先破解當前函數使得我們可以進入隱藏關。
0x4015d8 <+20> ~ 0x4015df <+27>: 判斷 0x202181(%rip) 中的值是否為 6 ,不為 6 則直接返回當前函數,否則會執行函數體。通過注釋 0x603760 <num_input_strings> 我們可以判斷出這個值記錄的是輸入的字符串的個數,只有當輸入 6 個字符串後才有可能進入隱藏關,即我們必須通過前六關。
0x4015e1 <+29> ~ 0x401602 <+62>: 從 0x603870 開始的字符串讀入 3 個參數,前兩個是有符號整型,第三個是字符串,如果未成功讀入 3 個參數,則直接返回
•x /s 0x402619: 查看模式串為 %d %d %s ,從而可以得出三個變量的類型分別為:int, int, char *•x /s 0x603870: 當我們打上斷點,在通過第六關後進入 phase_defused ,運行這個命令可得字符串 7 0 ,這個看起來是第四關的字符串,那麼就需要我們將第四關的字符串替換為 7 0 DrEvil ,這樣我們就能成功進入隱藏關
0x401604 <+64> ~ 0x401615 <+81>: 判斷剛剛讀入的第三個字符串是否等於 DrEvil ,不等則直接返回
•x /s 0x402622: 可知第三個字符串需要和 DrEvil 相等
0x401617 <+83> ~ 0x40163a <+118>: 輸出兩端話提示我們進入隱藏關,然後通過隱藏關後再提示我們成功拆除炸彈
破解隱藏關main.c 中開始可以使用兩種方式讀入字符串,第一種是使用標準輸入,第二種是從文件中讀入,我們可以將字符串都存入 in.txt 中,然後通過運行 (gdb) run in.txt 避免多次重複輸入字符串
# 直接查看 0x401242 所在函數的彙編代碼(gdb) disas 0x401242Dump of assembler code for function secret_phase: 0x401242 <+0>: push %rbx 0x401243 <+1>: callq 0x40149e <read_line> 0x401248 <+6>: mov $0xa,%edx 0x40124d <+11>: mov $0x0,%esi 0x401252 <+16>: mov %rax,%rdi 0x401255 <+19>: callq 0x400bd0 <strtol@plt> 0x40125a <+24>: mov %rax,%rbx 0x40125d <+27>: lea -0x1(%rax),%eax 0x401260 <+30>: cmp $0x3e8,%eax 0x401265 <+35>: jbe 0x40126c <secret_phase+42> 0x401267 <+37>: callq 0x40143a <explode_bomb> 0x40126c <+42>: mov %ebx,%esi 0x40126e <+44>: mov $0x6030f0,%edi 0x401273 <+49>: callq 0x401204 <fun7> 0x401278 <+54>: cmp $0x2,%eax 0x40127b <+57>: je 0x401282 <secret_phase+64> 0x40127d <+59>: callq 0x40143a <explode_bomb> 0x401282 <+64>: mov $0x402438,%edi 0x401287 <+69>: callq 0x400b10 <puts@plt> 0x40128c <+74>: callq 0x4015c4 <phase_defused> 0x401291 <+79>: pop %rbx 0x401292 <+80>: retqEnd of assembler dump.0x401243 <+1> ~ 0x401255 <+19>: 讀入一個字符串並將其按照 10 進位對待轉換成一個長整型 num
0x40125a <+24> ~ 0x401267 <+37>: 判斷 (unsigned) (num - 1) <= 0x3e8 是否成立,不成立則引爆炸彈
0x40126c <+42> ~ 0x40128c <+74>: 調用函數 fun7(0x6030f0, num) ,判斷返回值是否為 2 ,是則提示成功拆除炸彈,不是則引爆炸彈
# 直接查看 0x401204 所在函數的彙編代碼(gdb) disas 0x401204Dump of assembler code for function fun7: 0x401204 <+0>: sub $0x8,%rsp 0x401208 <+4>: test %rdi,%rdi 0x40120b <+7>: je 0x401238 <fun7+52> 0x40120d <+9>: mov (%rdi),%edx 0x40120f <+11>: cmp %esi,%edx 0x401211 <+13>: jle 0x401220 <fun7+28> 0x401213 <+15>: mov 0x8(%rdi),%rdi 0x401217 <+19>: callq 0x401204 <fun7> 0x40121c <+24>: add %eax,%eax 0x40121e <+26>: jmp 0x40123d <fun7+57> 0x401220 <+28>: mov $0x0,%eax 0x401225 <+33>: cmp %esi,%edx 0x401227 <+35>: je 0x40123d <fun7+57> 0x401229 <+37>: mov 0x10(%rdi),%rdi 0x40122d <+41>: callq 0x401204 <fun7> 0x401232 <+46>: lea 0x1(%rax,%rax,1),%eax 0x401236 <+50>: jmp 0x40123d <fun7+57> 0x401238 <+52>: mov $0xffffffff,%eax 0x40123d <+57>: add $0x8,%rsp 0x401241 <+61>: retqEnd of assembler dump.0x401208 <+4> ~ 0x40120b <+7>: 判斷第一個參數是否為 0 ,是則直接返回 0xffffffff ,不是則繼續運行
現在基本可以判斷第一個參數是一個指針,剛剛的語句在判斷指針是否為空,目前還需要判斷這個指針指向的數據類型。
(gdb) x /64xg 0x6030f00x6030f0 <n1>: 0x24 0x6031100x603100 <n1+16>: 0x603130 0x00x603110 <n21>: 0x8 0x6031900x603120 <n21+16>: 0x603150 0x00x603130 <n22>: 0x32 0x6031700x603140 <n22+16>: 0x6031b0 0x00x603150 <n32>: 0x16 0x6032700x603160 <n32+16>: 0x603230 0x00x603170 <n33>: 0x2d 0x6031d00x603180 <n33+16>: 0x603290 0x00x603190 <n31>: 0x6 0x6031f00x6031a0 <n31+16>: 0x603250 0x00x6031b0 <n34>: 0x6b 0x6032100x6031c0 <n34+16>: 0x6032b0 0x00x6031d0 <n45>: 0x28 0x00x6031e0 <n45+16>: 0x0 0x00x6031f0 <n41>: 0x1 0x00x603200 <n41+16>: 0x0 0x00x603210 <n47>: 0x63 0x00x603220 <n47+16>: 0x0 0x00x603230 <n44>: 0x23 0x00x603240 <n44+16>: 0x0 0x00x603250 <n42>: 0x7 0x00x603260 <n42+16>: 0x0 0x00x603270 <n43>: 0x14 0x00x603280 <n43+16>: 0x0 0x00x603290 <n46>: 0x2f 0x00x6032a0 <n46+16>: 0x0 0x00x6032b0 <n48>: 0x3e9 0x00x6032c0 <n48+16>: 0x0 0x00x6032d0 <node1>: 0x10000014c 0x6032e00x6032e0 <node2>: 0x2000000a8 0x6032f0看起來這個數據類型佔 32 字節,第一個存儲一個數,後面兩個存儲指針,分別指向相同的類型,所以可以猜測這是一個二叉樹節點。通過 0x40120d <+9>: mov (%rdi),%edx 可以猜測第一個欄位是整型,而後面兩個欄位分別是左右子節點,剩餘的字節未使用。因此這個節點的定義大致如下:
type TreeNode { int val; struct *TreeNode left, right;};現在我們可以直譯出 func7 對應的 C 代碼了:
int func7 (TreeNode *root, int num) { if (root == NULL) { return 0xffffffff; }
if (root -> val <= num) { if (root -> val == num) { return 0; } else { return 2 * fun7(root -> right, num) + 1 } } else { return 2 * fun7(root -> left); }}至此我們知道了 fun7 的代碼,並且需要 fun7(0x6030f0, num) 的返回值為 2 ,為了方便計算,需要將樹中的每個值畫出來
這是一個四層的滿二叉樹,根據遞歸的計算方式和所需的最終返回值可以繼續推導:
第一層:節點值為 0x24 ,需要返回 2 ,那麼就需要走 return 2 * fun7(root -> left) 這個邏輯,限定了 num < 0x24
第二層:節點值為 0x8 ,需要返回 1 ,那麼需要走 return 2 * fun7(root -> right, num) + 1 這個邏輯,限定了 num > 0x8
第三層:節點值為 0x16 ,需要返回 0 ,有兩個邏輯可走,限定了 num <= 0x16
•走 return 0 這個邏輯:限定了 num = 0x16•走 return 2 * fun7(root -> left) 這個邏輯,限定了 num < 0x16
第四層:節點值為 0x14 ,需要返回 0 ,那麼需要走 return 0 這個邏輯,限定了 num = 0x14
綜上可知,隱藏關有兩個解:0x14 和 0x16 ,對應的十進位數分別為: 20 和 22
小結前五關都是平鋪直述的各種簡單語句,讓我信心大增,內心 OS :就這?第六關立刻教我做人,來來回回很久怎麼翻譯都不對(主要就是因為類型沒推斷出來,導致後續的推導不自洽),看來類型還是很重要的,如果沒有推斷出類型,那很容易誤入歧途。
這些關卡強化了彙編的相關記憶(基本指令的具體含義、 gdb 的各種基本命令以及常用的寄存器的含義),中後期基本不需要再翻書查閱了。