wilfredshen

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 也算是告一段落,这个实验的确是花费了不少功夫,包括最初的摸索,到后来的娴熟,成长也是很大。而写这篇博客花费的时间也许超过了我做实验的时间。

在此,我非常感谢你能阅读完这一篇长长的文章,也希望你能有所收获,你的每一次点击都是对我莫大的鼓励

分类:

技术点:

相关文章: