0%

CSAPP 二进制炸弹实验

实验简介

二进制炸弹是一个作为目标代码提供的程序。运行时提示用户输入6个不同的字符串,如其中一个字符串不正确,炸弹会引爆并打印一条错误信息。需要通过反汇编确定输入的6个字符串,从而拆除炸弹。

知识点

  • 汇编语言基础
  • GDB和OBJDUMP工具的使用

实验环境

Centos7 x86_64

获取二进制炸弹

首先从CSAPP官网获取二进制炸弹bomb.tar: http://csapp.cs.cmu.edu/3e/labs.html

在linux下执行tar xvf bomb.tar,得到二进制炸弹的文件,文件列表如下:

1
2
|-- bomb		# 二进制炸弹,x86-64位
|-- bomb.c # 主程序,逻辑是接受用户输入的6个字符串,并判断每个字符串是否正确。如果正确,调用phase_defused进入下一关,否则调用explode_bomb引爆炸弹

第一关

1. 首先理解main函数执行过程

反汇编炸弹,使用objdump -d bomb > bomb.txt命令,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
0000000000400da0 <main>:
400da0: 53 push %rbx
......
400e19: e8 84 05 00 00 callq 4013a2 <initialize_bomb>
400e1e: bf 38 23 40 00 mov $0x402338,%edi
400e23: e8 e8 fc ff ff callq 400b10 <puts@plt>
400e28: bf 78 23 40 00 mov $0x402378,%edi
400e2d: e8 de fc ff ff callq 400b10 <puts@plt>
400e32: e8 67 06 00 00 callq 40149e <read_line> # 接受用户输入的字符串,保存到%rax寄存器
400e37: 48 89 c7 mov %rax,%rdi # 用户输入的字符串保存在$rdi寄存器,并作为phase_1函数的第一个入参传递
400e3a: e8 a1 00 00 00 callq 400ee0 <phase_1> # 调用phase_1(), 执行第一关代码
400e3f: e8 80 07 00 00 callq 4015c4 <phase_defused>

查看0x400e32处的代码: 主程序调用read_line接收用户输入的字符串,保存到$rax,并且将该字符串作为函数入参传递给phase_1,执行第一阶段的代码。

这里需要理解x86-64的过程调用规则:(关于过程调用,可参考《CSAPP原书第三版》3.7小节 —— 过程)

  • 函数调用中,利用%rax寄存器保存返回值。
  • 关于参数传递 , 如果函数参数不超过6个,会依次通过%rdi, %rsi, %rdx, %rcx, %r8, %r9传递; 如超过6个参数,超出的参数利用栈传递。
  • %rbx, %rbp, %r12~%15被划分为被调用者保存寄存器;其余的寄存器,除了栈指针%rsp,都被划分为调用者保存寄存器

2. 理解phase_1执行过程

查看phase_1的代码,如下:

1
2
3
4
5
6
7
8
9
0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 sub $0x8,%rsp
400ee4: be 00 24 40 00 mov $0x402400,%esi # $esi作为strings_not_equal函数的第二个参数传递
400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal> # 调用strings_not_equal函数,比较用户输入的字符串($rdi)和$esi处的字符串是否相等
400eee: 85 c0 test %eax,%eax # 判断%eax的值是否为0
400ef0: 74 05 je 400ef7 <phase_1+0x17> # 如%eax等于0,跳转到400ef7,正常退出
400ef2: e8 43 05 00 00 callq 40143a <explode_bomb> # 如%eax不等于0,炸弹爆炸
400ef7: 48 83 c4 08 add $0x8,%rsp
400efb: c3 retq

phase_1执行过程如下:

  • 首先通过callq指令调用strings_not_equal函数。根据过程调用的规则,可以确定这个函数接受2个参数。第一个参数是%rdi, 上面分析过,是我们拆弹时输入的字符串;第二个参数是$esi,值为0x402400。顾名思义,strings_not_equal函数用来判断两个字符串是否相等。
  • 接着用test指令判断函数的返回值是否为0。如果为0进行跳转并返回,调用phase_defused拆除炸弹,否则通过callq指令调用explode_bomb,引爆炸弹。

因此,拆弹的关键在于,确认0x402400地址处的字符串是什么。只需要在0x400ee9处打个gdb断点就可以了。

3. gdb调试炸弹

执行gdb bomb, 设置断点b *0x400ee9, 执行run。 此时程序会要求用户输入字符串,先随便输一个字符串使程序运行到我们设置的断点 0x400ee9处,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# gdb bomb
(gdb) b *0x400ee9 # 设置断点, b相当于break
Breakpoint 1 at 0x400ee9
(gdb) r # 运行程序, r相当于run
Starting program: /home/pc/CSAPP/bomb/bomb
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
123 # 随便输入一个字符串,让程序运行到断点0x400ee9
Breakpoint 1, 0x0000000000400ee9 in phase_1 ()
(gdb) p (char *)$rdi # $rdi保存用户输入字符串,为123,与x/s $rdi指令等价
$2 = 0x603780 <input_strings> "123"
(gdb) x/s $esi # 查看esi处字符串,这就是第一关的答案
$1 = 0x402400 "Border relations with Canada have never been better."

查看%esi处字符串, 即第一关的答案,如下:

Border relations with Canada have never been better.

quit指令退出gdb, 新建一个文件answer,将答案写入该文件。重新执行bomb程序,指定answer文件名作为参数,如下:

1
2
3
4
# ./bomb answer
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?

显示Phase 1 defused, 表示第一关已成功通过。

几个调试汇编代码相关的GDB指令

使用disassemble指令查看汇编代码,箭头表示下一步即将执行的汇编指令。

si指令用于单步执行汇编代码,相当于stepi

ni指令以函数调用为单位进行单步执行, 相当于nexti

1
2
3
4
5
6
7
8
9
10
11
(gdb) disassemble
Dump of assembler code for function phase_1:
0x0000000000400ee0 <+0>: sub $0x8,%rsp
0x0000000000400ee4 <+4>: mov $0x402400,%esi
=> 0x0000000000400ee9 <+9>: callq 0x401338 <strings_not_equal>
0x0000000000400eee <+14>: test %eax,%eax
0x0000000000400ef0 <+16>: je 0x400ef7 <phase_1+23>
0x0000000000400ef2 <+18>: callq 0x40143a <explode_bomb>
0x0000000000400ef7 <+23>: add $0x8,%rsp
0x0000000000400efb <+27>: retq
End of assembler dump.

更多用法参考gdb手册或《CSAPP 原书第3版》3.10.2小节 —— 使用GDB调试器

拆弹小技巧

  • 查看bomb符号表或者直接查看汇编代码,会发现有个explode_bomb符号,该函数用来引爆炸弹。

  • 调试时可以对该函数设置断点,在拆弹失败时暂停运行,不让其爆炸,便于调试。可使用 b explode_bomb指令设置断点

第二关

先贴出第二关phase_2的部分汇编代码:

1
2
3
4
5
6
7
0000000000400efc <phase_2>:
400efc: 55 push %rbp
400efd: 53 push %rbx
400efe: 48 83 ec 28 sub $0x28,%rsp
400f02: 48 89 e6 mov %rsp,%rsi # %rsi作为read_six_numbers的第二个入参传递, $rsp既是入参也是出参;第一个入参为$rdi, 即用户输入的字符串
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers> # 读六个数字
......

phase_2程序先调用read_six_numbers。该函数接受两个参数,第一个参数为$rdi, 即我们输入的字符串;第二个参数为$rsp, $rsp既是入参也是出参,用于保存read_six_numbers函数解析$rdi后得到的6个整数。想得到这个结论,需要分析read_six_numbers代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(gdb) disassemble read_six_numbers
Dump of assembler code for function read_six_numbers:
0x000000000040145c <+0>: sub $0x18,%rsp
0x0000000000401460 <+4>: mov %rsi,%rdx # sscanf函数的第1个可变参数,第3个参数,通过%rdx传递
0x0000000000401463 <+7>: lea 0x4(%rsi),%rcx # sscanf函数的第2个可变参数,第4个参数, 通过%rcx传递
0x0000000000401467 <+11>: lea 0x14(%rsi),%rax # sscanf函数的第6个可变参数,第8个参数,通过栈传递
0x000000000040146b <+15>: mov %rax,0x8(%rsp)
0x0000000000401470 <+20>: lea 0x10(%rsi),%rax # sscanf函数的第5个可变参数,第7个参数,通过栈传递
0x0000000000401474 <+24>: mov %rax,(%rsp)
0x0000000000401478 <+28>: lea 0xc(%rsi),%r9 # sscanf函数的第4个可变参数,第6个参数, 通过%r9传递
0x000000000040147c <+32>: lea 0x8(%rsi),%r8 # sscanf函数的第3个可变参数,第5个参数, 通过%r8传递
0x0000000000401480 <+36>: mov $0x4025c3,%esi # sscanf函数的第2个参数,通过%esi传递,0x4025c3地址的格式化字符串为"%d %d %d %d %d %d",表示输入字符串应该为6个整数
0x0000000000401485 <+41>: mov $0x0,%eax
0x000000000040148a <+46>: callq 0x400bf0 <__isoc99_sscanf@plt>
0x000000000040148f <+51>: cmp $0x5,%eax # sscanf读入的可变参数需大于5个,否则爆炸
0x0000000000401492 <+54>: jg 0x401499 <read_six_numbers+61>
0x0000000000401494 <+56>: callq 0x40143a <explode_bomb>
0x0000000000401499 <+61>: add $0x18,%rsp
0x000000000040149d <+65>: retq

查看0x400f02, 0x401463 ~ 0x40147c处的代码,我们可以把调用者phase_2中的$rsp看作一个一维数组的首地址,该数组的长度为6,内容依次为$rsp, $rsp + 4, $rsp + 8, $rsp + 12, $rsp + 16, $rsp + 20,用于 保存sscanf函数执行后生成的6个整数。

炸弹用sscanf读取并解析用户输入字符串

注意到0x40148a处调用sscanf函数,作用是从用户输入的字符串%rdi中解析出6个整数,保存到调用者phase_2中的$rsp

C语言中sscanf函数原型如下:

1
int sscanf(const char *str, const char *format, ...);

对照汇编代码, $rdi相当于sscanf的参数str;查看0x401480代码,$esi相当于sscanf的参数format, 用gdb查看0x4025c3处的字符串,为"%d %d %d %d %d %d", 说明sscanf中的可变参数个数为6,且都是指向int类型的地址。

1
2
(gdb) x/s 0x4025c3
0x4025c3: "%d %d %d %d %d %d"

可以看出sscanf函数实际上接收8个入参。read_six_numbers利用%rdi, %rsi%rdx, %rcx, %r8, %r9分别传递用户输入字符串、格式化字符串、6个整数中的前4个。而第5、6个整数超出了六个参数,需通过栈传递。

到此,确定了第二关需要输入6个整数, 且这6个整数保存在调用者phase_2%rsp,用gdb验证这个结论:

1
2
3
4
5
6
7
8
gdb bomb
(gdb) set args answer # 第二行可随便输入6个数,例如1 2 3 4 5 6
(gdb) b *0x400f0a
(gdb) c
Breakpoint 3, 0x0000000000400f0a in phase_2 ()
(gdb) x/6x $rsp # 查看$rsp, 和输入字符串中6个数一致
0x7fffffffe420: 0x00000001 0x00000002 0x00000003 0x00000004
0x7fffffffe430: 0x00000005 0x00000006

再看下phase_2完整代码:

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
0000000000400efc <phase_2>:
400efc: 55 push %rbp
400efd: 53 push %rbx
400efe: 48 83 ec 28 sub $0x28,%rsp
400f02: 48 89 e6 mov %rsp,%rsi # %rsi作为read_six_numbers的第二个入参传递, $rsp既是入参也是出参;第一个入参为$rdi, 即用户输入的字符串
400f05: e8 52 05 00 00 callq 40145c <read_six_numbers> # 读六个数字
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp) # %rsp为调用者保存寄存器,过程调用前后值不变,因此保存的是read_six_numbers输出的6个数,(%rsp)保存的是第一个整数
400f0e: 74 20 je 400f30 <phase_2+0x34>
400f10: e8 25 05 00 00 callq 40143a <explode_bomb>
400f15: eb 19 jmp 400f30 <phase_2+0x34># 将6个整数看作一个数组
400f17: 8b 43 fc mov -0x4(%rbx),%eax # 将数组前一个数保存到%eax
400f1a: 01 c0 add %eax,%eax # 将%eax乘以2
400f1c: 39 03 cmp %eax,(%rbx) # 判断当前整数是否为前一个数的两倍, 不等则爆炸,相等跳转到400f25,
400f1e: 74 05 je 400f25 <phase_2+0x29>
400f20: e8 15 05 00 00 callq 40143a <explode_bomb>
400f25: 48 83 c3 04 add $0x4,%rbx # 每次循环,将rbx值加4,即指向数组的下一个元素
400f29: 48 39 eb cmp %rbp,%rbx # rbp指向数组结尾,标识循环是否结束
400f2c: 75 e9 jne 400f17 <phase_2+0x1b>
400f2e: eb 0c jmp 400f3c <phase_2+0x40>
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx # 最初rbx指向第二个数,
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp # %rbp = $rsp + 24
400f3a: eb db jmp 400f17 <phase_2+0x1b>
400f3c: 48 83 c4 28 add $0x28,%rsp
400f40: 5b pop %rbx
400f41: 5d pop %rbp
400f42: c3 retq

0x400f0a: cmpl $0x1,(%rsp)可知,第一个整数一定是1,然后跳转到0x400f30进入循环。

将这6个数看作一个数组, 由0x400f30处代码,$rbx可看作这个数组的下标,初始值为1,指向第2个整数;由0x400f35处代码,$rbp标识着数组的结尾,用于判断循环是否退出。

0x400f17 ~ 0x400f1c代码可知,每次循环判断数组当前元素是否为前一个元素的两倍,不等则爆炸。因此答案为1 2 4 8 16 32, 唯一解。

第三关

phase_3的代码如下:

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
0000000000400f43 <phase_3>:
400f43: 48 83 ec 18 sub $0x18,%rsp
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx # 第二个整数位于$rsp + 12
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx # 第一个整数位于%rsp + 8
400f51: be cf 25 40 00 mov $0x4025cf,%esi # 查看0x4025cf地址处内存,为"%d %d",表示接受两个整数作为输入
400f56: b8 00 00 00 00 mov $0x0,%eax
400f5b: e8 90 fc ff ff callq 400bf0 <__isoc99_sscanf@plt>
400f60: 83 f8 01 cmp $0x1,%eax # sscanf返回值需大于1,否则爆炸。说明可变参数个数为2
400f63: 7f 05 jg 400f6a <phase_3+0x27>
400f65: e8 d0 04 00 00 callq 40143a <explode_bomb>
400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp) # 第一个数必须小于7,否则爆炸
400f6f: 77 3c ja 400fad <phase_3+0x6a># 比较结果大于0则跳转
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax
400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8) # 跳转表结构,对应C语言中的switch语句
400f7c: b8 cf 00 00 00 mov $0xcf,%eax # %rax = 0,跳转到400f7c
400f81: eb 3b jmp 400fbe <phase_3+0x7b>
400f83: b8 c3 02 00 00 mov $0x2c3,%eax
400f88: eb 34 jmp 400fbe <phase_3+0x7b>
400f8a: b8 00 01 00 00 mov $0x100,%eax
400f8f: eb 2d jmp 400fbe <phase_3+0x7b>
400f91: b8 85 01 00 00 mov $0x185,%eax
400f96: eb 26 jmp 400fbe <phase_3+0x7b>
400f98: b8 ce 00 00 00 mov $0xce,%eax
400f9d: eb 1f jmp 400fbe <phase_3+0x7b>
400f9f: b8 aa 02 00 00 mov $0x2aa,%eax
400fa4: eb 18 jmp 400fbe <phase_3+0x7b>
400fa6: b8 47 01 00 00 mov $0x147,%eax
400fab: eb 11 jmp 400fbe <phase_3+0x7b>
400fad: e8 88 04 00 00 callq 40143a <explode_bomb>
400fb2: b8 00 00 00 00 mov $0x0,%eax
400fb7: eb 05 jmp 400fbe <phase_3+0x7b>
400fb9: b8 37 01 00 00 mov $0x137,%eax
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax # 判断%eax值与第二个参数是否相等,不等则爆炸
400fc2: 74 05 je 400fc9 <phase_3+0x86>
400fc4: e8 71 04 00 00 callq 40143a <explode_bomb>
400fc9: 48 83 c4 18 add $0x18,%rsp
400fcd: c3 retq

与第二关类似,查看0x400f51处代码mov $0x4025cf $esi , 用gdb打印0x4025cf处内存,如下:

1
2
(gdb) x/s 0x4025cf
0x4025cf: "%d %d"

内容为"%d %d",表示这一关需要输入两个整数。

400f47 ~ 400f4c代码可知,第一个整数位于$rsp + 8地址,第二个整数位于$rsp + 12地址

确认这两个整数应满足的条件

观察0x400f6a处的cmp指令。注意比较顺序,是计算*(%rsp + 8) - 7 的值,再判断这个值是否大于0

1
2
400f6a:   83 7c 24 08 07          cmpl   $0x7,0x8(%rsp)		# 第一个数必须小于7,否则爆炸
400f6f: 77 3c ja 400fad # 引爆炸弹

以上两句汇编等同于 if (*rsp+8) > 7, 跳转到0x400fad, 因此第一个数必须不大于7。

0x400f75jmpq *0x402470(,%rax,8)是一个间接跳转指令, 可以看出这段代码是典型的switch语句,跳转表就存在于0x402470%rax取值为[0, 7],代表switch语句中8条不同的case。 打印这张跳转表:

1
2
3
4
5
(gdb) x/8g 0x402470
0x402470: 0x0000000000400f7c 0x0000000000400fb9
0x402480: 0x0000000000400f83 0x0000000000400f8a
0x402490: 0x0000000000400f91 0x0000000000400f98
0x4024a0: 0x0000000000400f9f 0x0000000000400fa6

举例,第一个整数取0时,会跳转到0x400f7c, 将0xcf赋给%rax0x400fbe处再判断$rax和第二个整数是否相等。因此0 207为满足条件的一组解。依次类推,一共得到8组解,答案不唯一,任选一种即可:

1
2
3
4
5
6
7
8
0 207
1 311
2 707
3 256
4 389
5 206
6 682
7 327

switch语句和跳转表内容可参考 《CSAPP 原书第3版》 3.6.8小节 —— switch语句。

第四关

phase_4的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
000000000040100c <phase_4>:
40100c: 48 83 ec 18 sub $0x18,%rsp
401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx # 第二个整数,用$rcx保存
401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx # 第一个整数,用%rdx保存
40101a: be cf 25 40 00 mov $0x4025cf,%esi # %rsi处字符串: "%d %d"
40101f: b8 00 00 00 00 mov $0x0,%eax
401024: e8 c7 fb ff ff callq 400bf0 <__isoc99_sscanf@plt>
401029: 83 f8 02 cmp $0x2,%eax
40102c: 75 07 jne 401035 <phase_4+0x29>
40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp) # 将第一个整数和14比较
401033: 76 05 jbe 40103a <phase_4+0x2e># 如果不大于14跳转,否则引爆炸弹
401035: e8 00 04 00 00 callq 40143a <explode_bomb>
40103a: ba 0e 00 00 00 mov $0xe,%edx # func4函数的第一个入参,初值为14
40103f: be 00 00 00 00 mov $0x0,%esi # func4函数的第二个入参,初值为0
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi # func4函数的第三个入参,初值为输入的第一个整数
401048: e8 81 ff ff ff callq 400fce <func4> # 调用func4, func4为递归函数
40104d: 85 c0 test %eax,%eax # func4函数必须返回0,否则爆炸
40104f: 75 07 jne 401058 <phase_4+0x4c>
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp) # 第二个整数必须为0, 否则爆炸
401056: 74 05 je 40105d <phase_4+0x51>
401058: e8 dd 03 00 00 callq 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp
401061: c3 retq

同样的,先确认输入字符串的格式,查看0x4025cf处的格式化字符串

1
2
(gdb) x/s 0x4025cf
0x4025cf: "%d %d"

内容为"%d %d", 说明需要输入两个整数。第一个整数位于%rsp + 8, 第二个整数位于%rsp + 12

0x40102e ~ 0x401033代码可知,第一个整数必须不大于14, 否则引爆炸弹。

注意到0x401048处调用func4函数,并判断该函数返回值是否为0,不等于0则引爆炸弹。

0x40103a ~ 0x401044三条语句可知,func4函数接受三个入参,且三个参数的初始值从左到右分别为输入的第一个整数,14, 0。下面查看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
# int func4(int x, int y, int z);
# x in %edi, y in $esi, z in $edx, ret in $eax
0000000000400fce <func4>:
400fce: 48 83 ec 08 sub $0x8,%rsp
400fd2: 89 d0 mov %edx,%eax # ret = z
400fd4: 29 f0 sub %esi,%eax # ret -= y
400fd6: 89 c1 mov %eax,%ecx # ecx = ret
400fd8: c1 e9 1f shr $0x1f,%ecx # ecx = (ecx >> 31) & 0x1
400fdb: 01 c8 add %ecx,%eax # ret += ecx
400fdd: d1 f8 sar %eax # ret >>= 1
400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx # ecx = ret + y
400fe2: 39 f9 cmp %edi,%ecx
400fe4: 7e 0c jle 400ff2 <func4+0x24> # if ecx <= x, jump to 0x400ff2
400fe6: 8d 51 ff lea -0x1(%rcx),%edx # z = rcx - 1
400fe9: e8 e0 ff ff ff callq 400fce <func4>
400fee: 01 c0 add %eax,%eax # ret *= 2
400ff0: eb 15 jmp 401007 <func4+0x39>
400ff2: b8 00 00 00 00 mov $0x0,%eax # ret = 0
400ff7: 39 f9 cmp %edi,%ecx
400ff9: 7d 0c jge 401007 <func4+0x39> # if ecx >= x, jump to 0x401007
400ffb: 8d 71 01 lea 0x1(%rcx),%esi # y = ecx + 1
400ffe: e8 cb ff ff ff callq 400fce <func4> # ret = func(x, y, z)
401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax# ret = 2 * ret + 1
401007: 48 83 c4 08 add $0x8,%rsp
40100b: c3 retq

400fe9400ffe处的callq 400fce <func4>指令可知发生了递归调用。我们可以将func4的汇编代码逐句翻译成C语言,将第一个整数从0取到14依次调用func4函数,看哪些取值能成功返回0。这里需要了解add, sub, sar, shr,lea, jle等指令的用法以及注意操作数的顺序。

func4的递归过程,可以转换为如下的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
// x in %edi, y in $esi, z in $edx, ret in %eax
int func4(int x, int y, int z) {
int ecx;
int ret = z - y;

if(z < y) {
ret += 1;
}
ret >>= 1;
ecx = ret + y;

if(ecx == x) {
return 0;
} else if (ecx <= x) {
return 2 * func4(x, ecx + 1, z) + 1;
} else {
return 2 * func4(x, y, ecx - 1);
}
return ret;
}
int main() {
for(int i = 0; i <= 14; ++i)
if(func4(i, 0, 14) == 0)
printf("answer: %d\n", i); // 打印出第一个整数的所有取值
return 0;
}

编译执行C程序,发现第一个整数可以是0137

phase_40x401051 ~ 0x401056代码可知,第二个整数必须为0,否则引爆炸弹。

因此,一共得到四组解,答案不唯一,任选一种即可:

1
2
3
4
0 0
1 0
3 0
7 0

第五关

