CSAPP-bomblab


date: 2020-05-04

本實驗中博主采用對 objdump -D 令生成的文本進行分析求解(需要有基本的匯編知識,了解各種指令以及棧幀結構,之后的討論中不會對此進行介紹),gdb 調試僅在求解 secret_phase 時使用。

博主的部分數據有進行改動,故答案與官網的不同,但解法相同,可作參考學習。

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 的返回值,若 trueexplode_bomb,若 falsereturn 說明 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,若 falseexplode_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%esi0x1c(%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 個參數,分別是:

  1. char*: phase_3 的參數
  2. char*: "%d %c %d"
  3. unsigned: var1
  4. char: var2
  5. 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,之后比較字面值 0x1e8var3,若不相同則 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,傳入了三個參數:

  1. int: 變量 var1
  2. int: 字面值 0
  3. 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 條:

  1. func4 * 2
  2. func4 * 2 + 1
  3. 0

終止條件是 (var2 + var3) / 2 == var1

想要得到 4,至少需要 1 次 +1,否則無論如何計算結果都只能為 0。而經過簡單的推導,我們發現前 2 次為 +1 不可能計算出 4,因為前 2 次為 +1 的解集為 \({3,6,12,...}\),不包含 4,所以結果只能是 1 次 +1 與 2 次 *2

列公式如下

\[\begin{cases} (var2 + var3) / 2 > var1 \\ \lbrack var2 + (var2 + var3) / 2 - 1 \rbrack / 2 > var1 \\ \lbrace var2 + \lbrack var2 + (var2 + var3) / 2 - 1 \rbrack / 2 - 1 \rbrace / 2 < var1 \\ \lbrace \lbrace var2 + \lbrack var2 + (var2 + var3) / 2 - 1 \rbrack / 2 - 1 \rbrace / 2 + 1 + \lbrack var2 + (var2 + var3) / 2 - 1 \rbrack / 2 - 1 \rbrace / 2 = var1 \end{cases} \]

由於除法是整除,所以化簡不一定能計算出精確的答案,但可以計算出一個大概的區間,然后逐個代入計算。由於情況較少,所以直接代入 $ (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 1150-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)

首先將 %esi0,用於記錄循環的次數,當 %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 分別指向編號 +1node,形成一個鏈表。

完成值的設置之后,判斷當前數組元素是否大於 1,若是則進入內層循環,若否則跳過內層循環。

內存循環執行如下操作:

  1. %edx = %edx -> next,將指針指向下一節點
  2. %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 也算是告一段落,這個實驗的確是花費了不少功夫,包括最初的摸索,到后來的嫻熟,成長也是很大。而寫這篇博客花費的時間也許超過了我做實驗的時間。

在此,我非常感謝你能閱讀完這一篇長長的文章,也希望你能有所收獲,你的每一次點擊都是對我莫大的鼓勵😁。

個人博客:https://wilfredshen.cn/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM