0%

哈工大计算机系统Lab2.二进制炸弹

说明

给定一个可执行目标文件bomb,共有6个阶段,需要用gdb等程序分析反汇编,以获取每个阶段对应的密码。若密码输错则炸弹爆炸。其中一个炸弹可以从这里获取(若提示无法安全下载可以复把链接复制到浏览器试试)。如果想要答案可以直接翻到最后一部分。

准备工作

实验环境:Ubuntu 20.04

获取反汇编

1
objdump -d bomb > asm.txt

在这里插入图片描述
就可以把反汇编以文件形式存到当前文件夹下。

炸弹程序框架

和bomb同时下发了一个bomb.c文件,为炸弹程序框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/***************************************************************************
* Dr. Evil's Insidious Bomb, Version 1.1
* Copyright 2011, Dr. Evil Incorporated. All rights reserved.
*
* LICENSE:
*
* Dr. Evil Incorporated (the PERPETRATOR) hereby grants you (the
* VICTIM) explicit permission to use this bomb (the BOMB). This is a
* time limited license, which expires on the death of the VICTIM.
* The PERPETRATOR takes no responsibility for damage, frustration,
* insanity, bug-eyes, carpal-tunnel syndrome, loss of sleep, or other
* harm to the VICTIM. Unless the PERPETRATOR wants to take credit,
* that is. The VICTIM may not distribute this bomb source code to
* any enemies of the PERPETRATOR. No VICTIM may debug,
* reverse-engineer, run "strings" on, decompile, decrypt, or use any
* other technique to gain knowledge of and defuse the BOMB. BOMB
* proof clothing may not be worn when handling this program. The
* PERPETRATOR will not apologize for the PERPETRATOR's poor sense of
* humor. This license is null and void where the BOMB is prohibited
* by law.
***************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include "support.h"
#include "phases.h"

/*
* Note to self: Remember to erase this file so my victims will have no
* idea what is going on, and so they will all blow up in a
* spectaculary fiendish explosion. -- Dr. Evil
*/

FILE *infile;

int main(int argc, char *argv[])
{
char *input;

/* Note to self: remember to port this bomb to Windows and put a
* fantastic GUI on it. */

/* When run with no arguments, the bomb reads its input lines
* from standard input. */
if (argc == 1) {
infile = stdin;
}

/* When run with one argument <file>, the bomb reads from <file>
* until EOF, and then switches to standard input. Thus, as you
* defuse each phase, you can add its defusing string to <file> and
* avoid having to retype it. */
else if (argc == 2) {
if (!(infile = fopen(argv[1], "r"))) {
printf("%s: Error: Couldn't open %s\n", argv[0], argv[1]);
exit(8);
}
}

/* You can't call the bomb with more than 1 command line argument. */
else {
printf("Usage: %s [<input_file>]\n", argv[0]);
exit(8);
}

/* Do all sorts of secret stuff that makes the bomb harder to defuse. */
initialize_bomb();

printf("Welcome to my fiendish little bomb. You have 6 phases with\n");
printf("which to blow yourself up. Have a nice day!\n");

/* Hmm... Six phases must be more secure than one phase! */
input = read_line(); /* Get input */
phase_1(input); /* Run the phase */
phase_defused(); /* Drat! They figured it out!
* Let me know how they did it. */
printf("Phase 1 defused. How about the next one?\n");

/* The second phase is harder. No one will ever figure out
* how to defuse this... */
input = read_line();
phase_2(input);
phase_defused();
printf("That's number 2. Keep going!\n");

/* I guess this is too easy so far. Some more complex code will
* confuse people. */
input = read_line();
phase_3(input);
phase_defused();
printf("Halfway there!\n");

/* Oh yeah? Well, how good is your math? Try on this saucy problem! */
input = read_line();
phase_4(input);
phase_defused();
printf("So you got that one. Try this one.\n");

/* Round and 'round in memory we go, where we stop, the bomb blows! */
input = read_line();
phase_5(input);
phase_defused();
printf("Good work! On to the next...\n");

/* This phase will never be used, since no one will get past the
* earlier ones. But just in case, make this one extra hard. */
input = read_line();
phase_6(input);
phase_defused();

/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */

return 0;
}

可以看到程序在每个阶段之前调用read_line函数读取一行,然后进入各个phase。

输入方式

程序可以直接从键盘获取输入;或者以文件给出头一部分输入,随后用键盘输入,用以避免破解后面阶段时对前面阶段答案的重复输入。以文件给出输入时,需要在bomb的目录下新建文本文件文件(如ans.txt),输入若干个阶段的答案,在运行时传入另一个参数:

1
./bomb ans.txt

或者在gdb内:

1
2
gdb bomb
run ans.txt

注:当运行时出现permission denied,
右键文件->Properties(属性)->Permissions(许可)->把execute的Allow executing file as program属性勾上即可。
在这里插入图片描述
在这里插入图片描述

要用到的GDB指令

disas

用于反汇编某一函数(有了objdump反汇编出的就可以不用了)。

1
disas phase_1

break

break指令用于设置断点。可以设置于反汇编的某地址处:

1
break * 0x4013ec

或者设置于某一函数开始处偏移一定位置处:

1
break * phase_1+9

在这里插入图片描述

print

用于打印某处内存/寄存器的值,一般读内存加上类型强制转换:

1
2
3
4
5
6
7
8
9
10
11
(注意不是%rax而是$rax)
print $rax

以16进制输出
print /x $rax

即0x8(%rsp),以四字节整型解释,不加强制转换会被视为void*
print *(int*)($rsp+8)

以字符串方式解释0x403150处的数据
print (char*)0x403150

x

以一定格式输出字符串的值。
格式: x /nfu <addr>
其中:
n为要显示的单元数;
f为显示格式,本实验中有用的有d:十进制,x:十六进制
u为单元大小,b/h/w/g分别表示1/2/4/8字节。

1
2
以十六进制打印0x4050f0开始的80个8字节值
x /80xg 0x4050f0

阶段一

考察点:字符串比较;函数传参
反汇编如下(汇编中前面不空两格的是注释):

1
2
3
4
5
6
7
8
9
10
00000000004013ec <phase_1>:
4013ec: 48 83 ec 08 sub $0x8,%rsp
4013f0: be 50 31 40 00 mov $0x403150,%esi
4013f5: e8 b7 04 00 00 callq 4018b1 <strings_not_equal>
4013fa: 85 c0 test %eax,%eax
4013fc: 75 05 jne 401403 <phase_1+0x17>
4013fe: 48 83 c4 08 add $0x8,%rsp
401402: c3 retq
401403: e8 8b 05 00 00 callq 401993 <explode_bomb>
401408: eb f4 jmp 4013fe <phase_1+0x12>

可以看到,在程序调用strings_not_equal之前,将地址0x403150传入参数。猜测内存该处存放了答案字符串。用gdb设置断点,在此处查看该地址处的字符串:

1
print (char*)0x403150

在这里插入图片描述

即为phase_1的答案**I was trying to give Tina Fey more material.**。至于strings_not_equal函数具体如何实现,在这一阶段并不是重点,只要功能和函数名对上就不用费时间去考察了。

阶段二

考察点:循环
反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
000000000040140a <phase_2>:
let ebx = i
40140a: 53 push %rbx
40140b: 48 83 ec 20 sub $0x20,%rsp
40140f: 48 89 e6 mov %rsp,%rsi
401412: e8 a0 05 00 00 callq 4019b7 <read_six_numbers>
401417: 83 3c 24 01 cmpl $0x1,(%rsp)
40141b: 75 07 jne 401424 <phase_2+0x1a>
40141d: bb 01 00 00 00 mov $0x1,%ebx
401422: eb 0f jmp 401433 <phase_2+0x29>
401424: e8 6a 05 00 00 callq 401993 <explode_bomb>
401429: eb f2 jmp 40141d <phase_2+0x13>
40142b: e8 63 05 00 00 callq 401993 <explode_bomb>
401430: 83 c3 01 add $0x1,%ebx
401433: 83 fb 05 cmp $0x5,%ebx
401436: 7f 14 jg 40144c <phase_2+0x42>
401438: 48 63 d3 movslq %ebx,%rdx
40143b: 8d 43 ff lea -0x1(%rbx),%eax
40143e: 48 98 cltq
401440: 8b 04 84 mov (%rsp,%rax,4),%eax
401443: 01 c0 add %eax,%eax
401445: 39 04 94 cmp %eax,(%rsp,%rdx,4)
401448: 74 e6 je 401430 <phase_2+0x26>
40144a: eb df jmp 40142b <phase_2+0x21>
40144c: 48 83 c4 20 add $0x20,%rsp
401450: 5b pop %rbx
401451: c3 retq

在401412地址处调用read_six_numbers,推测要输入六个参数。随后比较(%rsp)与1,若(%rsp)!=1,则引爆炸弹,于是可知第一个参数(存放于栈指针处)为1.接下来程序初始化%ebx为1,并且在%ebx>5时跳转到40144c,通过phase_2.因此只需让程序顺利通过该循环即可。
设%ebx代表循环变量i,那么下面的循环相当于如下代码(省去了一些中间过程):

1
2
3
4
5
6
7
8
9
arg[0] = 1;//第1个参数
int i;//%ebx
for(i=1;i<=5;i++) {
int prev_arg = arg[i-1];//%eax
int now_arg = arg[i];//(%rsp,%rdx,4)
if(prev_arg * 2 != now_arg)
explode_bomb();
}
return;

该程序检查6个参数是否为首项为1,公比为2的等比数列。因此答案即为等比数列前6项1 2 4 8 16 32

阶段三

考察点:switch(跳转表)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
0000000000401452 <phase_3>:
401452: 48 83 ec 18 sub $0x18,%rsp
401456: 4c 8d 44 24 08 lea 0x8(%rsp),%r8
40145b: 48 8d 4c 24 07 lea 0x7(%rsp),%rcx
401460: 48 8d 54 24 0c lea 0xc(%rsp),%rdx
401465: be a6 31 40 00 mov $0x4031a6,%esi
40146a: b8 00 00 00 00 mov $0x0,%eax
40146f: e8 9c fc ff ff callq 401110 <__isoc99_sscanf@plt>
401474: 83 f8 02 cmp $0x2,%eax
401477: 7e 16 jle 40148f <phase_3+0x3d>
401479: 8b 44 24 0c mov 0xc(%rsp),%eax
40147d: 83 f8 07 cmp $0x7,%eax
401480: 0f 87 0d 01 00 00 ja 401593 <phase_3+0x141>
401486: 89 c0 mov %eax,%eax
401488: ff 24 c5 c0 31 40 00 jmpq *0x4031c0(,%rax,8)
40148f: e8 ff 04 00 00 callq 401993 <explode_bomb>
401494: eb e3 jmp 401479 <phase_3+0x27>
401496: 81 7c 24 08 b2 01 00 cmpl $0x1b2,0x8(%rsp)
40149d: 00
40149e: 75 0a jne 4014aa <phase_3+0x58>
4014a0: b8 65 00 00 00 mov $0x65,%eax
4014a5: e9 f3 00 00 00 jmpq 40159d <phase_3+0x14b>
4014aa: e8 e4 04 00 00 callq 401993 <explode_bomb>
4014af: b8 65 00 00 00 mov $0x65,%eax
4014b4: e9 e4 00 00 00 jmpq 40159d <phase_3+0x14b>
4014b9: 81 7c 24 08 46 02 00 cmpl $0x246,0x8(%rsp)
4014c0: 00
4014c1: 75 0a jne 4014cd <phase_3+0x7b>
4014c3: b8 74 00 00 00 mov $0x74,%eax
4014c8: e9 d0 00 00 00 jmpq 40159d <phase_3+0x14b>
4014cd: e8 c1 04 00 00 callq 401993 <explode_bomb>
4014d2: b8 74 00 00 00 mov $0x74,%eax
4014d7: e9 c1 00 00 00 jmpq 40159d <phase_3+0x14b>
4014dc: 81 7c 24 08 81 00 00 cmpl $0x81,0x8(%rsp)
4014e3: 00
4014e4: 75 0a jne 4014f0 <phase_3+0x9e>
4014e6: b8 6c 00 00 00 mov $0x6c,%eax
4014eb: e9 ad 00 00 00 jmpq 40159d <phase_3+0x14b>
4014f0: e8 9e 04 00 00 callq 401993 <explode_bomb>
4014f5: b8 6c 00 00 00 mov $0x6c,%eax
4014fa: e9 9e 00 00 00 jmpq 40159d <phase_3+0x14b>
4014ff: 81 7c 24 08 7f 02 00 cmpl $0x27f,0x8(%rsp)
401506: 00
401507: 75 0a jne 401513 <phase_3+0xc1>
401509: b8 68 00 00 00 mov $0x68,%eax
40150e: e9 8a 00 00 00 jmpq 40159d <phase_3+0x14b>
401513: e8 7b 04 00 00 callq 401993 <explode_bomb>
401518: b8 68 00 00 00 mov $0x68,%eax
40151d: eb 7e jmp 40159d <phase_3+0x14b>
40151f: 81 7c 24 08 12 01 00 cmpl $0x112,0x8(%rsp)
401526: 00
401527: 75 07 jne 401530 <phase_3+0xde>
401529: b8 6e 00 00 00 mov $0x6e,%eax
40152e: eb 6d jmp 40159d <phase_3+0x14b>
401530: e8 5e 04 00 00 callq 401993 <explode_bomb>
401535: b8 6e 00 00 00 mov $0x6e,%eax
40153a: eb 61 jmp 40159d <phase_3+0x14b>
40153c: 81 7c 24 08 74 03 00 cmpl $0x374,0x8(%rsp)
401543: 00
401544: 75 07 jne 40154d <phase_3+0xfb>
401546: b8 76 00 00 00 mov $0x76,%eax
40154b: eb 50 jmp 40159d <phase_3+0x14b>
40154d: e8 41 04 00 00 callq 401993 <explode_bomb>
401552: b8 76 00 00 00 mov $0x76,%eax
401557: eb 44 jmp 40159d <phase_3+0x14b>
401559: 81 7c 24 08 b9 03 00 cmpl $0x3b9,0x8(%rsp)
401560: 00
401561: 75 07 jne 40156a <phase_3+0x118>
401563: b8 75 00 00 00 mov $0x75,%eax
401568: eb 33 jmp 40159d <phase_3+0x14b>
40156a: e8 24 04 00 00 callq 401993 <explode_bomb>
40156f: b8 75 00 00 00 mov $0x75,%eax
401574: eb 27 jmp 40159d <phase_3+0x14b>
401576: 81 7c 24 08 af 00 00 cmpl $0xaf,0x8(%rsp)
40157d: 00
40157e: 75 07 jne 401587 <phase_3+0x135>
401580: b8 64 00 00 00 mov $0x64,%eax
401585: eb 16 jmp 40159d <phase_3+0x14b>
401587: e8 07 04 00 00 callq 401993 <explode_bomb>
40158c: b8 64 00 00 00 mov $0x64,%eax
401591: eb 0a jmp 40159d <phase_3+0x14b>
401593: e8 fb 03 00 00 callq 401993 <explode_bomb>
401598: b8 6c 00 00 00 mov $0x6c,%eax
40159d: 38 44 24 07 cmp %al,0x7(%rsp)
4015a1: 75 05 jne 4015a8 <phase_3+0x156>
4015a3: 48 83 c4 18 add $0x18,%rsp
4015a7: c3 retq
4015a8: e8 e6 03 00 00 callq 401993 <explode_bomb>
4015ad: eb f4 jmp 4015a3 <phase_3+0x151>

地址40146f调用了sscanf函数:获取格式化的字符串中的参数。接下来检查sscanf的返回值%eax:若%eax<=2,则引爆炸弹。
检查sscanf的格式串(存在0x4031a6):
在这里插入图片描述

第一个参数和第三个参数是int,第二个是char。从0x401456~0x401460传入的参数可以知道,三个参数分别存在0x7(%rsp),0x8(%rsp),0xc(%rsp)处。有了参数的输入格式,可以开始分析整个程序了。
在地址0x401480附近,可以得到如下结论:第一个参数一定要<=7,否则炸弹直接爆炸。紧接着为一条switch跳转语句:

跳转表位于0x4031c0处。而switch的对象是%rax(之前把第一个参数挪到%rax),而第一个参数<=7,所以猜测表项共有8项,即0~7.因此用gdb的x指令看0x4031c0处开始的八个八字节:
在这里插入图片描述

就获取了转移表的表项。选取一个最靠近函数末尾的进行分析:即跳转表偏移为7*8字节处的0x401576,当第一个参数为7时,跳转到0x401576.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
401576:	81 7c 24 08 af 00 00 	cmpl   $0xaf,0x8(%rsp)
40157d: 00
40157e: 75 07 jne 401587 <phase_3+0x135>
401580: b8 64 00 00 00 mov $0x64,%eax
401585: eb 16 jmp 40159d <phase_3+0x14b>
401587: e8 07 04 00 00 callq 401993 <explode_bomb>
40158c: b8 64 00 00 00 mov $0x64,%eax
401591: eb 0a jmp 40159d <phase_3+0x14b>
401593: e8 fb 03 00 00 callq 401993 <explode_bomb>
401598: b8 6c 00 00 00 mov $0x6c,%eax
40159d: 38 44 24 07 cmp %al,0x7(%rsp)
4015a1: 75 05 jne 4015a8 <phase_3+0x156>
4015a3: 48 83 c4 18 add $0x18,%rsp
4015a7: c3 retq
4015a8: e8 e6 03 00 00 callq 401993 <explode_bomb>
4015ad: eb f4 jmp 4015a3 <phase_3+0x151>

该部分的逻辑较简单。比较0xaf与0x8(%rsp);%al与0x7(%rsp).%al即%eax的低位字节,0x64代表ASCII中的’d’。因此只需使0x7(%rsp)和0x8(%rsp)取到’d’和0xaf(175)即可。因此第一个参数为7,第二个参数为’d’,第三个参数为175.

阶段四

考察点:递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
00000000004015ec <phase_4>:
4015ec: 48 83 ec 18 sub $0x18,%rsp
4015f0: 48 8d 4c 24 08 lea 0x8(%rsp),%rcx
4015f5: 48 8d 54 24 0c lea 0xc(%rsp),%rdx
4015fa: be 4f 33 40 00 mov $0x40334f,%esi
4015ff: b8 00 00 00 00 mov $0x0,%eax
401604: e8 07 fb ff ff callq 401110 <__isoc99_sscanf@plt>
401609: 83 f8 02 cmp $0x2,%eax
40160c: 75 0d jne 40161b <phase_4+0x2f>
40160e: 8b 44 24 0c mov 0xc(%rsp),%eax
401612: 85 c0 test %eax,%eax
401614: 78 05 js 40161b <phase_4+0x2f>
401616: 83 f8 0e cmp $0xe,%eax
401619: 7e 05 jle 401620 <phase_4+0x34>
40161b: e8 73 03 00 00 callq 401993 <explode_bomb>
401620: ba 0e 00 00 00 mov $0xe,%edx
401625: be 00 00 00 00 mov $0x0,%esi
40162a: 8b 7c 24 0c mov 0xc(%rsp),%edi
40162e: e8 7c ff ff ff callq 4015af <func4>
401633: 83 f8 04 cmp $0x4,%eax
401636: 75 07 jne 40163f <phase_4+0x53>
401638: 83 7c 24 08 04 cmpl $0x4,0x8(%rsp)
40163d: 74 05 je 401644 <phase_4+0x58>
40163f: e8 4f 03 00 00 callq 401993 <explode_bomb>
401644: 48 83 c4 18 add $0x18,%rsp
401648: c3 retq

0x401604处调用sscanf;紧接着比较%eax与2,仅当%eax==2时,炸弹才不会爆炸。因此输入参数为两个。接下来的语句限制arg[0](第一个参数,0xc(%rsp))必须大于等于0(由js指令看出);之后(0x401620~0x40162a)分别把arg[0],0,0xe移动到func4的三个参数上。即调用func4(arg[0], 0, 15)。0x401633处比较返回值%eax是否等于4,因此,需要分析当arg[0]为多少时,func4的值为4.于是对func4进行反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
x in %edi, y in %esi, z in %edx
00000000004015af <func4>:
4015af: 48 83 ec 08 sub $0x8,%rsp
4015b3: 89 d1 mov %edx,%ecx
4015b5: 29 f1 sub %esi,%ecx

int ret = ((((z - y) >> 31) + (z - y)) >> 1) + y;

4015b7: 89 c8 mov %ecx,%eax
4015b9: c1 e8 1f shr $0x1f,%eax
4015bc: 01 c8 add %ecx,%eax
4015be: d1 f8 sar %eax
4015c0: 01 f0 add %esi,%eax
4015c2: 39 f8 cmp %edi,%eax
4015c4: 7f 0c jg 4015d2 <func4+0x23>
4015c6: 7c 16 jl 4015de <func4+0x2f>

if(ret == 0) return 0;
4015c8: b8 00 00 00 00 mov $0x0,%eax
4015cd: 48 83 c4 08 add $0x8,%rsp
4015d1: c3 retq
if(ret > 0) return 2 * func4(x, y, ret - 1);
4015d2: 8d 50 ff lea -0x1(%rax),%edx
4015d5: e8 d5 ff ff ff callq 4015af <func4>
4015da: 01 c0 add %eax,%eax
4015dc: eb ef jmp 4015cd <func4+0x1e>
if(ret < 0) return 2 * func4(x, ret + 1, z);
4015de: 8d 70 01 lea 0x1(%rax),%esi
4015e1: e8 c9 ff ff ff callq 4015af <func4>
4015e6: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
4015ea: eb e1 jmp 4015cd <func4+0x1e>

在函数开始前标注了参数所在寄存器。在0x4015c2处比较%edi与%eax的值,%edi为传入的第一个参数x;%eax经过前几行的一系列操作,其值为449行注释的表达式。接下来分别于459,465,472行标注了比较的三种情况。可以将func4近似为如下的C语言递归函数:

1
2
3
4
5
6
int func4(int x, int l, int r) {
int mid = (((r - l) >> 31) + (r - l))/ 2 + l;
if(mid == x) return 0;
else if(mid > x) return 2 * func(x, l,mid - 1);
else return 2 * func(x, mid + 1, r) + 1;
}

用程序进行检验,很容易发现当arg[0]=2时,func4的值为4.
在这里插入图片描述

接下来回到phase_4的末尾部分:只要0x8(%rsp)(第二个参数)==4即可。因此答案为2 4.

阶段五

考察点:数组(指针)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0000000000401649 <phase_5>:
401649: 53 push %rbx
40164a: 48 89 fb mov %rdi,%rbx
40164d: e8 4b 02 00 00 callq 40189d <string_length>
401652: 83 f8 06 cmp $0x6,%eax
401655: 75 25 jne 40167c <phase_5+0x33>
401657: b9 00 00 00 00 mov $0x0,%ecx
40165c: b8 00 00 00 00 mov $0x0,%eax
401661: 83 f8 05 cmp $0x5,%eax
401664: 7f 1d jg 401683 <phase_5+0x3a>
401666: 48 63 d0 movslq %eax,%rdx
401669: 0f b6 14 13 movzbl (%rbx,%rdx,1),%edx
40166d: 83 e2 0f and $0xf,%edx
401670: 03 0c 95 00 32 40 00 add 0x403200(,%rdx,4),%ecx
401677: 83 c0 01 add $0x1,%eax
40167a: eb e5 jmp 401661 <phase_5+0x18>
40167c: e8 12 03 00 00 callq 401993 <explode_bomb>
401681: eb d4 jmp 401657 <phase_5+0xe>
401683: 83 f9 31 cmp $0x31,%ecx
401686: 75 02 jne 40168a <phase_5+0x41>
401688: 5b pop %rbx
401689: c3 retq
40168a: e8 04 03 00 00 callq 401993 <explode_bomb>
40168f: eb f7 jmp 401688 <phase_5+0x3f>

注意到0x40164d处调用了string_length函数,在0x401652处将返回值与6比较,推测输入为一个长度为6的字符串。接下来初始化%ecx=0,%eax=0,在%eax>5时,跳转至地址0x401683.因此下面是一个循环变量i(%eax)从0到5共6次的循环.接下来,0x401669对(%rbx,%rdx,1)做零扩展传送到%edx,将其按位与0xf(即取低四位)。先检验一下该内存地址是什么:
在这里插入图片描述
这次检测中输入了字符串”string”,而用char*方式解释%rbx开始的位置正好就是输入的字符串”string”.因此,%rbx就是输入的字符串的首地址,而%rdx=%eax是循环变量i。因此循环每次取arg[i],即输入字符串的第i位字符;并将其按位与0xf后的值存入%edx中。
继续看0x401670处:

1
401670:	03 0c 95 00 32 40 00 	add    0x403200(,%rdx,4),%ecx

发现它把0x403200偏移4*%edx处的四字节值累加到%ecx.因此可以推测出0x403200处有一个含16个int的数组(因为%edx是按位与0xf得来的,共有16种可能)。用x指令查看该数组:
在这里插入图片描述
该数组即

1
2
int A[] = {2, 10, 6, 1, 12, 16, 9, 3,
4, 7, 14, 5, 11, 8, 15, 13};

查看phase_5出循环后的代码:其比较%ecx是否等于0x31=49.

1
401683:	83 f9 31             	cmp    $0x31,%ecx

经过上述分析,可以将过程转化为如下C代码:

1
2
3
4
5
6
7
8
9
10
11
void phase_5() {
char input[10];
scanf("%s", input);
if(strlen(input) != 6) explode_bomb();
int i, sum = 0;
for(i = 0; i < 6; i++) {
int index = input[i] & 0xf;
sum += A[index];
}
if(sum != 49) explode_bomb();
}

因此,关键就在于选择输入字符串的值,使得6次累加的值为49.可以在A数组中找到7+7+7+7+7+14=49,即5个A[9]和1个A[10],接下来只要找到char型的ch1,ch2使得(ch1&0xf == 9) && (ch2&0xf == 10)即可。选择ch1=’i’,ch2=’j’即满足要求。因此只要输入5个i和1个j即可。(顺序可以调换)

阶段六

考察点:结构体,链表
phase_6的反汇编有100多行,分为多个步骤,接下来分步骤说明。

1
2
3
4
5
6
7
0000000000401691 <phase_6>:
401691: 41 54 push %r12
401693: 55 push %rbp
401694: 53 push %rbx
401695: 48 83 ec 50 sub $0x50,%rsp
401699: 48 8d 74 24 30 lea 0x30(%rsp),%rsi
40169e: e8 14 03 00 00 callq 4019b7 <read_six_numbers>

首先与前面的阶段一样,读入6个数字;由传入的参数(0x401699)可以知道,第一个读入的数字存在0x30(%rsp)处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let rbp = i, ebx = j
4016a3: bd 00 00 00 00 mov $0x0,%ebp
4016a8: eb 29 jmp 4016d3 <phase_6+0x42>
4016aa: e8 e4 02 00 00 callq 401993 <explode_bomb>
4016af: eb 36 jmp 4016e7 <phase_6+0x56>
4016b1: e8 dd 02 00 00 callq 401993 <explode_bomb>
4016b6: 83 c3 01 add $0x1,%ebx
4016b9: 83 fb 05 cmp $0x5,%ebx
4016bc: 7f 12 jg 4016d0 <phase_6+0x3f>
4016be: 48 63 c5 movslq %ebp,%rax
4016c1: 48 63 d3 movslq %ebx,%rdx
edi = arg[j]
4016c4: 8b 7c 94 30 mov 0x30(%rsp,%rdx,4),%edi
compare arg[i] and arg[j]
4016c8: 39 7c 84 30 cmp %edi,0x30(%rsp,%rax,4)
arg[i] != arg[j],jump to bomb_explode
4016cc: 75 e8 jne 4016b6 <phase_6+0x25>
4016ce: eb e1 jmp 4016b1 <phase_6+0x20>
4016d0: 44 89 e5 mov %r12d,%ebp
4016d3: 83 fd 05 cmp $0x5,%ebp
4016d6: 7f 18 jg 4016f0 <phase_6+0x5f>
4016d8: 48 63 c5 movslq %ebp,%rax
4016db: 8b 44 84 30 mov 0x30(%rsp,%rax,4),%eax
4016df: 83 e8 01 sub $0x1,%eax
4016e2: 83 f8 05 cmp $0x5,%eax
4016e5: 77 c3 ja 4016aa <phase_6+0x19>
4016e7: 44 8d 65 01 lea 0x1(%rbp),%r12d
4016eb: 44 89 e3 mov %r12d,%ebx
4016ee: eb c9 jmp 4016b9 <phase_6+0x28>

接下来(经过分析)是一个二重循环。首先初始化%ebp=0,设其为i;随后jmp到0x4016d3,当i>5时,结束这一部分。否则,把%ebp作符号扩展传送到%rax,把0x30(%rsp,%rax,4)即arg[i](rsp+0x30+4*%rax)传送到%eax.然后比较%eax-1与5(即%eax与6),若%eax>6,炸弹爆炸。因此推测出arg[i]<=6(i=0,1,…,5)
在0x4016e7,0x4016eb两行把%rbp+1即i+1赋给%ebx,跳转到0x4016b9.紧接着1比较%ebx与5,当%ebx>5时跳转(正因如此确定这是一个二重循环)。设%ebx为j,接下来的几行比较arg[j]与arg[i],当arg[i] == arg[j]时,炸弹爆炸。因此这一部分可以转化为如下的C代码:

1
2
3
4
5
6
7
8
int i, j;
for(i = 0; i < 6; i++) {
for(j = i + 1; j < 6; j++) {
if(arg[i] == arg[j]) {
explode_bomb();
}
}
}

从这一段C代码就可以看出这一部分是确保对任意的i != j,arg[i] != arg[j].再结合之前的arg[i]<=6,可以得出,输入应当是一个1~6的排列。

继续下一段:

1
2
3
4
5
6
7
8
9
10
let eax = i
4016f0: b8 00 00 00 00 mov $0x0,%eax
4016f5: eb 13 jmp 40170a <phase_6+0x79>
4016f7: 48 63 c8 movslq %eax,%rcx
4016fa: ba 07 00 00 00 mov $0x7,%edx
4016ff: 2b 54 8c 30 sub 0x30(%rsp,%rcx,4),%edx
401703: 89 54 8c 30 mov %edx,0x30(%rsp,%rcx,4)
401707: 83 c0 01 add $0x1,%eax
40170a: 83 f8 05 cmp $0x5,%eax
40170d: 7e e8 jle 4016f7 <phase_6+0x66>

开始初始化%eax=0,立即跳转到0x40170a与5进行比较.由经验,%eax一般为第一层循环变量,设其为i;在i<=5时,跳转到0x4016f7,接下来的0x4016fa~0x401703将7-arg[i]赋值给arg[i],即对参数进行了一次变换,但六个参数还是一个1~6的排列。

下一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let esi = i, eax = j, edx = (pointer*)ptr, rcx = k
40170f: be 00 00 00 00 mov $0x0,%esi
401714: eb 17 jmp 40172d <phase_6+0x9c>
401716: 48 8b 52 08 mov 0x8(%rdx),%rdx
40171a: 83 c0 01 add $0x1,%eax
40171d: 48 63 ce movslq %esi,%rcx
401720: 39 44 8c 30 cmp %eax,0x30(%rsp,%rcx,4)
401724: 7f f0 jg 401716 <phase_6+0x85>
401726: 48 89 14 cc mov %rdx,(%rsp,%rcx,8)
40172a: 83 c6 01 add $0x1,%esi
40172d: 83 fe 05 cmp $0x5,%esi
401730: 7f 0c jg 40173e <phase_6+0xad>

loop initialize:
401732: b8 01 00 00 00 mov $0x1,%eax
401737: ba d0 52 40 00 mov $0x4052d0,%edx
40173c: eb df jmp 40171d <phase_6+0x8c>

本段初始化%esi为0,随后jmp到0x40172d与5比较。由经验设%esi=i,当i<=5时,把%eax赋值1,%edx赋值0x4052d0.因此推测,%eax是一个int型临时变量j;%edx是一个指针型临时变量,但不确定其类型,记为pointer *ptr.
做完这些初始化后jmp到0x40171d,把i赋给%rcx(记为k,这些对应关系在这段汇编的开始标注);比较arg[k]与j,若arg[k]>j,跳到0x401716,做

1
401716:	48 8b 52 08          	mov    0x8(%rdx),%rdx

这样一条操作,随后j++。图中指令形式很类似于链表,即跳转到下一个结点:ptr=ptr->next.把自己指向的位置偏移8处赋给自身,说明链表节点的next指针在首地址偏移8处。因此节点数据大小应为8字节。推测链表结构声明如下:

1
2
3
4
struct node {
long val;//8 bytes
node *next;//pointer
};

继续分析:程序做完上面的操作后又会落到判断arg[k]和j的大小关系处,可以把这里转化为一个while循环。当跳出while循环后,

1
2
401726:	48 89 14 cc          	mov    %rdx,(%rsp,%rcx,8)
40172a: 83 c6 01 add $0x1,%esi

把%rdx(ptr)赋值给rsp偏移为8*%rcx(8*i)处,因此这部分汇编可转换为如下C语言(其中的rsp[k]可视为一个指针数组:pointer *rsp[6]; ):

1
2
3
4
5
6
7
8
9
10
11
12
  //let esi = i,eax = j,edx = ptr,rcx = k
int i, j, k, ptr;
for(i = 0; i <= 5;i++) {
k = i;
j = 1;
ptr = 0x4052d0;
while(arg[k] > j) {
ptr = ptr->next;
j++;
}//add until arg[i] == j
rsp[k] = ptr;
}

可以看到,对于每个arg[i],ptr初始化都在0x4052d0处,每次j自增,ptr都会指向自己的下一个元素。最后,把ptr最终指向的值赋给rsp寄存器偏移k的位置。注意到在单次循环内k没有改变,因此可以把k和i等同,从而消除k:

1
2
3
4
5
6
7
8
9
10
int i, j, ptr;
for(i = 0; i <= 5;i++) {
j = 1;
ptr = 0x4052d0;
while(arg[i] > j) {
ptr = ptr->next;
j++;
}//add until arg[i] == j
rsp[i] = ptr;
}

为了验证猜想,我们需要查看运行时0x4052d0的内存。经过上面的假设,可以得到节点大小为16字节,应该共有6个这样的节点。因此,运行时用x指令查看:
在这里插入图片描述
查看的结果很明显:结构体名为node1~node6,每个节点的第二个数据首尾相接(如,0x4052d0的第二个数据段为0x4052e0,正好是下一块内存的地址),第一个数据应当就是数据域。
验证了上面的猜想后,就可以解释这段代码的行为了:rsp[i]指向从0x4052d0开始的第arg[i]个节点(更准确地说,rsp[i]保存该节点的首地址)。如arg[0]=5,那么j应该向后跳4次,ptr指向第5个节点(0x405310)。由于arg[i]是一个排列,因此rsp[i]指向的内容也应该不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let rbx = ptr1, rcx = ptr2, eax = i, rdx = j, ebp = k
The arg[i]-th node's next node is the arg[i+1]-th node
40173e: 48 8b 1c 24 mov (%rsp),%rbx
401742: 48 89 d9 mov %rbx,%rcx
401745: b8 01 00 00 00 mov $0x1,%eax
40174a: eb 11 jmp 40175d <phase_6+0xcc>
40174c: 48 63 d0 movslq %eax,%rdx
40174f: 48 8b 14 d4 mov (%rsp,%rdx,8),%rdx
401753: 48 89 51 08 mov %rdx,0x8(%rcx)
401757: 83 c0 01 add $0x1,%eax
40175a: 48 89 d1 mov %rdx,%rcx
40175d: 83 f8 05 cmp $0x5,%eax
401760: 7e ea jle 40174c <phase_6+0xbb>
401762: 48 c7 41 08 00 00 00 movq $0x0,0x8(%rcx)
401769: 00

下一部分:把rsp[0]移动到%rbx,设%rbx为node *ptr1;又将%rbx传送给%rcx,设%rcx为node *ptr2.%eax初始化为1,设为i。对于i<=5的情况,程序跳转到0x40174c:把%rdx赋值为rsp[i](在0x40174f),再把0x8(%rcx)赋值%rdx,即ptr2->next = rsp[i];
0x40175a处的mov指令把%rcx赋值%rdx=rsp[i].出循环后(0x401762)给ptr2->next赋值0,即NULL.结合起来即

1
2
3
4
5
6
node *ptr2 = rsp[0];
for(int i = 1; i <= 5; i++) {
ptr2->next = rsp[i];
ptr2 = ptr2->next;
}
ptr->next = NULL;