phase_5的代码如下, 根据0x40107f处的cmp $0x6, %eax指令,可确定这关需要输入长度为6的字符串

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
0000000000401062 <phase_5>:
401062: 53 push %rbx
401063: 48 83 ec 20 sub $0x20,%rsp
401067: 48 89 fb mov %rdi,%rbx # rbx保存输入字符串
40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
401071: 00 00
401073: 48 89 44 24 18 mov %rax,0x18(%rsp)
401078: 31 c0 xor %eax,%eax
40107a: e8 9c 02 00 00 callq 40131b <string_length>
40107f: 83 f8 06 cmp $0x6,%eax # 输入字符串的长度必须为6,否则爆炸
401082: 74 4e je 4010d2 <phase_5+0x70>
401084: e8 b1 03 00 00 callq 40143a <explode_bomb>
401089: eb 47 jmp 4010d2 <phase_5+0x70>
40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx
40108f: 88 0c 24 mov %cl,(%rsp)
401092: 48 8b 14 24 mov (%rsp),%rdx
401096: 83 e2 0f and $0xf,%edx # 将当前字符与上0xf,结果保存在%edx
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx # 将(0x4024b0+%edx)处的字符保存在%rdx
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1)
4010a4: 48 83 c0 01 add $0x1,%rax # 每次循环%rax加1
4010a8: 48 83 f8 06 cmp $0x6,%rax # 用%rax循环计数,循环6次
4010ac: 75 dd jne 40108b <phase_5+0x29>
4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp)
4010b3: be 5e 24 40 00 mov $0x40245e,%esi # 0x40245e处字符串为flyers
4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi # 这里需要构造输入串,使得(%rsp+0x10)处的串等于"flyers"
4010bd: e8 76 02 00 00 callq 401338 <strings_not_equal>
4010c2: 85 c0 test %eax,%eax
4010c4: 74 13 je 4010d9 <phase_5+0x77>
4010c6: e8 6f 03 00 00 callq 40143a <explode_bomb>
4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
4010d0: eb 07 jmp 4010d9 <phase_5+0x77>
4010d2: b8 00 00 00 00 mov $0x0,%eax # 循环开始,eax初值为0
4010d7: eb b2 jmp 40108b <phase_5+0x29>
4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax
4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
4010e5: 00 00
4010e7: 74 05 je 4010ee <phase_5+0x8c>
4010e9: e8 42 fa ff ff callq 400b30 <__stack_chk_fail@plt>
4010ee: 48 83 c4 20 add $0x20,%rsp
4010f2: 5b pop %rbx
4010f3: c3 retq

根据jmp 40108b <phase_5+0x29>,看出这段代码是循环。%eax初始为0, 每次循环将%eax加1,再和6进行比较(0x4010a8)。循环结束后调用strings_not_equal,将0x40245e处的字符串和$rsp + 0x10比较,两个字符串必须相等,否则爆炸。先查看0x40245e处的内容:

1
2
(gdb) x/s 0x40245e
0x40245e: "flyers"

内容为flyers, 再看看$rsp + 0x10处的字符串是怎么来的:

0x401067: mov %rdi,%rbx看出,我们输入的字符串位于%rbx。这里依次将%rbx的每一个字符先与0xf做与运算,然后加上0x4024b0得到新的地址x, 最后取地址x处的字符作为输出;循环结束后,输出一个长度为6的字符串,将以上逻辑改写为如下C代码:

1
2
3
4
5
6
7
int phase_5(char str[6]) {	// str表示用户输入的字符串
char res[6];
for(int i = 0; i < 6; ++i) {
res[i] = *(char *)((str[i] & 0xf) + 0x4024b0);
}
return !strcmp(res[i], "flyers");
}

因此,我们只需确定0x4024b0处的内容,然后对照ASCII码表,即可得到答案。

先查看0x4024b0, 只需查看前16个字符

1
2
3
(gdb) x/16c 0x4024b0
0x4024b0 <array.3449> : 109 'm' 97 'a' 100 'd' 117 'u' 105 'i' 101 'e' 114 'r' 115 's'
0x4024b8 <array.3449+8>: 110 'n' 102 'f' 111 'o' 116 't' 118 'v' 98 'b' 121 'y' 108 'l'

发现flyers中的每个字符都可以找到。根据偏移确定输入的每个字符的ASCII码最低一个字节依次为0x9, 0xF, 0xE, 0x5, 0x6, 0x7, 答案不唯一。对照ASCII码表 http://ascii.911cha.com/, 我们找到一组解:IONUVW

二进制 十进制 十六进制 图形
0100 1001 73 49 I
0100 1111 79 4F O
0100 1110 78 4E N
0101 0101 85 55 U
0101 0110 86 56 V
0101 0111 87 57 W

第六关

