date: 2020-05-04
本實驗中博主采用對 objdump -D
令生成的文本進行分析求解(需要有基本的匯編知識,了解各種指令以及棧幀結構,之后的討論中不會對此進行介紹),gdb
調試僅在求解 secret_phase
時使用。
博主的部分數據有進行改動,故答案與官網的不同,但解法相同,可作參考學習。
- 實驗環境:Windows10 系統下 VMware 虛擬機 Ubuntu12.04 桌面版 32 位
- 原址鏈接:http://csapp.cs.cmu.edu/3e/labs.html
phase_1
phase_1
的主體是一個簡單的函數調用
block 1
08048b50 <phase_1>:
8048b50: 83 ec 1c sub $0x1c,%esp
; strings_not_equal(char*, "I was trying to give Tina Fey more material.")
8048b53: c7 44 24 04 64 a2 04 movl $0x804a264,0x4(%esp)
8048b5a: 08
8048b5b: 8b 44 24 20 mov 0x20(%esp),%eax; %eax = input
8048b5f: 89 04 24 mov %eax,(%esp); %eax = input
8048b62: e8 2d 05 00 00 call 8049094 <strings_not_equal>
首先調用 strings_not_equal
函數,分析 %esp
可以了解到它有兩個參數。第一個參數是 phase_1
傳入的字符串,第二個參數是應該是一個字面值,存儲在內存中的 0x804a264
處,如下:
804a261: 65 2e 00 49 20 gs add %cl,%cs:%gs:0x20(%ecx)
804a266: 77 61 ja 804a2c9 <_IO_stdin_used+0x1a5>
804a268: 73 20 jae 804a28a <_IO_stdin_used+0x166>
804a26a: 74 72 je 804a2de <_IO_stdin_used+0x1ba>
804a26c: 79 69 jns 804a2d7 <_IO_stdin_used+0x1b3>
804a26e: 6e outsb %ds:(%esi),(%dx)
804a26f: 67 20 74 6f and %dh,0x6f(%si)
804a273: 20 67 69 and %ah,0x69(%edi)
804a276: 76 65 jbe 804a2dd <_IO_stdin_used+0x1b9>
804a278: 20 54 69 6e and %dl,0x6e(%ecx,%ebp,2)
804a27c: 61 popa
804a27d: 20 46 65 and %al,0x65(%esi)
804a280: 79 20 jns 804a2a2 <_IO_stdin_used+0x17e>
804a282: 6d insl (%dx),%es:(%edi)
804a283: 6f outsl %ds:(%esi),(%dx)
804a284: 72 65 jb 804a2eb <_IO_stdin_used+0x1c7>
804a286: 20 6d 61 and %ch,0x61(%ebp)
804a289: 74 65 je 804a2f0 <_IO_stdin_used+0x1cc>
804a28b: 72 69 jb 804a2f6 <_IO_stdin_used+0x1d2>
804a28d: 61 popa
804a28e: 6c insb (%dx),%es:(%edi)
804a28f: 2e 00 00 add %al,%cs:(%eax)
轉換一下就能得到字符串 "I was trying to give Tina Fey more material."
。
block 2
; if (strings_not_equal(/**/))
8048b67: 85 c0 test %eax,%eax
8048b69: 74 05 je 8048b70 <phase_1+0x20>
; explode_bomb()
8048b6b: e8 36 06 00 00 call 80491a6 <explode_bomb>
8048b70: 83 c4 1c add $0x1c,%esp
8048b73: c3 ret
第二塊判斷 strings_not_equal
的返回值,若 true
則 explode_bomb
,若 false
則 return
說明 phase_1
的答案為 "I was trying to give Tina Fey more material."
。
code
以下是根據匯編指令還原的代碼(功能相同,但代碼不一定完全相同):
int string_length(char* str)
{
int count = 0;
while (str[count])
++count;
}
int strings_not_equal(char* str1, char* str2)
{
int len1 = string_length(str1), len2 = string_length(str2), i;
if (len1 != len2)
return 0;
for (i = 0; i < len1; ++i)
if (str1[i] != str2[i])
return 0;
return 1;
}
void phase_1(char* str)
{
if (!strings_not_equal(str, "I was trying to give Tina Fey more material."))
explode_bomb();
}
phase_2
phase_2
的主體是一個簡單的for
循環
block 1
08048b74 <phase_2>:
8048b74: 56 push %esi
8048b75: 53 push %ebx
8048b76: 83 ec 34 sub $0x34,%esp
; read_six_numbers(char* str, unsigned arr[])
8048b79: 8d 44 24 18 lea 0x18(%esp),%eax; %eax = arr
8048b7d: 89 44 24 04 mov %eax,0x4(%esp); 0x4(%esp) = arr
8048b81: 8b 44 24 40 mov 0x40(%esp),%eax; %eax = str = input
8048b85: 89 04 24 mov %eax,(%esp); (%esp) = str
8048b88: e8 4e 07 00 00 call 80492db <read_six_numbers>
首先調用 read_six_numbers
函數,分析 %esp
可以了解到它有兩個參數,第一個參數是 phase_2
傳入的字符串,第二個參數在此處暫時無法確定(暫且記作數組 arr
),但肯定是用於存放至少 6 個整數的。
block 2
; if (arr[0] != 1)
8048b8d: 83 7c 24 18 01 cmpl $0x1,0x18(%esp)
8048b92: 74 05 je 8048b99 <phase_2+0x25>
; explode_bomb()
8048b94: e8 0d 06 00 00 call 80491a6 <explode_bomb>
第二塊判斷第一個整數是否為 1
,若 false
則 explode_bomb
。
block 3
; for (p = arr + 1; p != arr + 6; p++)
8048b99: 8d 5c 24 1c lea 0x1c(%esp),%ebx; %ebx = arr + 1
8048b9d: 8d 74 24 30 lea 0x30(%esp),%esi; %esi = arr + 6
; if (*(p - 1) + *(p - 1) != *p)
8048ba1: 8b 43 fc mov -0x4(%ebx),%eax; %eax = *(p - 1)
8048ba4: 01 c0 add %eax,%eax; %eax = *(p - 1) + *(p - 1)
8048ba6: 39 03 cmp %eax,(%ebx)
8048ba8: 74 05 je 8048baf <phase_2+0x3b>
; explode_bomb()
8048baa: e8 f7 05 00 00 call 80491a6 <explode_bomb>
8048baf: 83 c3 04 add $0x4,%ebx
8048bb2: 39 f3 cmp %esi,%ebx
8048bb4: 75 eb jne 8048ba1 <phase_2+0x2d>
; loop for
8048bb6: 83 c4 34 add $0x34,%esp
8048bb9: 5b pop %ebx
8048bba: 5e pop %esi
8048bbb: c3 ret
- 第三塊首先將
0x1c(%esp)
和0x30(%esp)
分別存入了%ebx
和%esi
。0x1c(%esp)
是arr[1]
的地址,0x30(%esp)
是arr[6]
的地址 - 之后比較地址
%ebx - 4
中的值與地址%ebx
中的值(即比較arr[i]
與arr[i-1]
),若arr[i-1] + arr[i-1] != arr[i]
則explode_bomb
- 最后進行迭代循環,結束之后
return
總體分析,則第三塊代碼實現的功能就是判斷 arr
中后一個元素是否是前一個元素的兩倍,同時第二塊代碼中保證了 arr[0]
為 1
,所以可以得出答案為 1 2 4 8 16 32
。
code
arr
為何是 unsigned
而不是 int
在之后的 phase 中會得到體現。
void read_six_numbers(char* str, unsigned arr[])
{
if (sscanf(str, "%d %d %d %d %d %d", arr, arr + 1, arr + 2, arr + 3, arr + 4, arr + 5) <= 5)
explode_bomb();
}
void phase_2(char* str)
{
unsigned arr[6];
int* p;
read_six_numbers(str, arr);
if (arr[0] != 1)
explode_bomb();
for (p = arr + 1; p != arr + 6; p++)
if (*(p - 1) + *(p - 1) != *p)
explode_bomb();
}
phase_3
phase_3
的主體是一個switch...case
結構
block 1
08048bbc <phase_3>:
8048bbc: 83 ec 3c sub $0x3c,%esp
; sscanf(char* str, "%d %c %d", unsigned* pvar1, char* pvar2, int* pvar3)
8048bbf: 8d 44 24 28 lea 0x28(%esp),%eax; %eax = &var3
8048bc3: 89 44 24 10 mov %eax,0x10(%esp); pvar3 = &var3
8048bc7: 8d 44 24 2f lea 0x2f(%esp),%eax; %eax = &var2
8048bcb: 89 44 24 0c mov %eax,0xc(%esp); pvar2 = &var2
8048bcf: 8d 44 24 24 lea 0x24(%esp),%eax; %eax = &var1
8048bd3: 89 44 24 08 mov %eax,0x8(%esp); pvar1 = &var1
8048bd7: c7 44 24 04 ba a2 04 movl $0x804a2ba,0x4(%esp); "%d %c %d"
8048bde: 08
8048bdf: 8b 44 24 40 mov 0x40(%esp),%eax
8048be3: 89 04 24 mov %eax,(%esp)
8048be6: e8 85 fc ff ff call 8048870 <__isoc99_sscanf@plt>
; if (sscanf(/**/) <= 2)
8048beb: 83 f8 02 cmp $0x2,%eax
8048bee: 7f 05 jg 8048bf5 <phase_3+0x39>
; explode_bomb()
8048bf0: e8 b1 05 00 00 call 80491a6 <explode_bomb>
第一塊調用 sscanf
函數,傳入 5 個參數,分別是:
char*
:phase_3
的參數char*
:"%d %c %d"
unsigned
:var1
char
:var2
int
:var3
讀取完之后檢查讀取成功的個數,說明 phase_3
應當輸入 2 個整數和 1 個字符。
之后的代碼中分析可得第三個參數必須為 unsigned
block 2
; if (var1 <= 7)
8048bf5: 83 7c 24 24 07 cmpl $0x7,0x24(%esp)
8048bfa: 0f 87 f9 00 00 00 ja 8048cf9 <phase_3+0x13d>
; switch (var1)
8048c00: 8b 44 24 24 mov 0x24(%esp),%eax; %eax = var1
8048c04: ff 24 85 e0 a2 04 08 jmp *0x804a2e0(,%eax,4); *(0x08048c0b + 4 * var1)
; case 0
; if (var3 != 488)
8048c0b: b8 75 00 00 00 mov $0x75,%eax; %eax = 'u'
8048c10: 81 7c 24 28 e8 01 00 cmpl $0x1e8,0x28(%esp)
8048c17: 00
8048c18: 0f 84 e5 00 00 00 je 8048d03 <phase_3+0x147>
8048c1e: e8 83 05 00 00 call 80491a6 <explode_bomb>
8048c23: b8 75 00 00 00 mov $0x75,%eax; %eax = 'u'
8048c28: e9 d6 00 00 00 jmp 8048d03 <phase_3+0x147>
; case 1
; if (var3 != 341)
8048c2d: b8 78 00 00 00 mov $0x78,%eax; %eax = 'x'
8048c32: 81 7c 24 28 55 01 00 cmpl $0x155,0x28(%esp)
8048c39: 00
8048c3a: 0f 84 c3 00 00 00 je 8048d03 <phase_3+0x147>
8048c40: e8 61 05 00 00 call 80491a6 <explode_bomb>
8048c45: b8 78 00 00 00 mov $0x78,%eax; %eax = 'x'
8048c4a: e9 b4 00 00 00 jmp 8048d03 <phase_3+0x147>
; case 2
; if (var3 != 868)
8048c4f: b8 70 00 00 00 mov $0x70,%eax; %eax = 'p'
8048c54: 81 7c 24 28 64 03 00 cmpl $0x364,0x28(%esp)
8048c5b: 00
8048c5c: 0f 84 a1 00 00 00 je 8048d03 <phase_3+0x147>
8048c62: e8 3f 05 00 00 call 80491a6 <explode_bomb>
8048c67: b8 70 00 00 00 mov $0x70,%eax; %eax = 'p'
8048c6c: e9 92 00 00 00 jmp 8048d03 <phase_3+0x147>
; case 3
; if (var3 != 103)
8048c71: b8 62 00 00 00 mov $0x62,%eax; %eax = 'b'
8048c76: 83 7c 24 28 67 cmpl $0x67,0x28(%esp)
8048c7b: 0f 84 82 00 00 00 je 8048d03 <phase_3+0x147>
8048c81: e8 20 05 00 00 call 80491a6 <explode_bomb>
8048c86: b8 62 00 00 00 mov $0x62,%eax; %eax = 'b'
8048c8b: eb 76 jmp 8048d03 <phase_3+0x147>
; case 4
; if (var3 != 805)
8048c8d: b8 63 00 00 00 mov $0x63,%eax; %eax = 'c'
8048c92: 81 7c 24 28 25 03 00 cmpl $0x325,0x28(%esp)
8048c99: 00
8048c9a: 74 67 je 8048d03 <phase_3+0x147>
8048c9c: e8 05 05 00 00 call 80491a6 <explode_bomb>
8048ca1: b8 63 00 00 00 mov $0x63,%eax; %eax = 'c'
8048ca6: eb 5b jmp 8048d03 <phase_3+0x147>
; case 5
; if (var3 != 968)
8048ca8: b8 70 00 00 00 mov $0x70,%eax; %eax = 'p'
8048cad: 81 7c 24 28 c8 03 00 cmpl $0x3c8,0x28(%esp)
8048cb4: 00
8048cb5: 74 4c je 8048d03 <phase_3+0x147>
8048cb7: e8 ea 04 00 00 call 80491a6 <explode_bomb>
8048cbc: b8 70 00 00 00 mov $0x70,%eax; %eax = 'p'
8048cc1: eb 40 jmp 8048d03 <phase_3+0x147>
; case 6
; if (var3 != 372)
8048cc3: b8 67 00 00 00 mov $0x67,%eax; %eax = 'g'
8048cc8: 81 7c 24 28 74 01 00 cmpl $0x174,0x28(%esp)
8048ccf: 00
8048cd0: 74 31 je 8048d03 <phase_3+0x147>
8048cd2: e8 cf 04 00 00 call 80491a6 <explode_bomb>
8048cd7: b8 67 00 00 00 mov $0x67,%eax; %eax = 'g'
8048cdc: eb 25 jmp 8048d03 <phase_3+0x147>
; case 7
; if (var3 != 633)
8048cde: b8 61 00 00 00 mov $0x61,%eax; %eax = 'a'
8048ce3: 81 7c 24 28 79 02 00 cmpl $0x279,0x28(%esp)
8048cea: 00
8048ceb: 74 16 je 8048d03 <phase_3+0x147>
8048ced: e8 b4 04 00 00 call 80491a6 <explode_bomb>
8048cf2: b8 61 00 00 00 mov $0x61,%eax; %eax = 'a'
8048cf7: eb 0a jmp 8048d03 <phase_3+0x147>
; explode_bomb()
8048cf9: e8 a8 04 00 00 call 80491a6 <explode_bomb>
8048cfe: b8 6a 00 00 00 mov $0x6a,%eax
第二塊有大量的重復指令,簡單分析可得是一個 switch...case
結構。
首先保證 var1
不大於 7
,之后根據 var1
的值去內存中取值進行跳轉:
804a2df: 00 0b add %cl,(%ebx)
804a2e1: 8c 04 08 mov %es,(%eax,%ecx,1)
804a2e4: 2d 8c 04 08 4f sub $0x4f08048c,%eax
804a2e9: 8c 04 08 mov %es,(%eax,%ecx,1)
804a2ec: 71 8c jno 804a27a <_IO_stdin_used+0x156>
804a2ee: 04 08 add $0x8,%al
804a2f0: 8d 8c 04 08 a8 8c 04 lea 0x48ca808(%esp,%eax,1),%ecx
804a2f7: 08 c3 or %al,%bl
804a2f9: 8c 04 08 mov %es,(%eax,%ecx,1)
804a2fc: de 8c 04 08 0a 00 00 fimul 0xa08(%esp,%eax,1)
總共 8 種跳轉情況,要求 var1
的值必須為 0-7
,但最開始時只檢查了 var1
是否小於 7
,而如果要保證程序能正常運行,則需要滿足 var1
不能為負值,所以 var1
應當為 unsigned
,以確保其值的范圍在 0-7
。
由於各個 case
的結構相同,區別只有值,故只分析 case 0
:將字面值 75
( 字符 'u'
) 存入 %eax
,之后比較字面值 0x1e8
與 var3
,若不相同則 explode_bomb
,相同則 break
。
所以我們輸入的第三個整數應當與第一個整數相關。
block 3
; if (var2 == %al)
8048d03: 3a 44 24 2f cmp 0x2f(%esp),%al
8048d07: 74 05 je 8048d0e <phase_3+0x152>
; explode_bomb()
8048d09: e8 98 04 00 00 call 80491a6 <explode_bomb>
8048d0e: 83 c4 3c add $0x3c,%esp
8048d11: c3 ret
比較 var2
與 block 3 中存入 %eax
的值,若不同則 explode_bomb
,若相同則 return
,說明輸入的字符應當與第一個整數相關。
總體分析,可以發現輸入的字符和第三個整數都與第一個整數相關,根據 var1
的不同可以有 7 種答案:{ "0 u 488", "1 x 341", "2 p 868", "3 b 103", "4 c 805", "5 p 968", "6 g 372", "7 a 633" }
。
code
如果寄存器夠用,則變量不會寫入內存。
void phase_3(char* str)
{
unsigned var1;
int var3;
char var2, ans;
if (sscanf(str, "%d %c %d", &var1, &var2, &var3) <= 2)
explode_bomb();
switch (var1) {
case 0:
ans = 'u';
if (var3 != 488)
explode_bomb();
break;
case 1:
ans = 'x';
if (var3 != 341)
explode_bomb();
break;
case 2:
ans = 'p';
if (var3 != 868)
explode_bomb();
break;
case 3:
ans = 'b';
if (var3 != 103)
explode_bomb();
break;
case 4:
ans = 'c';
if (var3 != 805)
explode_bomb();
break;
case 5:
ans = 'p';
if (var3 != 968)
explode_bomb();
break;
case 6:
ans = 'g';
if (var3 != 372)
explode_bomb();
break;
case 7:
ans = 'a';
if (var3 != 633)
explode_bomb();
break;
default:
ans = 'j';
explode_bomb();
}
if (var2 != ans)
explode_bomb();
}
phase_4
phase_4
的主體是一個遞歸函數func4
的調用
block 1
08048d7f <phase_4>:
8048d7f: 83 ec 2c sub $0x2c,%esp
; sscanf(char* str, "%d %d", int* pvar1, int* pvar2)
8048d82: 8d 44 24 1c lea 0x1c(%esp),%eax; %eax = &var2
8048d86: 89 44 24 0c mov %eax,0xc(%esp); pvar1 = &var2
8048d8a: 8d 44 24 18 lea 0x18(%esp),%eax; %eax = &var1
8048d8e: 89 44 24 08 mov %eax,0x8(%esp); pvar2 = &var1
8048d92: c7 44 24 04 a3 a4 04 movl $0x804a4a3,0x4(%esp); "%d %d"
8048d99: 08
8048d9a: 8b 44 24 30 mov 0x30(%esp),%eax; %eax = str
8048d9e: 89 04 24 mov %eax,(%esp); (%esp) = str
8048da1: e8 ca fa ff ff call 8048870 <__isoc99_sscanf@plt>
第一塊是一個與 phase_3
中類似的 sscanf
,相比之下只讀取了兩個整數。
block 2
; if (sscanf(/**/) == 2 && var1 >= 0 && var1 <= 14)
; if (sscanf(/**/) == 2)
8048da6: 83 f8 02 cmp $0x2,%eax
8048da9: 75 0d jne 8048db8 <phase_4+0x39>
; if (var1 >= 0)
8048dab: 8b 44 24 18 mov 0x18(%esp),%eax
8048daf: 85 c0 test %eax,%eax
8048db1: 78 05 js 8048db8 <phase_4+0x39>
; if (var1 <= 14)
8048db3: 83 f8 0e cmp $0xe,%eax
8048db6: 7e 05 jle 8048dbd <phase_4+0x3e>
; else
; explode_bomb()
8048db8: e8 e9 03 00 00 call 80491a6 <explode_bomb>
第二塊是對 var1
的一個檢測,要求 var1
在范圍 0-14
之間。
block 3
; func4(var1 , 0, 14)
8048dbd: c7 44 24 08 0e 00 00 movl $0xe,0x8(%esp)
8048dc4: 00
8048dc5: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
8048dcc: 00
8048dcd: 8b 44 24 18 mov 0x18(%esp),%eax
8048dd1: 89 04 24 mov %eax,(%esp)
8048dd4: e8 39 ff ff ff call 8048d12 <func4>
第三塊調用了函數 func4
,傳入了三個參數:
int
: 變量var1
int
: 字面值0
int
: 字面值14
block 4
; if (func4(/**/) == 4 && var2 == 4)
; if (func4 == 4)
8048dd9: 83 f8 04 cmp $0x4,%eax
8048ddc: 75 07 jne 8048de5 <phase_4+0x66>
; if (var2 == 4)
8048dde: 83 7c 24 1c 04 cmpl $0x4,0x1c(%esp)
8048de3: 74 05 je 8048dea <phase_4+0x6b>
; explode_bomb()
8048de5: e8 bc 03 00 00 call 80491a6 <explode_bomb>
8048dea: 83 c4 2c add $0x2c,%esp
8048ded: c3 ret
第四塊是對 func4
函數的返回值和變量 var2
的檢測,要求兩者都為 4
。
總體分析,phase_4
類似 phase_1
,主體是對一個函數的調用與返回值檢測,但是 phase_4
中的函數是一個遞歸調用的函數,在下一節中我們具體分析 func4
這個遞歸函數。
code
void phase_4(char* str)
{
int var1, var2;
if (sscanf(str, "%d %d", &var1, &var2) == 2 && var1 >= 0 && var1 <= 14)
if (func4(var1, 0, 14) == 4 && var2 == 4)
return;
explode_bomb();
}
func4
func4
的主體是一個if...else
分支結構,進行遞歸調用的判斷
block 1
; func4(int var1, int var2, int var3)
08048d12 <func4>:
8048d12: 83 ec 1c sub $0x1c,%esp
; initial
8048d15: 89 5c 24 14 mov %ebx,0x14(%esp)
8048d19: 89 74 24 18 mov %esi,0x18(%esp)
8048d1d: 8b 54 24 20 mov 0x20(%esp),%edx; %edx = var1
8048d21: 8b 44 24 24 mov 0x24(%esp),%eax; %eax = var2
8048d25: 8b 5c 24 28 mov 0x28(%esp),%ebx; %ebx = var3
8048d29: 89 d9 mov %ebx,%ecx; %ecx = var3
第一塊是一個初始化,騰出了 %ebx
和 %esi
兩個寄存器,並將參數存入了寄存器。
block 2
; %ecx = (var3 - var2) / 2 + var2
8048d2b: 29 c1 sub %eax,%ecx; %ecx = var3 - var2
8048d2d: 89 ce mov %ecx,%esi; %esi = var3 - var2
8048d2f: c1 ee 1f shr $0x1f,%esi; %esi = (unsigned)(var3 - var2) >> 31
; %ecx = var3 - var2 + (unsigned)(var3 - var2) >> 31
8048d32: 01 f1 add %esi,%ecx
; %ecx = (int)(var3 - var2 + (unsigned)(var3 - var2) >> 31) >> 1
8048d34: d1 f9 sar %ecx
; %ecx = (int)(var3 - var2 + (unsigned)(var3 - var2) >> 31) >> 1 + var2
8048d36: 01 c1 add %eax,%ecx
第三塊是幾條運算指令,逐步分析得到一個表達式 (int)(var3 - var2 + (unsigned)(var3 - var2) >> 31) >> 1 + var2
,如果熟悉的話很快就能反應過來前面的是一個除法,簡化之后的表達式為 (var3 - var2) / 2 + var2
,當然可以進一步簡化為 (var2 + var3) / 2
,運算的結果都是相同的。
block 3
; if ((var3 - var2) / 2 + var2 > var1)
8048d38: 39 d1 cmp %edx,%ecx
8048d3a: 7e 17 jle 8048d53 <func4+0x41>
; return func4(var1, var2, (var3 - var2) / 2 + var2 - 1) * 2
8048d3c: 83 e9 01 sub $0x1,%ecx; %ecx = (var3 - var2) / 2 + var2 - 1
8048d3f: 89 4c 24 08 mov %ecx,0x8(%esp); 0x8(%esp) = (var3 - var2) / 2 + var2 - 1
8048d43: 89 44 24 04 mov %eax,0x4(%esp); 0x4(%esp) = var2
8048d47: 89 14 24 mov %edx,(%esp); (%esp) = var1
8048d4a: e8 c3 ff ff ff call 8048d12 <func4>
8048d4f: 01 c0 add %eax,%eax; %eax = func4 * 2
8048d51: eb 20 jmp 8048d73 <func4+0x61>
; else
; return 0
8048d53: b8 00 00 00 00 mov $0x0,%eax; %eax = 0
; else if ((var3 - var2) / 2 + var2 < var1)
8048d58: 39 d1 cmp %edx,%ecx
8048d5a: 7d 17 jge 8048d73 <func4+0x61>
; return func4(var1, (var3 - var2) / 2 + var2 + 1, var3) * 2 + 1
8048d5c: 89 5c 24 08 mov %ebx,0x8(%esp); 0x8(%esp) = var3
8048d60: 83 c1 01 add $0x1,%ecx; %ecx = (var3 - var2) / 2 + var2 + 1
8048d63: 89 4c 24 04 mov %ecx,0x4(%esp); 0x4(%esp) = (var3 - var2) / 2 + var2 + 1
8048d67: 89 14 24 mov %edx,(%esp); (%esp) = var1
8048d6a: e8 a3 ff ff ff call 8048d12 <func4>
8048d6f: 8d 44 00 01 lea 0x1(%eax,%eax,1),%eax; %eax = func4 * 2 + 1
8048d73: 8b 5c 24 14 mov 0x14(%esp),%ebx
8048d77: 8b 74 24 18 mov 0x18(%esp),%esi
8048d7b: 83 c4 1c add $0x1c,%esp
8048d7e: c3 ret
第三塊是對遞歸調用的判斷,結構並不復雜,很快就能分析出來,接下來要做的就是找合適的輸入使它的返回值為 4
。
code
// int func4(int var1, int var2, int var3)
// {
// if ((var2 + var3) / 2 > var1)
// return func4(var1, var2, (var2 + var3) / 2 - 1) * 2;
// else if ((var2 + var3) / 2 < var1)
// return func4(var1, (var2 + var3) / 2 + 1, var3) * 2 + 1;
// else
// return 0;
// }
int func4(int var1, int var2, int var3)
{
if ((var3 - var2) / 2 + var2 > var1)
return func4(var1, var2, (var3 - var2) / 2 + var2 - 1) * 2;
else if ((var3 - var2) / 2 + var2 < var1)
return func4(var1, (var3 - var2) / 2 + var2 + 1, var3) * 2 + 1;
else
return 0;
}
代入法
一種取巧的解法,只需要實現 func4
,然后將所有可能的輸入代入計算即可。由於只有 15 種情況,所以手算也是可行的,這里不作過多介紹了。
分析法
func4
的分支共有 3 條:
func4 * 2
func4 * 2 + 1
0
終止條件是 (var2 + var3) / 2 == var1
。
想要得到 4
,至少需要 1 次 +1
,否則無論如何計算結果都只能為 0
。而經過簡單的推導,我們發現前 2 次為 +1
不可能計算出 4
,因為前 2 次為 +1
的解集為 \({3,6,12,...}\),不包含 4
,所以結果只能是 1 次 +1
與 2 次 *2
。
列公式如下
由於除法是整除,所以化簡不一定能計算出精確的答案,但可以計算出一個大概的區間,然后逐個代入計算。由於情況較少,所以直接代入 $ (4) $ 式計算可能會更快。
計算結果:0 0 4 0 2 2 6 0 1 1 5 1 3 3 7
,所以 phase_4
的答案為 2 4
。
phase_5
phase_5
的主體是一個do...while
的循環結構
block 1
08048dee <phase_5>:
8048dee: 83 ec 2c sub $0x2c,%esp
; sscanf(char* str, "%d %d", int* pvar1, int* pvar2)
8048df1: 8d 44 24 1c lea 0x1c(%esp),%eax; %eax = &var2
8048df5: 89 44 24 0c mov %eax,0xc(%esp); pvar1 = &var2
8048df9: 8d 44 24 18 lea 0x18(%esp),%eax; %eax = &var1
8048dfd: 89 44 24 08 mov %eax,0x8(%esp); pvar2 = &var1
8048e01: c7 44 24 04 a3 a4 04 movl $0x804a4a3,0x4(%esp); "%d %d"
8048e08: 08
8048e09: 8b 44 24 30 mov 0x30(%esp),%eax; %eax = str
8048e0d: 89 04 24 mov %eax,(%esp); (%esp) = str
8048e10: e8 5b fa ff ff call 8048870 <__isoc99_sscanf@plt>
與 phase_4
完全相同的 sscanf
函數。
block 2
; if (sscanf(/**/) <= 1)
8048e15: 83 f8 01 cmp $0x1,%eax
8048e18: 7f 05 jg 8048e1f <phase_5+0x31>
; explode_bomb()
8048e1a: e8 87 03 00 00 call 80491a6 <explode_bomb>
; else
8048e1f: 8b 44 24 18 mov 0x18(%esp),%eax; %eax = var1
8048e23: 83 e0 0f and $0xf,%eax; %eax = var1 & 15
8048e26: 89 44 24 18 mov %eax,0x18(%esp); var1 = var1 & 15
; if(var1 & 15 != 15)
8048e2a: 83 f8 0f cmp $0xf,%eax
8048e2d: 74 2a je 8048e59 <phase_5+0x6b>
8048e2f: b9 00 00 00 00 mov $0x0,%ecx; %ecx = 0
8048e34: ba 00 00 00 00 mov $0x0,%edx; %edx = 0
第二塊首先對讀取的結果進行檢測,如果正確無誤則對第一個整數 var1
取低 4 位,判斷是否不等於 15
,所以輸入的 var1
只需要滿足低 4 位相同就可以得到相同的計算結果。
block 3
; int phase_5_arr[] = {10, 2, 14, 7, 8, 12, 15, 11, 0, 4, 1, 13, 3, 9, 6};
; do
8048e39: 83 c2 01 add $0x1,%edx; %edx = %edx + 1
8048e3c: 8b 04 85 00 a3 04 08 mov 0x804a300(,%eax,4),%eax; %eax = phase_5_arr[var1]
8048e43: 01 c1 add %eax,%ecx; %ecx = 0 + phase_5_arr[var1]
8048e45: 83 f8 0f cmp $0xf,%eax;
8048e48: 75 ef jne 8048e39 <phase_5+0x4b>
; while (phase_5_arr[var1] != 15)
8048e4a: 89 44 24 18 mov %eax,0x18(%esp); var1 = %eax
接 block 2 中的 if(var1 & 15 != 15)
,當條件成立時進入 do...while
循環。
地址 0x804a300
應該是一個數組的首地址(博主命名為 phase_5_arr
),其中的元素應該為:{10, 2, 14, 7, 8, 12, 15, 11, 0, 4, 1, 13, 3, 9, 6}
,共 15 個元素。
0804a300 <array.2957>:
804a300: 0a 00 or (%eax),%al
804a302: 00 00 add %al,(%eax)
804a304: 02 00 add (%eax),%al
804a306: 00 00 add %al,(%eax)
804a308: 0e push %cs
804a309: 00 00 add %al,(%eax)
804a30b: 00 07 add %al,(%edi)
804a30d: 00 00 add %al,(%eax)
804a30f: 00 08 add %cl,(%eax)
804a311: 00 00 add %al,(%eax)
804a313: 00 0c 00 add %cl,(%eax,%eax,1)
804a316: 00 00 add %al,(%eax)
804a318: 0f 00 00 sldt (%eax)
804a31b: 00 0b add %cl,(%ebx)
804a31d: 00 00 add %al,(%eax)
804a31f: 00 00 add %al,(%eax)
804a321: 00 00 add %al,(%eax)
804a323: 00 04 00 add %al,(%eax,%eax,1)
804a326: 00 00 add %al,(%eax)
804a328: 01 00 add %eax,(%eax)
804a32a: 00 00 add %al,(%eax)
804a32c: 0d 00 00 00 03 or $0x3000000,%eax
804a331: 00 00 add %al,(%eax)
804a333: 00 09 add %cl,(%ecx)
804a335: 00 00 add %al,(%eax)
804a337: 00 06 add %al,(%esi)
804a339: 00 00 add %al,(%eax)
804a33b: 00 05 00 00 00 53 add %al,0x53000000
%edx
記錄 do...while
循環執行的次數,循環體內將 phase_5_arr
(定義為) 內的元素逐個相加並存入 %ecx
,初始索引為輸入的 var1
,之后每次的索引是上一次循環的元素值,直到元素值為 15
時退出循環(phase_5_arr
可以看作是一個存放鏈表鏈節的數組)。
block 4
; if (%edx == 15)
8048e4e: 83 fa 0f cmp $0xf,%edx;
8048e51: 75 06 jne 8048e59 <phase_5+0x6b>
; if (%ecx == var2)
8048e53: 3b 4c 24 1c cmp 0x1c(%esp),%ecx
8048e57: 74 05 je 8048e5e <phase_5+0x70>
; explode_bomb()
8048e59: e8 48 03 00 00 call 80491a6 <explode_bomb>
8048e5e: 83 c4 2c add $0x2c,%esp
8048e61: c3 ret
首先檢測循環的次數,要求循環次數為 15,而phase_5_arr
的元素個數就是 15,說明這 15 個鏈節形成了一個鏈表,輸入的 var1
應當為鏈表的表頭。
再檢測累加的值是否與輸入的第二個整數 var2
相等,要求 var2
與運算結果相同。
總體分析,phase_5
要求輸入鏈表的表頭以及各個鏈節的和,所以答案為 5 115
,0-15
中缺失的元素為 5
。
code
void phase_5(char* str)
{
int var1, var2, count, sum;
if (sscanf(str, "%d %d", &var1, &var2) <= 1)
explode_bomb();
var1 = var1 & 15;
if (var1 != 15) {
count = sum = 0;
do {
++count;
var1 = phase_5_arr[var1];
sum += var1;
} while (var1 != 15);
}
if (count != 15 || sum != var2)
explode_bomb();
}
phase_6
phase_6
的結構比較復雜,存在許多分支以及嵌套的循環,跳轉指令互相交錯
block 1
08048e62 <phase_6>:
8048e62: 56 push %esi
8048e63: 53 push %ebx
8048e64: 83 ec 44 sub $0x44,%esp
; read_six_numbers(char* str, unsigned arr[])
8048e67: 8d 44 24 10 lea 0x10(%esp),%eax; %eax = arr
8048e6b: 89 44 24 04 mov %eax,0x4(%esp); 0x4(%esp) = arr
8048e6f: 8b 44 24 50 mov 0x50(%esp),%eax; %eax = str
8048e73: 89 04 24 mov %eax,(%esp); (%esp) = str
8048e76: e8 60 04 00 00 call 80492db <read_six_numbers>
調用了 phase_2
中出現過的 read_six_numbers
,將在之后逐步分析為什么應該是 unsigned
數組。
block 2
8048e7b: be 00 00 00 00 mov $0x0,%esi; %esi = 0
; do
8048e80: 8b 44 b4 10 mov 0x10(%esp,%esi,4),%eax; %eax = arr[%esi]
8048e84: 83 e8 01 sub $0x1,%eax; %eax -= arr[%esi] - 1
; if (arr[%esi] - 1 > 5)
8048e87: 83 f8 05 cmp $0x5,%eax
8048e8a: 76 05 jbe 8048e91 <phase_6+0x2f>
; explode_bomb()
8048e8c: e8 15 03 00 00 call 80491a6 <explode_bomb>
; if (++%esi == 6)
; break
8048e91: 83 c6 01 add $0x1,%esi; %esi += 1
8048e94: 83 fe 06 cmp $0x6,%esi
8048e97: 74 33 je 8048ecc <phase_6+0x6a>
; for (%ebx = %esi, %ebx <= 5; ++%ebx)
8048e99: 89 f3 mov %esi,%ebx; %ebx = %esi
8048e9b: 8b 44 9c 10 mov 0x10(%esp,%ebx,4),%eax; %eax = arr[%ebx]
; if (arr[%esi-1] == arr[%ebx])
8048e9f: 39 44 b4 0c cmp %eax,0xc(%esp,%esi,4)
8048ea3: 75 05 jne 8048eaa <phase_6+0x48>
; explode_bomb()
8048ea5: e8 fc 02 00 00 call 80491a6 <explode_bomb>
8048eaa: 83 c3 01 add $0x1,%ebx; %ebx += 1
8048ead: 83 fb 05 cmp $0x5,%ebx
8048eb0: 7e e9 jle 8048e9b <phase_6+0x39>
; loop for
8048eb2: eb cc jmp 8048e80 <phase_6+0x1e>
; while (true)
首先將 %esi
置 0
,用於記錄循環的次數,當 %esi == 6
時退出循環。
循環體內首先判斷當前元素減 1
之后是否大於 5
,若是則 explode_bomb
,此處元素的類型將會影響結果。若為 int
,則輸入的值只需要不大於 5
即可,可以為負數;若為 unsigned
,則輸入的值的范圍必須為 1-6
。
判斷完元素值之后,循環計數 +1
,判斷是否退出循環,若不退出循環,則進入 for
循環。
for
循環中首先將 %esi
存入 %ebx
,作為 for
循環的迭代變量。循環體內比較 arr[%ebx]
與 arr[%esi-1]
的值,如果相等則 explode_bomb
,直至變量完所有元素。
綜合分析以上兩個循環,可以發現其功能是判斷輸入的元素是否存在相同元素,如果存在則 explode_bomb
,所以輸入的元素應當不重復。如果類型為 unsigned
,則進一步約束為 1-6
,且每個值出現 1 次。
block 3
; do
8048eb4: 8b 52 08 mov 0x8(%edx),%edx; %edx = %edx -> next
8048eb7: 83 c0 01 add $0x1,%eax; %eax += 1
8048eba: 39 c8 cmp %ecx,%eax
8048ebc: 75 f6 jne 8048eb4 <phase_6+0x52>
; while (++%eax != arr[%ebx])
8048ebe: 89 54 b4 28 mov %edx,0x28(%esp,%esi,4); nodes[%esi] = %edx
8048ec2: 83 c3 01 add $0x1,%ebx; %ebx += 1
8048ec5: 83 fb 06 cmp $0x6,%ebx
8048ec8: 75 07 jne 8048ed1 <phase_6+0x6f>
; while (%ebx != 6)
; else
8048eca: eb 1c jmp 8048ee8 <phase_6+0x86>
8048ecc: bb 00 00 00 00 mov $0x0,%ebx; %ebx = 0
; do
8048ed1: 89 de mov %ebx,%esi; %esi = %ebx
8048ed3: 8b 4c 9c 10 mov 0x10(%esp,%ebx,4),%ecx; %ecx = arr[%ebx]
8048ed7: b8 01 00 00 00 mov $0x1,%eax; %eax = 1
8048edc: ba 3c c1 04 08 mov $0x804c13c,%edx; %edx = 0x804c13c
; if (arr[%ebx] > 1)
8048ee1: 83 f9 01 cmp $0x1,%ecx
8048ee4: 7f ce jg 8048eb4 <phase_6+0x52>
; else
8048ee6: eb d6 jmp 8048ebe <phase_6+0x5c>
第三塊是兩個嵌套的 do...while
循環,但是外層循環中代碼並非按順序分布,所以可能難以理解。
外層循環的起點為 8048ed1
,其之前的地址為 8048ecc
的指令是循環計數的初始化指令,不在循環體內。
外層循環首先執行以下操作:
- 將外層循環計數器
%ebx
的值存入%esi
中 - 將
arr[%ebx]
存入%ecx
- 令內存循環計數器
%eax = 1
- 令
%edx = 0x804c13c
0804c13c <node1>:
804c13c: a6 cmpsb %es:(%edi),%ds:(%esi)
804c13d: 01 00 add %eax,(%eax)
804c13f: 00 01 add %al,(%ecx)
804c141: 00 00 add %al,(%eax)
804c143: 00 48 c1 add %cl,-0x3f(%eax)
804c146: 04 08 add $0x8,%al
0804c148 <node2>:
804c148: 4b dec %ebx
804c149: 03 00 add (%eax),%eax
804c14b: 00 02 add %al,(%edx)
804c14d: 00 00 add %al,(%eax)
804c14f: 00 54 c1 04 add %dl,0x4(%ecx,%eax,8)
804c153: 08 af 01 00 00 03 or %ch,0x3000001(%edi)
0804c154 <node3>:
804c154: af scas %es:(%edi),%eax
804c155: 01 00 add %eax,(%eax)
804c157: 00 03 add %al,(%ebx)
804c159: 00 00 add %al,(%eax)
804c15b: 00 60 c1 add %ah,-0x3f(%eax)
804c15e: 04 08 add $0x8,%al
0804c160 <node4>:
804c160: a2 00 00 00 04 mov %al,0x4000000
804c165: 00 00 add %al,(%eax)
804c167: 00 6c c1 04 add %ch,0x4(%ecx,%eax,8)
804c16b: 08 3a or %bh,(%edx)
0804c16c <node5>:
804c16c: 3a 03 cmp (%ebx),%al
804c16e: 00 00 add %al,(%eax)
804c170: 05 00 00 00 78 add $0x78000000,%eax
804c175: c1 04 08 c8 roll $0xc8,(%eax,%ecx,1)
0804c178 <node6>:
804c178: c8 02 00 00 enter $0x2,$0x0
804c17c: 06 push %es
804c17d: 00 00 add %al,(%eax)
804c17f: 00 00 add %al,(%eax)
804c181: 00 00 add %al,(%eax)
查看地址單元,發現 %edx
存儲的應該是 node1
的首地址,分析這幾個 node
的內存結果,猜測其結構應該為:
struct node {
int value;
int id;
struct node* next;
};
每一個 node
分別指向編號 +1
的 node
,形成一個鏈表。
完成值的設置之后,判斷當前數組元素是否大於 1
,若是則進入內層循環,若否則跳過內層循環。
內存循環執行如下操作:
%edx = %edx -> next
,將指針指向下一節點%eax += 1
,再判斷%eax
是否等於當前數組元素,若是則退出循環,若否則內層循環迭代
所以內層循環的功能應該是找到編號與當前數組元素相等的節點,由於編號為 1-6
,因此如果要避免死循環,則數組元素必須為 1-6
,所以元素類型應當為 unsigned
。
內層循環結束之后(或當前數組元素是否大於 1
),將指向的節點的首地址存入數組 arr
之后的地址中,如果以 arr
進行表示則是存入 arr[6 + %esi]
。由於 %esi
是外層循環計數器,值在不停增加,所以 arr
之后應當還有一個數組,用於存放指向 node
的指針,記作 struct node* nodes[]
。
總體分析上述內外層循環,發現實現的功能是按照數組元素的值,將相應編號的節點依次存入節點數組(通過指針訪問,並沒有存入相應的地址)中。
block 4
; 循環展開
8048ee8: 8b 5c 24 28 mov 0x28(%esp),%ebx; %ebx = nodes[0]
8048eec: 8b 44 24 2c mov 0x2c(%esp),%eax; %eax = nodes[1]
8048ef0: 89 43 08 mov %eax,0x8(%ebx); nodes[0]->next = nodes[1]
8048ef3: 8b 54 24 30 mov 0x30(%esp),%edx; %edx = nodes[2]
8048ef7: 89 50 08 mov %edx,0x8(%eax); nodes[1]->next = nodes[2]
8048efa: 8b 44 24 34 mov 0x34(%esp),%eax; %eax = nodes[3]
8048efe: 89 42 08 mov %eax,0x8(%edx); nodes[2]->next = nodes[3]
8048f01: 8b 54 24 38 mov 0x38(%esp),%edx; %edx = nodes[4]
8048f05: 89 50 08 mov %edx,0x8(%eax); nodes[3]->next = nodes[4]
8048f08: 8b 44 24 3c mov 0x3c(%esp),%eax; %eax = nodes[5]
8048f0c: 89 42 08 mov %eax,0x8(%edx); nodes[4]->next = nodes[5]
8048f0f: c7 40 08 00 00 00 00 movl $0x0,0x8(%eax); nodes[5]->next = NULL
第四塊按節點數組中的順序,將上一個節點指向下一個節點,結合 block 3 可以發現,兩塊指令實現的功能是按照輸入的數組對節點進行了重新的排列,形成了一個新的鏈表。
block 5
8048f16: be 05 00 00 00 mov $0x5,%esi; %esi = 5
; do
8048f1b: 8b 43 08 mov 0x8(%ebx),%eax; %eax = %ebx->next
8048f1e: 8b 10 mov (%eax),%edx; %edx = %ebx->next->value
; if (%ebx->value > %ebx->next->value)
8048f20: 39 13 cmp %edx,(%ebx)
8048f22: 7e 05 jle 8048f29 <phase_6+0xc7>
; explode_bomb()
8048f24: e8 7d 02 00 00 call 80491a6 <explode_bomb>
8048f29: 8b 5b 08 mov 0x8(%ebx),%ebx; %ebx = %ebx->next
8048f2c: 83 ee 01 sub $0x1,%esi; %esi -= 1
8048f2f: 75 ea jne 8048f1b <phase_6+0xb9>
; while (--%esi)
第五塊是一個簡單的 do...while
循環,首先將計數器置為 5
,然后進入循環。同時需要注意到,在 block 4 中,nodes[0]
存入 %ebx
之后 %ebx
的值沒有被覆寫,所以 %ebx
中存儲的是 nodes[0]
的首地址。
for
循環中首先將 %ebx
指向的下一個節點存入 %eax
,將下一個節點的值存入 %edx
。然后判斷當前節點的值是否大於下一節點的值,若是則 explode_bomb
,若否則計數器 -1
,循環迭代直至計數器為 0
。
總體分析,phase_6
的要求我們按各節點值的大小從小到大進行排序,然后將各節點的順序輸入。
由於節點值為 [ 0x1a6, 0x34b, 0x1af, 0xa2, 0x33a, 0x2c8 ]
,所以答案為 4 1 3 6 5 2
。
code
void phase_6(char* str)
{
unsigned arr[6];
struct node* nodes[6];
int i, j, index;
struct node* p;
read_six_numbers(str, arr);
i = 0;
do {
if (arr[i] - 1 > 5)
explode_bomb();
if (++i == 6)
break;
for (j = i; j <= 5; ++j)
if (arr[i - 1] == arr[j])
explode_bomb();
} while (1);
i = 0;
do {
index = 1;
p = &node1;
if (arr[i] > 1)
do
p = p->next;
while (++index != arr[i]);
nodes[i] = p;
} while (++i != 6);
nodes[0]->next = nodes[1];
nodes[1]->next = nodes[2];
nodes[2]->next = nodes[3];
nodes[3]->next = nodes[4];
nodes[4]->next = nodes[5];
nodes[5]->next = NULL;
p = nodes[0];
i = 5;
do {
if (p->value > p->next->value)
explode_bomb();
p = p->next;
} while (--i);
}
secret_phase_entrance
secret_phase
並沒有在主函數中直接調用,我們需要先找到它的入口。匯編指令中直接有標注 secret_phase
的地址,我們找到調用它的地方即可,就在 phase_defused
中。
phase_defused
0804932b <phase_defused>:
804932b: 81 ec 8c 00 00 00 sub $0x8c,%esp
; 哨兵
8049331: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8049337: 89 44 24 7c mov %eax,0x7c(%esp)
804933b: 31 c0 xor %eax,%eax
; if (*(int*)0x804c3cc == 6)
804933d: 83 3d cc c3 04 08 06 cmpl $0x6,0x804c3cc
8049344: 75 72 jne 80493b8 <phase_defused+0x8d>
; sscanf(char* str, "%d %d %s", int var1, int var2, char* s)
8049346: 8d 44 24 2c lea 0x2c(%esp),%eax
804934a: 89 44 24 10 mov %eax,0x10(%esp); 0x10(%esp) = s
804934e: 8d 44 24 28 lea 0x28(%esp),%eax
8049352: 89 44 24 0c mov %eax,0xc(%esp); 0xc(%esp) = var2
8049356: 8d 44 24 24 lea 0x24(%esp),%eax
804935a: 89 44 24 08 mov %eax,0x8(%esp); 0x08(%esp) = var1
804935e: c7 44 24 04 a9 a4 04 movl $0x804a4a9,0x4(%esp); "%d %d %s"
8049365: 08
8049366: c7 04 24 d0 c4 04 08 movl $0x804c4d0,(%esp);
804936d: e8 fe f4 ff ff call 8048870 <__isoc99_sscanf@plt>
; if (sscanf(/**/) == 3)
8049372: 83 f8 03 cmp $0x3,%eax
8049375: 75 35 jne 80493ac <phase_defused+0x81>
; strings_not_equal(char*, "DrEvil")
8049377: c7 44 24 04 b2 a4 04 movl $0x804a4b2,0x4(%esp); "DrEvil"
804937e: 08
804937f: 8d 44 24 2c lea 0x2c(%esp),%eax; %eax = s
8049383: 89 04 24 mov %eax,(%esp); (%esp) = s
8049386: e8 09 fd ff ff call 8049094 <strings_not_equal>
; if (strings_not_equal(/**/))
; return
804938b: 85 c0 test %eax,%eax
804938d: 75 1d jne 80493ac <phase_defused+0x81>
; "Curses, you've found the secret phase!"
804938f: c7 04 24 78 a3 04 08 movl $0x804a378,(%esp)
8049396: e8 65 f4 ff ff call 8048800 <puts@plt>
; "But finding it and solving it are quite different..."
804939b: c7 04 24 a0 a3 04 08 movl $0x804a3a0,(%esp)
80493a2: e8 59 f4 ff ff call 8048800 <puts@plt>
80493a7: e8 dc fb ff ff call 8048f88 <secret_phase>
; "Congratulations! You've defused the bomb!"
80493ac: c7 04 24 d8 a3 04 08 movl $0x804a3d8,(%esp)
80493b3: e8 48 f4 ff ff call 8048800 <puts@plt>
80493b8: 8b 44 24 7c mov 0x7c(%esp),%eax
80493bc: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
80493c3: 74 05 je 80493ca <phase_defused+0x9f>
80493c5: e8 06 f4 ff ff call 80487d0 <__stack_chk_fail@plt>
80493ca: 81 c4 8c 00 00 00 add $0x8c,%esp
80493d0: c3 ret
我們直接來分析什么時候觸發 secret_phase
。首先來看 0x804c3cc
這個地址,我們直接找到相應的指令處:
0804c3cc <num_input_strings>:
804c3cc: 00 00 add %al,(%eax)
發現它的命名是 num_input_strings
,字面意思就是輸入的字符串的個數,這大概率就是答題的次數,再搜索一下哪里出現了這個地址,並對它進行了寫值:
080491cd <read_line>:
8049287: a1 cc c3 04 08 mov 0x804c3cc,%eax
804928c: 8d 50 01 lea 0x1(%eax),%edx
804928f: 89 15 cc c3 04 08 mov %edx,0x804c3cc
最終我們在 read_line
中找到了它,整個匯編指令中只有這第三行指令對它進行了寫值,再結合上面的兩條指令,我們發現是對其進行了自增 1
的操作,符合我們的猜測。由於 read_line
部分指令太多,也沒有必要進行分析,我們去 gdb
中進行一下驗證:
可以看到結果與我們分析的完全符合。
接着往下走,我們發現調用了 sscanf
函數,讀取了兩個整數和一個字符串,但是直接去指令中找,我們發現被讀取的字符串是空的,所以這個字符串在運行時應當發生了改變,我們接着之前的 gdb
查看一下:
很顯然是我們輸入的 phase_4
的答案,而這個 sscanf
還讀取了一個字符串 "DrEvil"
,顯然我們 phase_4
的答案輸入 2 4 DrEvil
就能進入 secret_phase
:
至此,我們已經找到了 secret_phase
的入口,接下來就是解決它。
secret_phase
secret_phase
的主體如phase_4
是一個遞歸函數的調用
08048f88 <secret_phase>:
8048f88: 53 push %ebx
8048f89: 83 ec 18 sub $0x18,%esp
; read_line()
8048f8c: e8 3c 02 00 00 call 80491cd <read_line>; str = read_line()
; strtol(str, 0, 10)
8048f91: c7 44 24 08 0a 00 00 movl $0xa,0x8(%esp)
8048f98: 00
8048f99: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
8048fa0: 00
8048fa1: 89 04 24 mov %eax,(%esp)
8048fa4: e8 37 f9 ff ff call 80488e0 <strtol@plt>
8048fa9: 89 c3 mov %eax,%ebx; %ebx = %eax
8048fab: 8d 40 ff lea -0x1(%eax),%eax; %eax = %eax-1
; if (%eax > 1000)
8048fae: 3d e8 03 00 00 cmp $0x3e8,%eax
8048fb3: 76 05 jbe 8048fba <secret_phase+0x32>
; explode_bomb()
8048fb5: e8 ec 01 00 00 call 80491a6 <explode_bomb>
; fun7()
8048fba: 89 5c 24 04 mov %ebx,0x4(%esp); 0x4(%esp) = %ebx
8048fbe: c7 04 24 88 c0 04 08 movl $0x804c088,(%esp); (%esp) = (int*)0x804c088
8048fc5: e8 6d ff ff ff call 8048f37 <fun7>
; if (fun7 != 4)
8048fca: 83 f8 04 cmp $0x4,%eax
8048fcd: 74 05 je 8048fd4 <secret_phase+0x4c>
; explode_bomb()
8048fcf: e8 d2 01 00 00 call 80491a6 <explode_bomb>
; else
; "Wow! You've defused the secret stage!"
8048fd4: c7 04 24 94 a2 04 08 movl $0x804a294,(%esp)
8048fdb: e8 20 f8 ff ff call 8048800 <puts@plt>
8048fe0: e8 46 03 00 00 call 804932b <phase_defused>
8048fe5: 83 c4 18 add $0x18,%esp
8048fe8: 5b pop %ebx
8048fe9: c3 ret
主體並不復雜,約束條件僅有 $ 0 < x \leq 1000 $,然后將其作為 fun7
的第二個參數。我們觀察第一個參數:
0804c088 <n1>:
804c088: 24 00 and $0x0,%al
804c08a: 00 00 add %al,(%eax)
804c08c: 94 xchg %eax,%esp
804c08d: c0 04 08 a0 rolb $0xa0,(%eax,%ecx,1)
804c091: c0 04 08 08 rolb $0x8,(%eax,%ecx,1)
0804c094 <n21>:
804c094: 08 00 or %al,(%eax)
804c096: 00 00 add %al,(%eax)
804c098: c4 (bad)
804c099: c0 04 08 ac rolb $0xac,(%eax,%ecx,1)
804c09d: c0 04 08 32 rolb $0x32,(%eax,%ecx,1)
發現是一個類似 node
的節點(還有很多就不貼上來了),不過似乎有兩個指針,所以這個結構應該會比鏈表復雜,可能是雙向鏈表或樹或圖。
最后要滿足 fun7
的返回值為 4
,那我們就進入 fun7
一探究竟。
code
void secret_phase(char* str)
{
char* str;
unsigned x;
str = read_line();
x = strtol(str, 0, 10);
if (x - 1 > 1000)
explode_bomb();
if (fun7(&n1, x) != 4)
explode_bomb();
printf("Wow! You've defused the secret stage!");
}
fun7
; fun7(n* p, int var)
08048f37 <fun7>:
8048f37: 53 push %ebx
8048f38: 83 ec 18 sub $0x18,%esp
8048f3b: 8b 54 24 20 mov 0x20(%esp),%edx; %edx = p
8048f3f: 8b 4c 24 24 mov 0x24(%esp),%ecx; %ecx = var
; if (p != NULL)
8048f43: 85 d2 test %edx,%edx
8048f45: 74 37 je 8048f7e <fun7+0x47>
8048f47: 8b 1a mov (%edx),%ebx; %ebx = p->value
; if (p->value > var)
8048f49: 39 cb cmp %ecx,%ebx;
8048f4b: 7e 13 jle 8048f60 <fun7+0x29>
; fun7(p->left, var)
8048f4d: 89 4c 24 04 mov %ecx,0x4(%esp); 0x4(%esp) = var
8048f51: 8b 42 04 mov 0x4(%edx),%eax; %eax = p->left
8048f54: 89 04 24 mov %eax,(%esp); (%esp) = p->left
8048f57: e8 db ff ff ff call 8048f37 <fun7>
8048f5c: 01 c0 add %eax,%eax; return fun7 * 2
8048f5e: eb 23 jmp 8048f83 <fun7+0x4c>
; else
8048f60: b8 00 00 00 00 mov $0x0,%eax; return 0
; else if (p->value != var)
8048f65: 39 cb cmp %ecx,%ebx
8048f67: 74 1a je 8048f83 <fun7+0x4c>
; fun7(p->right, var)
8048f69: 89 4c 24 04 mov %ecx,0x4(%esp); 0x4(%esp) = var
8048f6d: 8b 42 08 mov 0x8(%edx),%eax; %eax = p->right
8048f70: 89 04 24 mov %eax,(%esp); (%esp) = p->right
8048f73: e8 bf ff ff ff call 8048f37 <fun7>
8048f78: 8d 44 00 01 lea 0x1(%eax,%eax,1),%eax; return fun7 * 2 + 1
8048f7c: eb 05 jmp 8048f83 <fun7+0x4c>
; else
8048f7e: b8 ff ff ff ff mov $0xffffffff,%eax; return -1
8048f83: 83 c4 18 add $0x18,%esp
8048f86: 5b pop %ebx
8048f87: c3 ret
跟 func4
幾乎是一個模子里刻出來的,只不過多了一步判斷 p
是否指向 NULL
,若是則返回 -1
。
在 p != NULL
時,共有三條分支:
p->value > var
:return fun7(p->left, var) * 2
p->value < var
:return fun7(p->right, var) * 2 + 1
p->value == var
:return 0
這時候也確定了 n
具有兩個指針,我們來分析一下它們之間的關系:
"n1": {
"value": 36,
"n21": {
"value": 8,
"n31": {
"value": 6,
"n41": { "value": 1 },
"n42": { "value": 7 }
},
"n32": {
"value": 22,
"n43": { "value": 20 },
"n44": { "value": 35 }
}
},
"n22": {
"value": 50,
"n33": {
"value": 45,
"n45": { "value": 40 },
"n46": { "value": 47 }
},
"n34": {
"value": 107,
"n47": { "value": 99 },
"n48": { "value": 1001 }
}
}
}
確定了這是一棵二叉樹,同時樹的高度為 4
。結合之前的三條分支,得到我們的任務是在第 4 步時計算出 4
。
步長只有 4,即使是窮舉也很快,我們得到的唯一的解是:{ "0", "fun7 * 2 + 1", "fun7 * 2", "fun7 * 2" }
,滿足的比較關系從根節點開始為:{ '<', '<', '>', '==' }
,在樹中尋找到路徑:n1 -> n21 -> n31 -> n42
,答案為 7
。
code
int fun7(struct n* p, int var)
{
if (p == NULL)
return -1;
if (p->value > var)
return fun7(p->left, var) * 2;
if (p->value != var)
return fun7(p->right, var) * 2 + 1;
return 0;
}
后記
至此 bomblab
也算是告一段落,這個實驗的確是花費了不少功夫,包括最初的摸索,到后來的嫻熟,成長也是很大。而寫這篇博客花費的時間也許超過了我做實驗的時間。
在此,我非常感謝你能閱讀完這一篇長長的文章,也希望你能有所收獲,你的每一次點擊都是對我莫大的鼓勵😁。