因此,这段代码的本质即对链表进行了一次重排序,规则为:第arg[i]个节点的下一个节点为第arg[i+1]个节点。如:arg[]={3,2,1,6,5,4},那么第3个节点为头,随后连接第2个节点,以此类推。(这里的第i个节点指的是内存的排列顺序:0x4052d0为第一个,0x4052e0为第二个)

接下来就是phase_6的最后一部分:对重排序的链表判断是否符合要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let rbx = ptr1, rcx = ptr2, eax = i, rdx = j, ebp = k
40176a: bd 00 00 00 00 mov $0x0,%ebp
40176f: eb 07 jmp 401778 <phase_6+0xe7>
401771: 48 8b 5b 08 mov 0x8(%rbx),%rbx
401775: 83 c5 01 add $0x1,%ebp
401778: 83 fd 04 cmp $0x4,%ebp
40177b: 7f 11 jg 40178e <phase_6+0xfd>
40177d: 48 8b 43 08 mov 0x8(%rbx),%rax
401781: 8b 00 mov (%rax),%eax
401783: 39 03 cmp %eax,(%rbx)
401785: 7d ea jge 401771 <phase_6+0xe0>
401787: e8 07 02 00 00 callq 401993 <explode_bomb>
40178c: eb e3 jmp 401771 <phase_6+0xe0>
40178e: 48 83 c4 50 add $0x50,%rsp
401792: 5b pop %rbx
401793: 5d pop %rbp
401794: 41 5c pop %r12
401796: c3 retq

把%ebp初始化为0,随即跳到0x401778.将其与4比较。若%ebp>4,就成功到达phase_6的结尾,因此只需度过这个循环即可。0x40177d把ptr1->next赋值给%rax,然后把(%rax)即ptr1->next->val(由于结构体偏移为0的地方为其数据段)赋值给%eax,比较%eax=ptr1->next->val与(%rbx)=ptr1->val.这里比较的逻辑很明显:比较ptr1指向的节点的数据是否比ptr1->next的大。还有一点需要注意:由于cmp指令其中一个参数是%eax,因此cmp应补为cmpl,按int型进行比较,而非long型。

那么答案的构造方法就清楚了:只需在重排序步骤中,使得重排序后的链表的数据递减。之前内存中各节点的数据从左到右分别为(按int型解释,用gdb print指令)258,355,577,973,465,330.
那么表头应该为973,随后是577,465,355,330,258.
按重排序的规则,很容易构造出符合的arg[]={4,3,5,2,6,1}.(第四个节点后面是第三个,然后是第二个,…)但是在前面程序又做了一步处理:arg[i] = 7 - arg[i].因此,需要把这个arg数组还原(用7减)。因此最初输入的arg就应该是{3,4,2,5,1,6}.
在漫长的分析后,终于得出六个参数依次为:3 4 2 5 1 6.

隐藏阶段

考察点:递归,结构体,二叉查找树
在破解phase_6的时候就可以发现下面还有fun7和secret_phase函数。搜索secret_phase的调用,发现其调用在phase_defused中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
0000000000401b24 <phase_defused>:
401b24: 83 3d 41 3c 00 00 06 cmpl $0x6,0x3c41(%rip) # 40576c <num_input_strings>
401b2b: 74 01 je 401b2e <phase_defused+0xa>
401b2d: c3 retq
401b2e: 48 83 ec 68 sub $0x68,%rsp
401b32: 4c 8d 44 24 10 lea 0x10(%rsp),%r8
401b37: 48 8d 4c 24 08 lea 0x8(%rsp),%rcx
401b3c: 48 8d 54 24 0c lea 0xc(%rsp),%rdx
401b41: be 99 33 40 00 mov $0x403399,%esi
401b46: bf 70 58 40 00 mov $0x405870,%edi
401b4b: b8 00 00 00 00 mov $0x0,%eax
401b50: e8 bb f5 ff ff callq 401110 <__isoc99_sscanf@plt>
401b55: 83 f8 03 cmp $0x3,%eax
401b58: 74 0f je 401b69 <phase_defused+0x45>
401b5a: bf d8 32 40 00 mov $0x4032d8,%edi
401b5f: e8 fc f4 ff ff callq 401060 <puts@plt>
401b64: 48 83 c4 68 add $0x68,%rsp
401b68: c3 retq
401b69: be a2 33 40 00 mov $0x4033a2,%esi
401b6e: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
401b73: e8 39 fd ff ff callq 4018b1 <strings_not_equal>
401b78: 85 c0 test %eax,%eax
401b7a: 75 de jne 401b5a <phase_defused+0x36>
401b7c: bf 78 32 40 00 mov $0x403278,%edi
401b81: e8 da f4 ff ff callq 401060 <puts@plt>
401b86: bf a0 32 40 00 mov $0x4032a0,%edi
401b8b: e8 d0 f4 ff ff callq 401060 <puts@plt>
401b90: b8 00 00 00 00 mov $0x0,%eax
call secret_phase!
401b95: e8 3a fc ff ff callq 4017d4 <secret_phase>
401b9a: eb be jmp 401b5a <phase_defused+0x36>