phase_6的代码如下,非常的长

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
00000000004010f4 <phase_6>:
4010f4: 41 56 push %r14
4010f6: 41 55 push %r13
4010f8: 41 54 push %r12
4010fa: 55 push %rbp
4010fb: 53 push %rbx
4010fc: 48 83 ec 50 sub $0x50,%rsp
401100: 49 89 e5 mov %rsp,%r13
401103: 48 89 e6 mov %rsp,%rsi
401106: e8 51 03 00 00 callq 40145c <read_six_numbers> #读6个数,保存到$rsp
# 步骤1:判断输入的每个数是否不超过6,且任意两个数都不相等
40110b: 49 89 e6 mov %rsp,%r14
40110e: 41 bc 00 00 00 00 mov $0x0,%r12d # %r12d = 0
401114: 4c 89 ed mov %r13,%rbp # 初始$rbp, %r13都指向第一个数
401117: 41 8b 45 00 mov 0x0(%r13),%eax
40111b: 83 e8 01 sub $0x1,%eax
40111e: 83 f8 05 cmp $0x5,%eax # 每个数必须小于等于6,否则爆炸
401121: 76 05 jbe 401128 <phase_6+0x34>
401123: e8 12 03 00 00 callq 40143a <explode_bomb>
401128: 41 83 c4 01 add $0x1,%r12d # %12d循环计数,每次加1
40112c: 41 83 fc 06 cmp $0x6,%r12d # 第一重循环,终止条件是%r12d等于6
401130: 74 21 je 401153 <phase_6+0x5f>
401132: 44 89 e3 mov %r12d,%ebx # %ebx <- %r12d
401135: 48 63 c3 movslq %ebx,%rax
401138: 8b 04 84 mov (%rsp,%rax,4),%eax # 将输入的下一个整数保存到%eax
40113b: 39 45 00 cmp %eax,0x0(%rbp) # 第二重循环,当前整数不能和它后面的任意一个数重复,否则爆炸; 两重循环用于确保输入的6个数没有重复数字,否则引爆炸弹
40113e: 75 05 jne 401145 <phase_6+0x51>
401140: e8 f5 02 00 00 callq 40143a <explode_bomb>
401145: 83 c3 01 add $0x1,%ebx
401148: 83 fb 05 cmp $0x5,%ebx
40114b: 7e e8 jle 401135 <phase_6+0x41>
40114d: 49 83 c5 04 add $0x4,%r13
401151: eb c1 jmp 401114 <phase_6+0x20>
# 步骤2:对于每个输入的整数,做这样的转换:用7减去这个整数的值替换原来的数
401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi
401158: 4c 89 f0 mov %r14,%rax
40115b: b9 07 00 00 00 mov $0x7,%ecx # %ecx初值为7
401160: 89 ca mov %ecx,%edx
401162: 2b 10 sub (%rax),%edx # %edx = 7 - (%rax), %rax指向当前整数
401164: 89 10 mov %edx,(%rax) # (%rax) = %edx
401166: 48 83 c0 04 add $0x4,%rax # 循环每执行一次, %rax指向下一个整数
40116a: 48 39 f0 cmp %rsi,%rax
40116d: 75 f1 jne 401160 <phase_6+0x6c>
# 步骤3:0x6032d0处表示一个包含6个节点的链表, 对于经过步骤2之后转换的每个整数i, 取链表第i个节点的value,依次保存在(%rsp + 32)处
40116f: be 00 00 00 00 mov $0x0,%esi # esi设为0
401174: eb 21 jmp 401197 <phase_6+0xa3>
401176: 48 8b 52 08 mov 0x8(%rdx),%rdx # 访问链表
40117a: 83 c0 01 add $0x1,%eax
40117d: 39 c8 cmp %ecx,%eax
40117f: 75 f5 jne 401176 <phase_6+0x82>
401181: eb 05 jmp 401188 <phase_6+0x94>
401183: ba d0 32 60 00 mov $0x6032d0,%edx # 0x6032d0处为链表,包含6个节点
401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2) #每次取链表中第%ecx个节点的值,保存到$rsp + 0x20 + 2 * $rsi处, %ecx表示每个
40118d: 48 83 c6 04 add $0x4,%rsi
401191: 48 83 fe 18 cmp $0x18,%rsi
401195: 74 14 je 4011ab <phase_6+0xb7>
401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx # ecx初始值为%rsp, 指向第一个数
40119a: 83 f9 01 cmp $0x1,%ecx
40119d: 7e e4 jle 401183 <phase_6+0x8f>
40119f: b8 01 00 00 00 mov $0x1,%eax
4011a4: ba d0 32 60 00 mov $0x6032d0,%edx # 0x6032d0处为链表,包含6个节点
4011a9: eb cb jmp 401176 <phase_6+0x82>
4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx # 保存6个节点的值
4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax
4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi
4011ba: 48 89 d9 mov %rbx,%rcx
4011bd: 48 8b 10 mov (%rax),%rdx
4011c0: 48 89 51 08 mov %rdx,0x8(%rcx)
4011c4: 48 83 c0 08 add $0x8,%rax
4011c8: 48 39 f0 cmp %rsi,%rax
4011cb: 74 05 je 4011d2 <phase_6+0xde>
4011cd: 48 89 d1 mov %rdx,%rcx
4011d0: eb eb jmp 4011bd <phase_6+0xc9>
# 4.判断(%rsp + 32)处的6个整数是否为降序排列,如不满足条件引爆炸弹
4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx)
4011d9: 00
4011da: bd 05 00 00 00 mov $0x5,%ebp
4011df: 48 8b 43 08 mov 0x8(%rbx),%rax #将链表下一个节点地址给rax
4011e3: 8b 00 mov (%rax),%eax #eax为链表下一个节点的值
4011e5: 39 03 cmp %eax,(%rbx) # 比较前后两个节点的值
4011e7: 7d 05 jge 4011ee <phase_6+0xfa>#前一个数要大于后一个,否则炸弹爆炸。即必须为降序排列
4011e9: e8 4c 02 00 00 callq 40143a <explode_bomb>
4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx
4011f2: 83 ed 01 sub $0x1,%ebp
4011f5: 75 e8 jne 4011df <phase_6+0xeb>
4011f7: 48 83 c4 50 add $0x50,%rsp
4011fb: 5b pop %rbx
4011fc: 5d pop %rbp
4011fd: 41 5c pop %r12
4011ff: 41 5d pop %r13
401201: 41 5e pop %r14
401203: c3 retq

40111b ~ 40111e: 将每个输入的整数和6比较,如存在某个数大于6,引爆炸弹。

40110b ~ 401153: 双重循环,用于判断输入的6个数字中是否存在两个数相同。如果存在,引爆炸弹。

举例:用户可以输入1,2,3,4,5,6,记为序列0,满足以上两个条件。

401160 ~ 40116d: 一重循环,对于序列0中的每个整数,做这样的转换:用7减去这个整数的结果替换原来的数,即得到序列16,5,4,3,2,1

注意到%edx初值为0x6032d0, 打印这块内存,发现这是一条链表, 包含6个节点。

1
2
3
4
5
6
7
(gdb) x/24x 0x6032d0
0x6032d0 <node1>: 0x0000014c 0x00000001 0x006032e0 0x00000000
0x6032e0 <node2>: 0x000000a8 0x00000002 0x006032f0 0x00000000
0x6032f0 <node3>: 0x0000039c 0x00000003 0x00603300 0x00000000
0x603300 <node4>: 0x000002b3 0x00000004 0x00603310 0x00000000
0x603310 <node5>: 0x000001dd 0x00000005 0x00603320 0x00000000
0x603320 <node6>: 0x000001bb 0x00000006 0x00000000 0x00000000

40116f ~ 4011d0:遍历转换后的序列1,对于每个整数i, 取第inode节点的值,依次存储到%rsp + 32处,本例中存储到%rsp + 32的6个数为0x1bb,0x1dd,0x2b3, 0x39c, 0xa8, 0x14c, 记为序列2

4011d2 ~ 4011f5:判断序列2是否为降序排列,本例中的序列2不满足条件。因此我们需要回过头,调整输入的6个整数的顺序,使得序列2为降序排列,过程如下:

  • 链表中6个节点降序排列应为: 0x39c,0x2b3,0x1dd,0x1bb,0x14c, 0xa8

  • 对应的6个节点序列为:node3,node4,node5,node6,node1,node2

  • 推导出序列1: 3,4,5,6,1,2

  • 根据序列1逆推出输入:7-3, 7-4, 7-5, 7-6, 7-1, 7-2 -> 4, 3, 5, 6, 1, 2

最终得到这一关的答案为4,3,5,6,1,2, 唯一解。

隐藏关

在汇编文件中搜secret_phase,发现phase_defused调用了它。先看看如何触发隐藏关,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
32
33
34
35
36
37
00000000004015c4 <phase_defused>:
4015c4: 48 83 ec 78 sub $0x78,%rsp
4015c8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
4015cf: 00 00
4015d1: 48 89 44 24 68 mov %rax,0x68(%rsp)
4015d6: 31 c0 xor %eax,%eax
4015d8: 83 3d 81 21 20 00 06 cmpl $0x6,0x202181(%rip) # 603760 <num_input_strings> 仅当第6关通过后,不进行跳转,进入隐藏关
4015df: 75 5e jne 40163f <phase_defused+0x7b>
4015e1: 4c 8d 44 24 10 lea 0x10(%rsp),%r8
4015e6: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
4015eb: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
4015f0: be 19 26 40 00 mov $0x402619,%esi # 格式为"%d %d %s"
4015f5: bf 70 38 60 00 mov $0x603870,%edi # 0x603870处保存第四关输入的答案, 可通过对phase_defused打断点,或者对0x603870打数据断点确认
4015fa: e8 f1 f5 ff ff callq 400bf0 <__isoc99_sscanf@plt>
4015ff: 83 f8 03 cmp $0x3,%eax # 需要输入3个参数,才能触发隐藏关,否则跳转0x401635
401602: 75 31 jne 401635 <phase_defused+0x71>
401604: be 22 26 40 00 mov $0x402622,%esi # %esi处字符串"DrEvil"
401609: 48 8d 7c 24 10 lea 0xls
40160e: e8 25 fd ff ff callq 401338 <strings_not_equal>
401613: 85 c0 test %eax,%eax
401615: 75 1e jne 401635 <phase_defused+0x71>
401617: bf f8 24 40 00 mov $0x4024f8,%edi
40161c: e8 ef f4 ff ff callq 400b10 <puts@plt>
401621: bf 20 25 40 00 mov $0x402520,%edi
401626: e8 e5 f4 ff ff callq 400b10 <puts@plt>
40162b: b8 00 00 00 00 mov $0x0,%eax
401630: e8 0d fc ff ff callq 401242 <secret_phase>
401635: bf 58 25 40 00 mov $0x402558,%edi
40163a: e8 d1 f4 ff ff callq 400b10 <puts@plt>
40163f: 48 8b 44 24 68 mov 0x68(%rsp),%rax
401644: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
40164b: 00 00
40164d: 74 05 je 401654 <phase_defused+0x90>
40164f: e8 dc f4 ff ff callq 400b30 <__stack_chk_fail@plt>
401654: 48 83 c4 78 add $0x78,%rsp
401658: c3 retq

首先查看4015ff: cmp $0x3,%eax, 说明sscanf需接受3个变参才能触发隐藏关。参数格式依次为%d, %d, %s

1
2
(gdb) x/s 0x402619
0x402619: "%d %d %s"

接下来确定0x603870处字符串怎么来的。在0x603870处设置gdb数据断点, 发现0x603870处内容依次变为7, 7 0,然后程序退出。而7 0恰好是我们第四关输入的答案。 说明我们只需在第四关后添加一个合适的字符串,作为第3个参数,即可触发隐藏关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(gdb) watch *0x603870							# 设置内存断点
Hardware watchpoint 5: *0x603870
(gdb) r
...
Hardware watchpoint 5: *0x603870
Old value = 0
New value = 55
0x00007ffff7aa1b53 in __memcpy_sse2 () from /lib64/libc.so.6
(gdb) x/s 0x603870
0x603870 <input_strings+240>: "7"
(gdb) c
Continuing.
Hardware watchpoint 5: *0x603870
Old value = 55
New value = 3153975
0x00007ffff7aa1b64 in __memcpy_sse2 () from /lib64/libc.so.6
(gdb) x/s 0x603870
0x603870 <input_strings+240>: "7 0"

根据401604: mov $0x402622,%esi, 确认输入的字符串为"DrEvil"

1
2
(gdb) x/s 0x402622
0x402622: "DrEvil"

因此,只需要将第四关答案改为7 0 DrEvil, 即可触发隐藏关。

查看隐藏关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0000000000401242 <secret_phase>:
401242: 53 push %rbx
401243: e8 56 02 00 00 callq 40149e <read_line>
401248: ba 0a 00 00 00 mov $0xa,%edx # strtol的第三个参数,base等于10
40124d: be 00 00 00 00 mov $0x0,%esi # strtol的第二个参数, endptr='\0'
401252: 48 89 c7 mov %rax,%rdi # strtol的第一个参数,str=用户输入字符串
401255: e8 76 f9 ff ff callq 400bd0 <strtol@plt>
40125a: 48 89 c3 mov %rax,%rbx # 将用户输入的整数保存到%rbx
40125d: 8d 40 ff lea -0x1(%rax),%eax # %eax = %rax - 1
401260: 3d e8 03 00 00 cmp $0x3e8,%eax # 0x3e8 = 1000
401265: 76 05 jbe 40126c <secret_phase+0x2a>
401267: e8 ce 01 00 00 callq 40143a <explode_bomb>
40126c: 89 de mov %ebx,%esi # fun7的第二个参数, %rbx
40126e: bf f0 30 60 00 mov $0x6030f0,%edi # fun7
401273: e8 8c ff ff ff callq 401204 <fun7>
401278: 83 f8 02 cmp $0x2,%eax # fun7的返回值必须为2,否则引爆
40127b: 74 05 je 401282 <secret_phase+0x40>
40127d: e8 b8 01 00 00 callq 40143a <explode_bomb>
401282: bf 38 24 40 00 mov $0x402438,%edi
401287: e8 84 f8 ff ff callq 400b10 <puts@plt>
40128c: e8 33 03 00 00 callq 4015c4 <phase_defused>
401291: 5b pop %rbx