phase_defused是一个在每次通过阶段后都调用的一个函数。为了使控制到达secret_phase,需要在0x401b2b处通过检验0x3c41(%rip)==6.注意到后面标记40576c ,推测它是0x3c41(%rip)处的一个int型。(事实上0x3c41+%rip=0x3c41+0x401b2b=0x40576c,考虑到%rip应该指向下一条指令)
经过多次检验,num_input_strings是输入字符串数(由于字符串不对就会立即爆炸,故也是通过阶段数)。那么隐藏阶段就是在第六阶段通过后进行判断的。在0x401b2e处设置断点,通过六阶段后控制也的确转移到这里。再看0x401b55处的判断:cmp $0x3,%eax,判断之前的一个sscanf的读入参数的个数是否为3.既然此处没有读入(完成六阶段后程序直接结束),那么这个sscanf就应该读的是之前的字符串。可以看到第一个参数是%edi=0x405870。在之前读入字符串的函数(read_line)后设置断点,可以发现0x405870是第四次读入的答案首地址。表面上第四阶段读入两个数字(2,4),但隐藏阶段需要在这里额外输入另一个参数触发。
在这里插入图片描述
查看sscanf的格式串(存于0x403399),可以直到第三个参数应为字符串。注意到后面调用了一次strings_not_equal,用一阶段的方法可以得到这个字符串为”DrEvil”.于是在第四阶段后面额外附加上这个字符串,进入了隐藏阶段。
在这里插入图片描述
接下来分析secret_phase:下面为其反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
00000000004017d4 <secret_phase>:
4017d4: 53 push %rbx
4017d5: e8 1c 02 00 00 callq 4019f6 <read_line>
4017da: 48 89 c7 mov %rax,%rdi
4017dd: e8 5e f9 ff ff callq 401140 <atoi@plt>
4017e2: 89 c3 mov %eax,%ebx
4017e4: 8d 40 ff lea -0x1(%rax),%eax
4017e7: 3d e8 03 00 00 cmp $0x3e8,%eax
if(val > 1001) explode_bomb();
4017ec: 77 22 ja 401810 <secret_phase+0x3c>
4017ee: 89 de mov %ebx,%esi
4017f0: bf f0 50 40 00 mov $0x4050f0,%edi
tmp = fun7(0x4050f0, val)
4017f5: e8 9d ff ff ff callq 401797 <fun7>
4017fa: 83 f8 03 cmp $0x3,%eax
if(tmp != 3) explode_bomb();
4017fd: 75 18 jne 401817 <secret_phase+0x43>
4017ff: bf 80 31 40 00 mov $0x403180,%edi
401804: e8 57 f8 ff ff callq 401060 <puts@plt>
401809: e8 16 03 00 00 callq 401b24 <phase_defused>
40180e: 5b pop %rbx
40180f: c3 retq
401810: e8 7e 01 00 00 callq 401993 <explode_bomb>
401815: eb d7 jmp 4017ee <secret_phase+0x1a>
401817: e8 77 01 00 00 callq 401993 <explode_bomb>
40181c: eb e1 jmp 4017ff <secret_phase+0x2b>

可以看到0x4017dd调用了atoi,那么输入为一个字符串,并将其转为数字。0x4017e2~0x404017ec判断了该数字应<=1001.随后调用fun7(0x4050f0, %edi)
(%edi就是输入的值),将其返回值与0x3比较。因此还需分析0x4050f0处的内存和fun7的行为,使得fun7返回3.先看0x4050f0处的内存:
在这里插入图片描述
可以看到和阶段6类似,每个结构体大小为32字节,每个数据占8字节;推测第一个为数据域,而第二个第三个显然为地址形式,推测为指针。第四个应为对齐要求,所有都为0.对于每个结构体有两个指针,很容易想到它是一棵二叉树。

可以推测节点定义如下:

1
2
3
4
5
struct node {
long val;
node *lson;
node *rson;
};

在这里插入图片描述
继续检查内存,发现这棵二叉树共有15个节点,正好符合4层满二叉树的节点个数。按照内存画出树的结构:
在这里插入图片描述
(只画出数据,因为地址意义不大)
可以看到它正好符合二叉查找树的结构:左儿子<根<右儿子。那么现在只需要弄清楚fun7的行为即可。fun7的反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let rdi = ptr, esi = val
0000000000401797 <fun7>:
if(ptr == NULL) return -1;
401797: 48 85 ff test %rdi,%rdi
40179a: 74 32 je 4017ce <fun7+0x37>
40179c: 48 83 ec 08 sub $0x8,%rsp
compare val and ptr->val
4017a0: 8b 07 mov (%rdi),%eax
4017a2: 39 f0 cmp %esi,%eax
4017a4: 7f 0c jg 4017b2 <fun7+0x1b>
4017a6: 75 17 jne 4017bf <fun7+0x28>
drop to here(when val == ptr->val):
4017a8: b8 00 00 00 00 mov $0x0,%eax
4017ad: 48 83 c4 08 add $0x8,%rsp
4017b1: c3 retq
if(val < ptr->val) return 2 * fun7(ptr->lson, val);
4017b2: 48 8b 7f 08 mov 0x8(%rdi),%rdi
4017b6: e8 dc ff ff ff callq 401797 <fun7>
4017bb: 01 c0 add %eax,%eax
4017bd: eb ee jmp 4017ad <fun7+0x16>
if(val > ptr->val) return 2 * fun7(ptr->rson, val);
4017bf: 48 8b 7f 10 mov 0x10(%rdi),%rdi
4017c3: e8 cf ff ff ff callq 401797 <fun7>
4017c8: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
4017cc: eb df jmp 4017ad <fun7+0x16>
4017ce: b8 ff ff ff ff mov $0xffffffff,%eax
4017d3: c3 retq

fun7的行为比较清晰。如注释标记,先检查第一个参数%rdi(称为ptr)是否为0(即NULL),若为NULL,返回0xffffffff即-1;然后把(%rdi),即ptr->val传送给%eax,将其与第二个参数%esi(称为val)比较,这与二叉查找树的查找过程非常相似。随后分出三种情况:大于/等于/小于。
$(1)$对于val==ptr->val的情况,直接下落到0x4017a8,返回0;
$(2)$对于valval的情况,返回2*fun7(0x8(%rdi),%esi),由节点的定义可知就是2*func(ptr->lson,val);
$(3)$对于val>ptr->val的情况,返回
2*fun7(0x10(%rdi),%esi)+1=2*fun7(ptr->rson,val)+1.
可以写出C代码:

1
2
3
4
5
6
int fun7(node *ptr, int val) {
if(ptr == NULL) return -1;
if(ptr->val == val) return 0;
if(val < ptr->val) return 2 * fun7(ptr->lson, val);
if(val > ptr->val) return 2 * fun7(ptr->rson, val);
}

接下来只需要寻找输入val,使得fun7(root,val)返回3即可。3可以通过
$v_1=20+1=1;$
$v_2=2
v_1+1=3$
得到。从二叉查找树的根root向下找,由于都是$2v+1$的形式,因此需要走两次右儿子,对照二叉树图可得该值为0x6b=107.也就是说,若输入107,在0x6b处函数返回0,然后在0x32处返回2*0+1=1,最后在根处返回21+1=3.因此,输入值应当为*107.
在这里插入图片描述

答案

以下为七个阶段(包含隐藏阶段)的ans.txt内容(答案可能不唯一,如第三阶段和第五阶段):

1
2
3
4
5
6
7
I was trying to give Tina Fey more material.
1 2 4 8 16 32
7 d 175
2 4 DrEvil
iiiiij
3 4 2 5 1 6
107