0x401255处调用了strtol函数,函数声明如下:

1
long int strtol(const char *str, char **endptr, int base);

查看0x401248 ~ 0x401267,发现最后一关需要输入一个整数,并且该整数不能超过1001。

查看0x40126c ~ 0x40127b, 发现调用了fun7函数,且该函数返回值必须为2。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
28
29
30
31
(gdb) x/120x 0x6030f0
0x6030f0 <n1>: 0x00000024 0x00000000 0x00603110 0x00000000
0x603100 <n1+16>: 0x00603130 0x00000000 0x00000000 0x00000000
0x603110 <n21>: 0x00000008 0x00000000 0x00603190 0x00000000
0x603120 <n21+16>: 0x00603150 0x00000000 0x00000000 0x00000000
0x603130 <n22>: 0x00000032 0x00000000 0x00603170 0x00000000
0x603140 <n22+16>: 0x006031b0 0x00000000 0x00000000 0x00000000
0x603150 <n32>: 0x00000016 0x00000000 0x00603270 0x00000000
0x603160 <n32+16>: 0x00603230 0x00000000 0x00000000 0x00000000
0x603170 <n33>: 0x0000002d 0x00000000 0x006031d0 0x00000000
0x603180 <n33+16>: 0x00603290 0x00000000 0x00000000 0x00000000
0x603190 <n31>: 0x00000006 0x00000000 0x006031f0 0x00000000
0x6031a0 <n31+16>: 0x00603250 0x00000000 0x00000000 0x00000000
0x6031b0 <n34>: 0x0000006b 0x00000000 0x00603210 0x00000000
0x6031c0 <n34+16>: 0x006032b0 0x00000000 0x00000000 0x00000000
0x6031d0 <n45>: 0x00000028 0x00000000 0x00000000 0x00000000
0x6031e0 <n45+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x6031f0 <n41>: 0x00000001 0x00000000 0x00000000 0x00000000
0x603200 <n41+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603210 <n47>: 0x00000063 0x00000000 0x00000000 0x00000000
0x603220 <n47+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603230 <n44>: 0x00000023 0x00000000 0x00000000 0x00000000
0x603240 <n44+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603250 <n42>: 0x00000007 0x00000000 0x00000000 0x00000000
0x603260 <n42+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603270 <n43>: 0x00000014 0x00000000 0x00000000 0x00000000
0x603280 <n43+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x603290 <n46>: 0x0000002f 0x00000000 0x00000000 0x00000000
0x6032a0 <n46+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x6032b0 <n48>: 0x000003e9 0x00000000 0x00000000 0x00000000
0x6032c0 <n48+16>: 0x00000000 0x00000000 0x00000000 0x00000000

把这棵二叉树画出来, 发现有4层,共15个节点:

1
2
3
4
5
6
7
                                        0x24
/ \
0x8 0x32
/ \ / \
0x6 0x16 0x2d 0x6b
/ \ / \ / \ / \
0x1 0x7 0x14 0x23 0x28 0x2f 0x63 0x3e9

fun7代码如下,可以看出这是个递归函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0000000000401204 <fun7>:
401204: 48 83 ec 08 sub $0x8,%rsp
401208: 48 85 ff test %rdi,%rdi
40120b: 74 2b je 401238 <fun7+0x34>
40120d: 8b 17 mov (%rdi),%edx # 首次调用fun7时, rdi指向二叉树的根节点
40120f: 39 f2 cmp %esi,%edx # 如当前节点值小于等于用户输入的数,跳转到0x401220
401211: 7e 0d jle 401220 <fun7+0x1c>
401213: 48 8b 7f 08 mov 0x8(%rdi),%rdi # 取当前节点的左子女
401217: e8 e8 ff ff ff callq 401204 <fun7>
40121c: 01 c0 add %eax,%eax
40121e: eb 1d jmp 40123d <fun7+0x39>
401220: b8 00 00 00 00 mov $0x0,%eax
401225: 39 f2 cmp %esi,%edx
401227: 74 14 je 40123d <fun7+0x39>
401229: 48 8b 7f 10 mov 0x10(%rdi),%rdi # 取当前节点的右子女
40122d: e8 d2 ff ff ff callq 401204 <fun7>
401232: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax # %eax = 2 * $rax + 1
401236: eb 05 jmp 40123d <fun7+0x39>
401238: b8 ff ff ff ff mov $0xffffffff,%eax # 当前节点为NULL,返回-1
40123d: 48 83 c4 08 add $0x8,%rsp
401241: c3 retq

将递归逻辑转换为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
// 二叉树的数组表示,树高4层, 共15个节点
const int arrTree[] = { 0x24,
0x8, 0x32,
0x6, 0x16, 0x2d, 0x6b,
0x1, 0x7 , 0x14, 0x23, 0x28, 0x2f, 0x63, 0x3e9 };
// idx表示数组arrTree索引,userInput即用户输入的答案
int fun7(int idx, int userInput) {
if(idx < 0 || idx > 14) {
return -1; // 如当前节点为NULL, 返回-1
}
int ret = 0;
if(arrTree[idx] < userInput) {
ret = 2 * fun7(2 * idx + 2, userInput) + 1; // 取当前节点的右子女
} else if(arrTree[idx] > userInput) {
ret = 2 * fun7(2 * idx + 1, userInput); // 取当前节点的左子女
}
return ret;
}

int main() {
for(int i = 0; i < 1002; ++i)
if(fun7(0, i) == 2) // idx初值为0, 表示从二叉树的根开始递归
printf("answer: %d\n", i);
return 0;
}

执行C程序,打印如下,说明答案有两组, 输入2022都可以。

1
2
answer: 20
answer: 22

总结

所有关卡的答案保存在answer文件中,内容如下:

1
2
3
4
5
6
7
8
# cat answer
Border relations with Canada have never been better.
1 2 4 8 16 32
6 682
7 0 DrEvil
IONUVW
4 3 2 1 6 5
20

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
# ./bomb answer
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2. Keep going!
Halfway there!
So you got that one. Try this one.
Good work! On to the next...
Curses, you've found the secret phase!
But finding it and solving it are quite different...
Wow! You've defused the secret stage!
Congratulations! You've defused the bomb!

参考资料

《深入理解计算机系统 原书第3版》