Lab 2: Bomb Lab

二进制炸弹 Bomb 是一个由多个阶段组成的程序,每个阶段会要求用户在 stdin 输入特定字符串。若输入正确,该阶段即被拆除并进入下一阶段;否则程序将输出"BOOM! ! ! "后终止(即炸弹爆炸)。只有当所有阶段均被拆除后,炸弹才算彻底解除。

Writeup文件有以下的指南:

Note

工具指南

  • gdb
    GNU调试器,支持单步跟踪/内存检查/断点设置等功能。推荐使用CS:APP官网提供的单页速查表:
    http://csapp.cs.cmu.edu/public/students.html

    • 设置断点可防止错误输入导致的爆炸
    • 通过help/man gdb/info gdb获取帮助文档
  • objdump

    • objdump -t:查看炸弹符号表(含函数/全局变量地址)
    • objdump -d:反汇编全部代码(注意系统调用会显示为晦涩格式,需结合gdb分析)
  • strings
    提取炸弹中的可打印字符串

    (AI翻译自Writeup文件


Solution

我们所需要做的就是通过反汇编找出六组密码,考虑到在线版的炸弹每一次引爆都会扣除分数,当然是 追求无伤clear什么的

Tip

为了锻炼个人阅读 x86-64 汇编代码的能力,我采用的拆弹方案是直接分析反汇编代码,因此下面的操作将很少使用 gdb 工具

在进行对 bomb 的反编译之前,可以看一看旁边的 bomb.c 文件(这是一个提示作用的文件,展示了原程序 <main> 函数的流程),这个C程序告诉我们 bomb 的运行方式,比如:

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");

另外最后的注释说明也暗示了总共不只有六个 bomb 需要拆除:

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

❯ objdump bomb -d -j .text 开始拆弹(反编译代码段),可以看到很多标记点,而我们目前需要关注的只有 <main> 部分和 <phase> 部分。其中 main 部分从 bomb.c 文件中就能理解意思(初始化炸弹,检测输入判断炸弹是否爆炸等等),所以直接看六个阶段的汇编代码即可:

(有趣的是有一个 <secret_phase> 标记点,这对应了 bomb.c 中暗示的隐藏炸弹)

(题解中没有明显区分诸如 %rbx(%rbx) 的写法,可能有一定混淆)

网页对 Tab 的渲染不是很好,在 Github Issues 页面或许会有更好的阅读体验


Phase 1

000000000400ee0 <phase_1>:
  400ee0:	48 83 ec 08          	sub    $0x8,%rsp					# 栈上分配 8byte 的空间(起对齐作用)
  400ee4:	be 00 24 40 00       	mov    $0x402400,%esi				# 将 0x402400 这个地址加载到 %esi
  400ee9:	e8 4a 04 00 00       	call   401338 <strings_not_equal>	# 调用执行 strings_not_equal 函数
  400eee:	85 c0                	test   %eax,%eax					# 计算 %eax & %eax,不保存结果,目的是改变标志位
  400ef0:	74 05                	je     400ef7 <phase_1+0x17>		# 当标志位 ZF==1 时跳转到 400ef7 (表示拆弹成功)
  400ef2:	e8 43 05 00 00       	call   40143a <explode_bomb>		# BOOM!!!
  400ef7:	48 83 c4 08          	add    $0x8,%rsp					# 释放栈空间
  400efb:	c3                   	ret									# 结束子程序
  
0000000000401338 <strings_not_equal>: # 比较 %rdi 与 %rsi 指向的字符串是否不相等,不相等则返回 1,反之返回 0,此处反编译代码略

strings_not_equal 比较的是输入 %rdi%rsi 指向的字符串是否不相同,而两个相同的输入可以触发 je 的跳转避免炸弹爆炸

(标志位就是条件码)

400ee4%esi 被赋值为 $0x402400 的字符串,在gdb环境中对这个地址查看存储的字符串内容:

❯ gdb ./bomb
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04.2) 12.1
(gdb) x/s 0x402400
0x402400:	"Border relations with Canada have never been better."

得到了第一个答案:Border relations with Canada have never been better.

Note

gdb 的 x 命令(eXamine memory)用于查看内存中的数据

x/[N][F][U] <address>
  • N:显示的单位数量(可选,默认为1)。
  • F:显示格式(如十六进制、十进制、字符串等)。
  • U:单位大小(如字节、字、双字等)。
  • <address>:内存地址(可以是寄存器、变量名或直接地址)。

Phase 2

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
  400f05:	e8 52 05 00 00       	call   40145c <read_six_numbers>	# 表明密码是六个(整)数
  400f0a:	83 3c 24 01          	cmpl   $0x1,(%rsp) 					# 根据 (%rsp) - 1 改变标志位
  400f0e:	74 20                	je     400f30 <phase_2+0x34> 		# 如果相等就跳转,否则下一步 BOOM!
  400f10:	e8 25 05 00 00       	call   40143a <explode_bomb>		# BOOM!
  400f15:	eb 19                	jmp    400f30 <phase_2+0x34>		# 无条件跳转
  400f17:	8b 43 fc             	mov    -0x4(%rbx),%eax				# 从 400f3a 跳转;将 (%rbx-4) 处数据存入 %eax
  400f1a:	01 c0                	add    %eax,%eax					# %eax *= 2
  400f1c:	39 03                	cmp    %eax,(%rbx)					# 根据(%rbx) - %eax 设置标志位
  400f1e:	74 05                	je     400f25 <phase_2+0x29>		# 如果相等就跳转,否则下一步 BOOM!
  400f20:	e8 15 05 00 00       	call   40143a <explode_bomb> 		# BOOM!
  400f25:	48 83 c3 04          	add    $0x4,%rbx					# 从 400f1e 跳转;%rbx+=4
  400f29:	48 39 eb             	cmp    %rbp,%rbx					# 根据 %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				# 从 400f0e 跳转;%rbx = %rsp+4
  400f35:	48 8d 6c 24 18       	lea    0x18(%rsp),%rbp				# %rbp = %rsp + 0x18
  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                   	ret

000000000040145c <read_six_numbers>: # 读取六个数
... ...
  401480:	be c3 25 40 00       	mov    $0x4025c3,%esi
... ...
# (gdb) x/s 0x4025c3
# 0x4025c3:	"%d %d %d %d %d %d"
# 说明读取的是整数,数字之间用空格分隔

首先看最开始的这一部分:

  400f0a:	83 3c 24 01          	cmpl   $0x1,(%rsp) 					# 根据 (%rsp) - 1 改变标志位
  400f0e:	74 20                	je     400f30 <phase_2+0x34> 		# 如果相等就跳转,否则下一步 BOOM!
  400f10:	e8 25 05 00 00       	call   40143a <explode_bomb>		# BOOM!

很明显,第一个数字要输入 1 ,否则炸弹爆炸

然后看这一部分

  400f30:	48 8d 5c 24 04       	lea    0x4(%rsp),%rbx				# 从 400f0e 跳转;%rbx = %rsp+4
  400f35:	48 8d 6c 24 18       	lea    0x18(%rsp),%rbp				# %rbp = %rsp + 0x18
  400f3a:	eb db                	jmp    400f17 <phase_2+0x1b>		# 无条件跳转

这里程序设定了 %rbx 存储第二个数字的地址%rbp 为第六个数字之后的地址0x18 = 24 = 6 int),接着跳转到一个循环部分

结合后面的程序可以看得出来,这段代码可以类比 for (int i = 1; i < 6; i++) 的前两个参数设置

接着是这一部分,看上去是个有限循环

  400f17:	8b 43 fc             	mov    -0x4(%rbx),%eax				# 从 400f3a 跳转;将 (%rbx-4) 处数据存入 %eax
  400f1a:	01 c0                	add    %eax,%eax					# %eax *= 2
  400f1c:	39 03                	cmp    %eax,(%rbx)					# 根据(%rbx) - %eax 设置标志位
  400f1e:	74 05                	je     400f25 <phase_2+0x29>		# 如果相等就跳转,否则下一步 BOOM!
  400f20:	e8 15 05 00 00       	call   40143a <explode_bomb> 		# BOOM!
  400f25:	48 83 c3 04          	add    $0x4,%rbx					# 从 400f1e 跳转;%rbx+=4
  400f29:	48 39 eb             	cmp    %rbp,%rbx					# 根据 %rbx - %rbp 改变标志位
  400f2c:	75 e9                	jne    400f17 <phase_2+0x1b>		# 如果不相等就跳转回上一次跳转点(一次循环)
  400f2e:	eb 0c                	jmp    400f3c <phase_2+0x40>		# 无条件跳转,表示拆弹成功

这里直接写出对应的 C 语言代码了,其中规定 %rsp 开始的六个数字组成数组 stack[6]%rbx + 4 表示 i++

for (int i = 1; i < 6; i++){
	int EAX = stack[i-1];													// EAX 为 “上一个数字”
    EAX *= 2;															// EAX 为 “上一个数字” 的两倍
    if(stack[i] - EAX == 0){												// ZF == 1 ?
        continue;														// 跳过引爆炸弹的函数
    }
    explode_bomb();
}

由此可见,接下来的五个数字都要求每个数字是上一个数字的两倍,因此第二个答案是 1 2 4 8 16 32

Phase 3

0000000000400f43 <phase_3>:
  400f43:	48 83 ec 18          	sub    $0x18,%rsp					# 开辟栈空间
  400f47:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx				# %rcx = %rsp + 12(地址计算)
  400f4c:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx				# %rdx = %rsp + 8(地址计算)
  400f51:	be cf 25 40 00       	mov    $0x4025cf,%esi				# (gdb) x/s 0x4025cf: "%d %d", 赋值给 %esi
  400f56:	b8 00 00 00 00       	mov    $0x0,%eax					# %eax = 0
  400f5b:	e8 90 fc ff ff       	call   400bf0 <__isoc99_sscanf@plt>	# 调用标准库函数 sscanf,读取的内容参见 0x4025cf
  400f60:	83 f8 01             	cmp    $0x1,%eax					# 比较 %eax ~ 1
  400f63:	7f 05                	jg     400f6a <phase_3+0x27>		# 有符号 “大于” 成立则跳过引爆代码,否则 BOOM!
  400f65:	e8 d0 04 00 00       	call   40143a <explode_bomb>		# BOOM!
  400f6a:	83 7c 24 08 07       	cmpl   $0x7,0x8(%rsp)				# 比较 (%rsp + 8) ~ 7
  400f6f:	77 3c                	ja     400fad <phase_3+0x6a>		# 无符号 “大于” 成立则跳转至引爆代码,否则继续向下运行
  
  400f71:	8b 44 24 08          	mov    0x8(%rsp),%eax				# %eax = %rsp + 8
  400f75:	ff 24 c5 70 24 40 00 	jmp    *0x402470(,%rax,8)			# 这里是间接跳转中的跳转表跳转,是 switch 语句的实现
  400f7c:	b8 cf 00 00 00       	mov    $0xcf,%eax					# %eax = 207
  400f81:	eb 3b                	jmp    400fbe <phase_3+0x7b>
  400f83:	b8 c3 02 00 00       	mov    $0x2c3,%eax					# %eax = 707
  400f88:	eb 34                	jmp    400fbe <phase_3+0x7b>
  400f8a:	b8 00 01 00 00       	mov    $0x100,%eax					# %eax = 256
  400f8f:	eb 2d                	jmp    400fbe <phase_3+0x7b>
  400f91:	b8 85 01 00 00       	mov    $0x185,%eax					# %eax = 389
  400f96:	eb 26                	jmp    400fbe <phase_3+0x7b>
  400f98:	b8 ce 00 00 00       	mov    $0xce,%eax					# %eax = 206
  400f9d:	eb 1f                	jmp    400fbe <phase_3+0x7b>
  400f9f:	b8 aa 02 00 00       	mov    $0x2aa,%eax					# %eax = 682
  400fa4:	eb 18                	jmp    400fbe <phase_3+0x7b>
  400fa6:	b8 47 01 00 00       	mov    $0x147,%eax					# %eax = 327
  400fab:	eb 11                	jmp    400fbe <phase_3+0x7b>
  400fad:	e8 88 04 00 00       	call   40143a <explode_bomb>		# BOOM!
  400fb2:	b8 00 00 00 00       	mov    $0x0,%eax					# %eax = 0
  400fb7:	eb 05                	jmp    400fbe <phase_3+0x7b>
  400fb9:	b8 37 01 00 00       	mov    $0x137,%eax					# %eax = 311
  
  400fbe:	3b 44 24 0c          	cmp    0xc(%rsp),%eax				# 比较 %eax ~ (%rsp + 12)
  400fc2:	74 05                	je     400fc9 <phase_3+0x86>		# 若 “相等” 成立则成功拆弹
  400fc4:	e8 71 04 00 00       	call   40143a <explode_bomb>		# BOOM!
  400fc9:	48 83 c4 18          	add    $0x18,%rsp
  400fcd:	c3                   	ret    

先看第一部分:

  400f43:	48 83 ec 18          	sub    $0x18,%rsp					# 开辟栈空间
  400f47:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx				# %rcx = %rsp + 12
  400f4c:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx				# %rdx = %rsp + 8
  400f51:	be cf 25 40 00       	mov    $0x4025cf,%esi				# (gdb) x/s 0x4025cf: "%d %d", 赋值给 %esi
  400f56:	b8 00 00 00 00       	mov    $0x0,%eax					# %eax = 0
  400f5b:	e8 90 fc ff ff       	call   400bf0 <__isoc99_sscanf@plt>	# 调用标准库函数 sscanf,读取的内容参见 0x4025cf
  400f60:	83 f8 01             	cmp    $0x1,%eax					# 比较 %eax ~ 1
  400f63:	7f 05                	jg     400f6a <phase_3+0x27>		# 有符号 “大于” 成立则跳过引爆代码,否则 BOOM!
  400f65:	e8 d0 04 00 00       	call   40143a <explode_bomb>		# BOOM!
  400f6a:	83 7c 24 08 07       	cmpl   $0x7,0x8(%rsp)				# 比较 (%rsp + 8) ~ 7
  400f6f:	77 3c                	ja     400fad <phase_3+0x6a>		# 无符号 “大于” 成立则跳转至引爆代码,否则继续向下运行

mov $0x4025cf,%esi 这一句可以知道需要读取的是两个整数

Note

sscanf 的函数原型中,第一个参数 %rdi 是输入字符串的指针,第二个参数是格式字符串的指针,这里也就是 0x4025cf 的内容; %rdx%rcx 分别存储第一个、第二个结果的地址(之后的参数依次 r8r9 存储,接着会使用栈传递)

因此两个 lea 语句看似没有作用,实则表示了两个整数的地址

%eax 根据约定被设定为 sscanf 的返回值,400f60 ~ 400f65 的判断跳转说明 “如果 %eax 的值,也就是输入的整数个数没达到 2 个,直接引爆炸弹”

接下来 cmpl $0x7,0x8(%rsp) 及其后面的跳转表明:第一个数(也就是 0x8(%rsp) , 也就是 %rdx 的对应值)必须 <= 7 ,否则炸弹爆炸(因为是无符号比较,所以负数输入都不正确)

总结:需要输入两个数;第一个数不能超过 7

再看第二部分:

  400f71:	8b 44 24 08          	mov    0x8(%rsp),%eax				# %eax = %rsp + 8
  400f75:	ff 24 c5 70 24 40 00 	jmp    *0x402470(,%rax,8)			# 这里是间接跳转中的跳转表跳转,是 switch 语句的实现
  400f7c:	b8 cf 00 00 00       	mov    $0xcf,%eax					# %eax = 207
  400f81:	eb 3b                	jmp    400fbe <phase_3+0x7b>
  400f83:	b8 c3 02 00 00       	mov    $0x2c3,%eax					# %eax = 707
  400f88:	eb 34                	jmp    400fbe <phase_3+0x7b>
  400f8a:	b8 00 01 00 00       	mov    $0x100,%eax					# %eax = 256
  400f8f:	eb 2d                	jmp    400fbe <phase_3+0x7b>
  400f91:	b8 85 01 00 00       	mov    $0x185,%eax					# %eax = 389
  400f96:	eb 26                	jmp    400fbe <phase_3+0x7b>
  400f98:	b8 ce 00 00 00       	mov    $0xce,%eax					# %eax = 206
  400f9d:	eb 1f                	jmp    400fbe <phase_3+0x7b>
  400f9f:	b8 aa 02 00 00       	mov    $0x2aa,%eax					# %eax = 682
  400fa4:	eb 18                	jmp    400fbe <phase_3+0x7b>
  400fa6:	b8 47 01 00 00       	mov    $0x147,%eax					# %eax = 327
  400fab:	eb 11                	jmp    400fbe <phase_3+0x7b>
  400fad:	e8 88 04 00 00       	call   40143a <explode_bomb>		# BOOM!
  400fb2:	b8 00 00 00 00       	mov    $0x0,%eax					# %eax = 0
  400fb7:	eb 05                	jmp    400fbe <phase_3+0x7b>
  400fb9:	b8 37 01 00 00       	mov    $0x137,%eax					# %eax = 311

第一步 mov 0x8(%rsp),%eax 把第一个输入的数字复制到了 %eax

接下来关键的一步 jmp *0x402470(,%rax,8) 是跳转表跳转。使用 gdb x 查看跳转表内容

(gdb) x/8gx 0x402470	# 以双字为单位,显示 8 个连续的十六进制数值(考虑到 %rdx 值 <= 7,显示 8 个值足够)
0x402470:	0x0000000000400f7c	0x0000000000400fb9
0x402480:	0x0000000000400f83	0x0000000000400f8a
0x402490:	0x0000000000400f91	0x0000000000400f98
0x4024a0:	0x0000000000400f9f	0x0000000000400fa6

*0x402470(,%rax,8) 表示内存间接寻址,可以理解为以 0x402470 为基址,加上 %rax64bit 的偏移量,比如 %rax1 时,就跳转到 400fb9

不妨就以输入的第一个数字为 1 为例,此时 %eax 被赋值为 311

最后看第三部分:

  400fbe:	3b 44 24 0c          	cmp    0xc(%rsp),%eax				# 比较 %eax ~ (%rsp + 12)
  400fc2:	74 05                	je     400fc9 <phase_3+0x86>		# 若 “相等” 成立则成功拆弹
  400fc4:	e8 71 04 00 00       	call   40143a <explode_bomb>		# BOOM!
  400fc9:	48 83 c4 18          	add    $0x18,%rsp
  400fcd:	c3                   	ret    

简单来说就是:如果上一部分设定的 %eax 与第二个输入相等就解除炸弹,否则引爆。比如 %rdx 值为 1 时,必须有 %rcx 值为 311 。也就是说跳转表决定了共计 8 个 %rdx 输入对应的唯一的 %rcx

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

在上面的组合中随意选择一项作为答案就可以拆除第三个炸弹,比如 2 707

Phase 4

(减少了注释量)

000000000040100c <phase_4>:
  40100c:	48 83 ec 18          	sub    $0x18,%rsp
  401010:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx
  401015:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx
  40101a:	be cf 25 40 00       	mov    $0x4025cf,%esi
  40101f:	b8 00 00 00 00       	mov    $0x0,%eax
  401024:	e8 c7 fb ff ff       	call   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)
  401033:	76 05                	jbe    40103a <phase_4+0x2e>
  401035:	e8 00 04 00 00       	call   40143a <explode_bomb>
  
  40103a:	ba 0e 00 00 00       	mov    $0xe,%edx
  40103f:	be 00 00 00 00       	mov    $0x0,%esi
  401044:	8b 7c 24 08          	mov    0x8(%rsp),%edi
  401048:	e8 81 ff ff ff       	call   400fce <func4>
  40104d:	85 c0                	test   %eax,%eax
  40104f:	75 07                	jne    401058 <phase_4+0x4c>
  401051:	83 7c 24 0c 00       	cmpl   $0x0,0xc(%rsp)
  401056:	74 05                	je     40105d <phase_4+0x51>
  401058:	e8 dd 03 00 00       	call   40143a <explode_bomb>
  40105d:	48 83 c4 18          	add    $0x18,%rsp
  401061:	c3                   	ret    

先看这部分:

  40100c:	48 83 ec 18          	sub    $0x18,%rsp
  401010:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx
  401015:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx
  40101a:	be cf 25 40 00       	mov    $0x4025cf,%esi				# (gdb) x/s 0x4025cf: "%d %d", 赋值给 %esi
  40101f:	b8 00 00 00 00       	mov    $0x0,%eax
  401024:	e8 c7 fb ff ff       	call   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)
  401033:	76 05                	jbe    40103a <phase_4+0x2e>
  401035:	e8 00 04 00 00       	call   40143a <explode_bomb>

这一部分和上一阶段几乎一样:读取两个整数;如果输入的整数个数不为 2 则爆炸,否则让第一个数字与 14 比较,如果第一个数字 <= 14 则跳转至下一部分(因为是无符号比较,所以负数输入都不正确),否则炸弹爆炸

于是我们知道了答案是两个数字,并且第一个数字是不超过 14 的非负数

再看另一部分:

  40103a:	ba 0e 00 00 00       	mov    $0xe,%edx
  40103f:	be 00 00 00 00       	mov    $0x0,%esi
  401044:	8b 7c 24 08          	mov    0x8(%rsp),%edi
  401048:	e8 81 ff ff ff       	call   400fce <func4>			# 调用一个神秘函数
  40104d:	85 c0                	test   %eax,%eax				# 只有返回值 %eax == 0 时使 ZF 置 1,和下一条指令结合使用
  40104f:	75 07                	jne    401058 <phase_4+0x4c>	# ZF == 0 时跳转爆炸,也就是说 func4 的返回值必须是 0
  401051:	83 7c 24 0c 00       	cmpl   $0x0,0xc(%rsp)			# 第二个数与 0 比较
  401056:	74 05                	je     40105d <phase_4+0x51>	# 第二个数如果等于 0 ,则解除炸弹,否则引爆
  401058:	e8 dd 03 00 00       	call   40143a <explode_bomb>
  40105d:	48 83 c4 18          	add    $0x18,%rsp
  401061:	c3                   	ret  

%esi%edx 分别设置为 014%edi 设置为第一个输入,然后调用 func4 函数,当且仅当返回值为 0 并且第二个输入的数为 0 时解除炸弹

现在来看 func4 :(%rdx%rcx 分别储存了两个输入的数的地址而不是值;%edi 储存了第一个数的值)

0000000000400fce <func4>:
  400fce:	48 83 ec 08          	sub    $0x8,%rsp				# 开辟了一个 int 大小的栈空间
  400fd2:	89 d0                	mov    %edx,%eax				# %eax = %edx
  400fd4:	29 f0                	sub    %esi,%eax				# %eax -= %esi 
  400fd6:	89 c1                	mov    %eax,%ecx				# %ecx = %eax
  400fd8:	c1 e9 1f             	shr    $0x1f,%ecx				# %ecx 逻辑右移 31 位,也就是取符号位
  400fdb:	01 c8                	add    %ecx,%eax				# %eax += %ecx
  400fdd:	d1 f8                	sar    %eax						# %eax 算术右移 1 位,也就是 %eax // 2
  400fdf:	8d 0c 30             	lea    (%rax,%rsi,1),%ecx		# %ecx = %rax + %rsi (地址计算)
  
  400fe2:	39 f9                	cmp    %edi,%ecx				# 比较 %ecx ~ %edi
  400fe4:	7e 0c                	jle    400ff2 <func4+0x24>		# 如果 <= 成立就跳转至 400ff2,否则继续
  # 在 %ecx > %edi (值比较)情况下
  400fe6:	8d 51 ff             	lea    -0x1(%rcx),%edx			# %edx = %rcx - 1(地址计算)
  400fe9:	e8 e0 ff ff ff       	call   400fce <func4>			# 递归调用自身!
  400fee:	01 c0                	add    %eax,%eax				# %eax *= 2,也就是返回值乘以 2
  400ff0:	eb 15                	jmp    401007 <func4+0x39>		# 跳转结束函数调用
  # 在 %ecx <= %edi 情况下:
  400ff2:	b8 00 00 00 00       	mov    $0x0,%eax				# %eax = 0
  400ff7:	39 f9                	cmp    %edi,%ecx				# 比较 %ecx ~ %edi
  400ff9:	7d 0c                	jge    401007 <func4+0x39>		# 如果 >= 成立跳转结束函数调用
  # 在 %ecx < %edi 情况下:
  400ffb:	8d 71 01             	lea    0x1(%rcx),%esi			# %esi = %rcx + 1(地址计算)
  400ffe:	e8 cb ff ff ff       	call   400fce <func4>			# 递归调用自身!
  401003:	8d 44 00 01          	lea    0x1(%rax,%rax,1),%eax	# %eax = 2 * %rax + 1 (地址计算)
  401007:	48 83 c4 08          	add    $0x8,%rsp				# 释放栈空间
  40100b:	c3                   	ret    							# 退出函数,返回值是 %eax

这是一个递归函数,我们尝试用 C 语言去描述上面的行为:

// scanf("%d %d",&x,&y); 
// 前面的一些操作可以理解为 edx = &x; ecx = &y; edi = x;
int func4(int edi, int esi, int edx){								// 更加易读的:func4(int target, int left, int right)
	eax = edi - esi; ecx = eax;
    if(ecx < 0) eax++;												// 原代码是通过获取符号位的方式进行负数的二分中点值调整
    ecx = (eax >> 1) + esi;
    	// 到这里的操作可以总结成:mid = l + (r - l) // 2,也就是二分查找计算中点值 
    if(ecx <= edi){													// 400fe4: jle 400ff2 <func4+0x24>
        if (ecx >= edi) return 0;									// 400ff9: jge 401007 <func4+0x39>
        return 2 * func4(edi, ecx+1, edx) + 1;
    }
    return 2 * func4(edi, esi, ecx-1);
}

一个典型的二分查找,只有找到这个目标值 edi = x 才会返回 0 值(另外那个负数偏移调整没有必要,因为输入的整数已经限制为了非负数)

所以我的第一个输入必须可以直接在二分查找的时候被精准定位:

14 // 2 = 7 7 // 2 = 3 3 // 2 = 1 1 // 2 = 0 这四次二分查找时出现的中间值 mid 就是所有成立的第一个输入

也就是说 7 0 3 0 1 0 0 0 都是正确答案

Phase 5

0000000000401062 <phase_5>:
  # Part 1
  401062:	53                   	push   %rbx
  401063:	48 83 ec 20          	sub    $0x20,%rsp
  401067:	48 89 fb             	mov    %rdi,%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)			# 将 %rax 的值存入栈地址 %rsp + 24
  401078:	31 c0                	xor    %eax,%eax				# 利用异或操作清零 %eax
  40107a:	e8 9c 02 00 00       	call   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       	call   40143a <explode_bomb>	# BOOM! 
  401089:	eb 47                	jmp    4010d2 <phase_5+0x70>
  # Part 2
  40108b:	0f b6 0c 03          	movzbl (%rbx,%rax,1),%ecx		# %ecx = ((%rbx + %rax 处的 1byte 值) 零扩展至 32 位)
  40108f:	88 0c 24             	mov    %cl,(%rsp)				# %rcx 的低八位 存入 %rsp 指向的地址
  401092:	48 8b 14 24          	mov    (%rsp),%rdx				# %rsp 地址处的内容传入 %rdx
  401096:	83 e2 0f             	and    $0xf,%edx				# %edx 与 0b1111 按位与,结果存入 %edx(也就是保留低四位值)
  401099:	0f b6 92 b0 24 40 00 	movzbl 0x4024b0(%rdx),%edx		# %edx = ((0x4024b0 + %rdx 处的 1byte 值) 零扩展至 32 位)
  4010a0:	88 54 04 10          	mov    %dl,0x10(%rsp,%rax,1)	# %rdx 的低八位 存入地址 %rsp + %rax + 0x10
  4010a4:	48 83 c0 01          	add    $0x1,%rax				# %rax++
  4010a8:	48 83 f8 06          	cmp    $0x6,%rax				# %rax6 比较
  4010ac:	75 dd                	jne    40108b <phase_5+0x29>	# 如果不相等,回到 40108b
  # Part 3
  4010ae:	c6 44 24 16 00       	movb   $0x0,0x16(%rsp)			# *(%rsp+22) = 0x00,更直观的来说:rsp[22] = 0;
  4010b3:	be 5e 24 40 00       	mov    $0x40245e,%esi			# (gdb) x/s 0x40245e: "flyers"
  4010b8:	48 8d 7c 24 10       	lea    0x10(%rsp),%rdi			# %rdi = %rsp + 16(地址计算)
  4010bd:	e8 76 02 00 00       	call   401338 <strings_not_equal>	# 比较两个字符串 %rdi 与 %rsi 是否不相等,不相等返回 1
  4010c2:	85 c0                	test   %eax,%eax				# 这一部分和 Phase 1 相同的操作
  4010c4:	74 13                	je     4010d9 <phase_5+0x77>	# 两个字符串相等才会跳转,否则炸弹爆炸
  4010c6:	e8 6f 03 00 00       	call   40143a <explode_bomb>	# BOOM!
  4010cb:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)			# 5 字节长度的 NOP 操作
  4010d0:	eb 07                	jmp    4010d9 <phase_5+0x77>	# 跳转
  4010d2:	b8 00 00 00 00       	mov    $0x0,%eax				# 我也不知道这两行有什么用
  4010d7:	eb b2                	jmp    40108b <phase_5+0x29>	# 0_0
  # Part 4
  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       	call   400b30 <__stack_chk_fail@plt>
  4010ee:	48 83 c4 20          	add    $0x20,%rsp
  4010f2:	5b                   	pop    %rbx
  4010f3:	c3                   	ret    

程序中有两处 00 00 有分隔作用,只要考虑 401073 ~ 4010de 的内容即可,其他的部分是 Canary 栈保护机制的体现,防攻击用的,无需深挖

先看这一部分:

  # Part 1
  401073:	48 89 44 24 18       	mov    %rax,0x18(%rsp)			# 将 %rax 的值存入栈地址 %rsp + 24
  401078:	31 c0                	xor    %eax,%eax				# 利用异或操作清零 %eax
  40107a:	e8 9c 02 00 00       	call   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       	call   40143a <explode_bomb>	# BOOM! 
  401089:	eb 47                	jmp    4010d2 <phase_5+0x70>

要求是输入一个长度为 6 的字符串,否则炸弹爆炸

接下来的一段可能不是很好理解,我们不妨先往后看:

  # Part 3
  4010ae:	c6 44 24 16 00       	movb   $0x0,0x16(%rsp)			# *(%rsp+22) = 0x00,更直观的来说:rsp[22] = 0;
  4010b3:	be 5e 24 40 00       	mov    $0x40245e,%esi			# (gdb) x/s 0x40245e: "flyers"
  4010b8:	48 8d 7c 24 10       	lea    0x10(%rsp),%rdi			# %rdi = %rsp + 16(地址计算)
  4010bd:	e8 76 02 00 00       	call   401338 <strings_not_equal>	# 比较两个字符串 %rdi 与 %rsi 是否不相等,不相等返回 1
  4010c2:	85 c0                	test   %eax,%eax				# 这一部分和 Phase 1 相同的操作
  4010c4:	74 13                	je     4010d9 <phase_5+0x77>	# 两个字符串相等才会跳转,否则炸弹爆炸
  4010c6:	e8 6f 03 00 00       	call   40143a <explode_bomb>	# BOOM!
  4010cb:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)			# 5 字节长度的 NOP 操作
  4010d0:	eb 07                	jmp    4010d9 <phase_5+0x77>	# 跳转
  4010d2:	b8 00 00 00 00       	mov    $0x0,%eax				# 我也不知道这两行有什么用
  4010d7:	eb b2                	jmp    40108b <phase_5+0x29>	# 0_0

有了 Phase 1 的经验我们可以看出来,这部分代码是将 %rdi 设置为输入的六位字符串的起始位置(4010b8,结合前面的 movb 指令可以看得出来),为字符串添加一个 \04010ae),然后将这个字符串和 0x40245e 存储的字符串 flyers 进行比较,如果相同则拆弹成功。似乎和 Phase 1 没有区别

回头看之前的部分,这一部分大概率会对原始输入的字符串进行修改(否则就和 Phase 1 一模一样了)

(注释部分进行了更加明显的修改,没错我用 AI 修正了一下)

  # Part 2
  40108b:	0f b6 0c 03          	movzbl (%rbx,%rax,1),%ecx		# 从 %rbx + %rax 地址读取 1byte,零扩展至 32 位存入 %ecx
  40108f:	88 0c 24             	mov    %cl,(%rsp)				# 将 %cl(%ecx 的低 8 位)存入栈顶 %rsp
  401092:	48 8b 14 24          	mov    (%rsp),%rdx				# 从栈顶 %rsp 读取 8byte 到 %rdx,实际只用到低 1byte
  401096:	83 e2 0f             	and    $0xf,%edx				# 保留 %edx 的低 4 位,其余清零
  401099:	0f b6 92 b0 24 40 00 	movzbl 0x4024b0(%rdx),%edx		# 从地址 0x4024b0 + %rdx 读取 1byte,零扩展至 32
  4010a0:	88 54 04 10          	mov    %dl,0x10(%rsp,%rax,1)	# 将 %dl(%edx 的低 8 位)存入 %rsp + %rax + 16
  4010a4:	48 83 c0 01          	add    $0x1,%rax				# %rax += 1(循环计数器递增)
  4010a8:	48 83 f8 06          	cmp    $0x6,%rax				# 比较 %rax ~ 6(检查是否循环 6 次)
  4010ac:	75 dd                	jne    40108b <phase_5+0x29>	# 若未完成 6 次循环,跳回 40108b

最后三行可以看得出来这部分内容进行了 6 次循环操作,对应了长度为 6 的输入字符串,大概率是对每一个字符进行了某种映射变换

不妨看一看 0x4024b0 处存储了什么内容:

(gdb) x 0x4024b0
0x4024b0 <array.3449>:	"maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"

后面那一串应该不用管,关键是前面的 maduiersnfotvbyl ,这样一串无序的字符串很像是一种映射表(flyers 这几个字母在这里都能找到)

也就是说,正确的输入(六位字符串)在经过映射之后会转化为 flyers ,或许会有一种很简单的假设:

input a b c d e f g h i j k l m n o p
(wrong) output m a d u i e r s n f o t v b y l

当然事实不是这样,我们发现:

  40108b:	0f b6 0c 03          	movzbl (%rbx,%rax,1),%ecx		# 从 %rbx + %rax 地址读取 1byte,零扩展至 32 位存入 %ecx
  40108f:	88 0c 24             	mov    %cl,(%rsp)				# 将 %cl(%ecx 的低 8 位)存入栈顶 %rsp
  401092:	48 8b 14 24          	mov    (%rsp),%rdx				# 从栈顶 %rsp 读取 8byte 到 %rdx,实际只用到低 1byte
  401096:	83 e2 0f             	and    $0xf,%edx				# 保留 %edx 的低 4 位,其余清零

每个字符(作为 ASCII 形式)最终仅仅读取了最低的四位,也就是说只有最后四位是有用的

  401099:	0f b6 92 b0 24 40 00 	movzbl 0x4024b0(%rdx),%edx		# 从地址 0x4024b0 + %rdx 读取 1byte,零扩展至 32
  4010a0:	88 54 04 10          	mov    %dl,0x10(%rsp,%rax,1)	# 将 %dl(%edx 的低 8 位)存入 %rsp + %rax + 16

接下来这四位二进制内容 %rdx 作为偏移值加在了 0x402400 的地址处,新的 %rdx 又被转移到了 %rsp + %rax + 16 (这个位置在之后用于检验字符串相等)

也就是说:字符串的每个字符对应的 ASCII 值的最低四位作为索引 %edx 映射 maduiersnfotvbyl (恰好是 16 位)的内容

input 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111
output m a d u i e r s n f o t v b y l

所以正确答案的六个字符的 ASCII 码低四位应该依次为 1001 1111 1110 0101 0110 0111 ,比如 ionefg yonuvw

Phase 6

00000000004010f4 <phase_6>:
  # Part 1
  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       	call   40145c <read_six_numbers>
  40110b:	49 89 e6             	mov    %rsp,%r14
  40110e:	41 bc 00 00 00 00    	mov    $0x0,%r12d
  # Part 2
  401114:	4c 89 ed             	mov    %r13,%rbp
  401117:	41 8b 45 00          	mov    0x0(%r13),%eax
  40111b:	83 e8 01             	sub    $0x1,%eax
  40111e:	83 f8 05             	cmp    $0x5,%eax
  401121:	76 05                	jbe    401128 <phase_6+0x34>
  401123:	e8 12 03 00 00       	call   40143a <explode_bomb>
  401128:	41 83 c4 01          	add    $0x1,%r12d
  40112c:	41 83 fc 06          	cmp    $0x6,%r12d
  401130:	74 21                	je     401153 <phase_6+0x5f>
  401132:	44 89 e3             	mov    %r12d,%ebx
  401135:	48 63 c3             	movslq %ebx,%rax
  401138:	8b 04 84             	mov    (%rsp,%rax,4),%eax
  40113b:	39 45 00             	cmp    %eax,0x0(%rbp)
  40113e:	75 05                	jne    401145 <phase_6+0x51>
  401140:	e8 f5 02 00 00       	call   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>
  # Part 3
  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
  401160:	89 ca                	mov    %ecx,%edx
  401162:	2b 10                	sub    (%rax),%edx
  401164:	89 10                	mov    %edx,(%rax)
  401166:	48 83 c0 04          	add    $0x4,%rax
  40116a:	48 39 f0             	cmp    %rsi,%rax
  40116d:	75 f1                	jne    401160 <phase_6+0x6c>
  # Part 4
  40116f:	be 00 00 00 00       	mov    $0x0,%esi
  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
  401188:	48 89 54 74 20       	mov    %rdx,0x20(%rsp,%rsi,2)
  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
  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
  4011a9:	eb cb                	jmp    401176 <phase_6+0x82>
  # Part 5
  4011ab:	48 8b 5c 24 20       	mov    0x20(%rsp),%rbx
  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>
  4011d2:	48 c7 42 08 00 00 00 	movq   $0x0,0x8(%rdx)
  4011d9:	00 
  # Part 6
  4011da:	bd 05 00 00 00       	mov    $0x5,%ebp
  4011df:	48 8b 43 08          	mov    0x8(%rbx),%rax
  4011e3:	8b 00                	mov    (%rax),%eax
  4011e5:	39 03                	cmp    %eax,(%rbx)
  4011e7:	7d 05                	jge    4011ee <phase_6+0xfa>
  4011e9:	e8 4c 02 00 00       	call   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>
  # Part 7
  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                   	ret 

太长了,我们一点一点的分析:

Tip

一些对较长的原程序的阶段划分方法:

call <explode_bomb> 可以帮助拆分一个程序的不同部分,虽然它不一定表示一个部分的结束

在之前的 Phase 遇到的相似代码也可以帮助拆分程序,让程序看上去更加有熟悉感

j 类型指令也有一定的参考作用

00000000004010f4 <phase_6>:
  # Part 1
  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       	call   40145c <read_six_numbers>
  40110b:	49 89 e6             	mov    %rsp,%r14
  40110e:	41 bc 00 00 00 00    	mov    $0x0,%r12d

读取六个数字分别位于 %rsp ~ %rsp+0x14 处,%r13 %r14 %rsi 都设置为 %rsp (也就是指向 num1),%r12d 设置为 0

  # Part 2
  401114:	4c 89 ed             	mov    %r13,%rbp
  401117:	41 8b 45 00          	mov    0x0(%r13),%eax
  40111b:	83 e8 01             	sub    $0x1,%eax
  40111e:	83 f8 05             	cmp    $0x5,%eax
  401121:	76 05                	jbe    401128 <phase_6+0x34>
  401123:	e8 12 03 00 00       	call   40143a <explode_bomb>
  401128:	41 83 c4 01          	add    $0x1,%r12d
  40112c:	41 83 fc 06          	cmp    $0x6,%r12d
  401130:	74 21                	je     401153 <phase_6+0x5f>
  401132:	44 89 e3             	mov    %r12d,%ebx
  401135:	48 63 c3             	movslq %ebx,%rax
  401138:	8b 04 84             	mov    (%rsp,%rax,4),%eax
  40113b:	39 45 00             	cmp    %eax,0x0(%rbp)
  40113e:	75 05                	jne    401145 <phase_6+0x51>
  401140:	e8 f5 02 00 00       	call   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>

%rbp 设置为 %r13%eax 设置为 (%r13) ,也就是 num1的值,然后将其减去 15 进行无符号比较,如果 <= 成立就跳转,否则炸弹爆炸,这里说明第一个数字必须 <= 6 (注意到是无符号比较并且有减一操作,因此这个数也必须 >= 1)。

接下来将 %r12d (此时为 0)自增并与 6 比较,如果相等就跳转到 401153 处(跳离上面的代码阶段)。这里是一个循环计数器的体现,循环六次。

否则将自增后的 %r12d (此时为 1)赋值给 %ebx,这个值经过符号扩展后传递给 %rax,接下来 %eax = (%rsp + 4 * %rax) ,这一部分的操作逻辑很像数组索引的计算,事实上 %eax 被赋值为 “下一个数字”

接下来比较 %rbp 指向的值与 %eax 内容是否相等,如果相等就爆炸,注意到 %rbp 是第一个数,因此以上的一轮操作是为了确保 “输入的前两个数不相等”

而接下来的三行指令(从 401145 开始)将 %ebx 自增并与 5 比较,如果 <= 成立就回到 401135 重新进行 “再下一个数字” 与第一个数字的比较,可见 “第一个数应该与它后面的所有数都不一样”

最后是对 %r13 增加 4 (一个数字的长度,也就是移动到下一个元素),进行以上所有操作的循环

这些内容可以总结为下面的 C 语言函数表示:检测输入的六个数是否可以组成 1 2 3 4 5 6 的某种无重复排列(比如 2 5 1 4 6 3

void check_1_to_6_permutation(int nums[6]) {	// 是否满足 1~6 的排列
    // 检查每个数字是否在 1-6 范围内
    for (int i = 0; i < 6; i++) {
        if (nums[i] < 1 || nums[i] > 6) explode_bomb();
        // 检查当前数字是否与后续数字重复
        for (int j = i + 1; j < 6; j++)
            if (nums[i] == nums[j]) explode_bomb();
    }
}

在了解了输入内容要求后继续分析下面的部分:

  # Part 3
  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
  401160:	89 ca                	mov    %ecx,%edx
  401162:	2b 10                	sub    (%rax),%edx
  401164:	89 10                	mov    %edx,(%rax)
  401166:	48 83 c0 04          	add    $0x4,%rax
  40116a:	48 39 f0             	cmp    %rsi,%rax
  40116d:	75 f1                	jne    401160 <phase_6+0x6c>

%rsi 的地址修改为 %rsp + 0x18%rax 被赋值为 %r14%r14 在之前指向了 num1 ),%ecx 设置为常数 7

%edx 设置为了 7 - num1%rcx - (%rax)),然后将 %rax 指向的内容修改为了 %edx ,接着将 %rax 增加四(从而指向下一个数字),判断 %rax%rsi 是否相同,不相同则跳转到赋值 %edx 的循环(循环计数器)

上面这一阶段的作用是将**每一个数字 num 修改为 7 - num **

  # Part 4
  40116f:	be 00 00 00 00       	mov    $0x0,%esi
  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
  401188:	48 89 54 74 20       	mov    %rdx,0x20(%rsp,%rsi,2)
  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
  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
  4011a9:	eb cb                	jmp    401176 <phase_6+0x82>

这一段开始出现了很多的跳转操作,我们尝试模拟一遍程序运行的流程。这里有一个 Mermaid 图表可供流程参考:

Note

Gmeek 渲染似乎不会渲染 Mermaid 图,可以自己把代码复制到其他地方渲染

flowchart TD
    A["mov $0x0,%esi"]
    A --> C["mov (%rsp,%rsi,1),%ecx"]
    C --> D{"cmp $0x1,%ecx"}
    D -->|ecx <= 1| E["mov $0x6032d0,%edx"]
    D -->|ecx > 1| F["mov $0x1,%eax<br>mov $0x6032d0,%edx"]
    F --> I["mov 0x8(%rdx),%rdx"]
    I --> J["add $0x1,%eax"]
    J --> K{"cmp %ecx,%eax"}
    K -->|eax != ecx| I
    K -->|eax == ecx| M["mov %rdx,0x20(%rsp,%rsi,2)<br>add $0x4,%rsi"]
    E --> M
    M --> O{"cmp $0x18,%rsi"}
    O -->|rsi != 24| C
    O -->|rsi == 24| P["Go to Part 5"]

看不懂也不要紧,我们不妨先看一下 0x6032d0 的内容:

(gdb) x/24 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
# 注释			   # 一个int数      # 一个序号	      # 指向的地址,恰好是 node1 -> node2 -> ... -> node6

<node> 这个标签来看, 0x6032d0 开始的地址存储了一个链表,用 C 语言可以这么描述:

struct node{
	int value;				// 当前节点的值
    int label;				// 节点序号
    struct node* next;		// 指向的下一节点地址
}

现在再结合 Mermaid 流程图看一遍这段程序:

%rsi 作为最外部的循环计数器,初始化为 0 ,每次加 440118d),加到 24 就停止循环,也就是 for(int i = 0; i < 6; i++)

循环内,mov (%rsp,%rsi,1),%ecx 这一步可以看作 int ECX = a[i] ,其中 a 是存储了六个数字的数组(之前的阶段也有类似的程序),%rsi 很明显是一个偏移值,用来遍历整个数组

%ecx 被更新赋值以后,其与 1 进行比较:

​ 如果这个数 <= 1(结合实际情况也就是判断是否为 1),就把 %edx 设置为 0x6032d0 (也就是头节点 node1 的地址)

​ 否则 %ecx > 1 ,此时将 %eax 设置为 1%edx 设置为 0x6032d0 ,接下来将 %rdx 设置为 (%rdx + 8) (也就是下一个地址 node->next),将 %rax 加上一,重复操作直到 %rcx == %rax

(也就是说: %edx 被设置为了第 %ecx 个节点的地址)

接下来 mov %rdx,0x20(%rsp,%rsi,2)%rdx 复制到了 (%rsp + 0x20 + 2 * %rsi) 指向的地址处,这个操作将根据最外部的循环计数器遍历整个数组

总结:此阶段将输入的六个数作为节点序号,依次对应六个节点地址按顺序存储到从 %rsp+20 开始的栈空间处(构造一个地址数组)

  # Part 5
  4011ab:	48 8b 5c 24 20       	mov    0x20(%rsp),%rbx			# 将上一个 Part 得到的地址数组的第一个地址存入 %rbx
  4011b0:	48 8d 44 24 28       	lea    0x28(%rsp),%rax			# %rax = %rsp + 40(第二个元素的地址)
  4011b5:	48 8d 74 24 50       	lea    0x50(%rsp),%rsi			# %rsi = %rsp + 32 + 6*8(地址数组末尾后的地址,也就是arr.end())
  4011ba:	48 89 d9             	mov    %rbx,%rcx				# %rcx = %rbx
  4011bd:	48 8b 10             	mov    (%rax),%rdx				# %rdx = (%rax)
  4011c0:	48 89 51 08          	mov    %rdx,0x8(%rcx)			# %rdx 值复制到 (%rcx+8) 地址
  4011c4:	48 83 c0 08          	add    $0x8,%rax				# %rax +=8
  4011c8:	48 39 f0             	cmp    %rsi,%rax				# 比较 %rax ~ %rsi
  4011cb:	74 05                	je     4011d2 <phase_6+0xde>	# 如果相等就跳转至 4011d2
  4011cd:	48 89 d1             	mov    %rdx,%rcx				# 否则 %rcx = %rdx
  4011d0:	eb eb                	jmp    4011bd <phase_6+0xc9>	# 并且跳回到 4011bd
  4011d2:	48 c7 42 08 00 00 00 	movq   $0x0,0x8(%rdx)			# %rax == %rsi 时,将 0 存储到内存地址为 %rdx + 8 的位置
  4011d9:	00 

不妨记上一个 Part 构造的地址数组为 addr[6], 用 C++ 重写程序

Node* Part5(Node* addr[]) {
    Node* first = addr[0];							// %rbx = addr[0]
    Node* current = first;							// %rcx = %rbx
    // 从第二个元素开始遍历地址数组
    for (int i = 1; i < 6; i++) {					// for (%rax = &addr[1], %rax < addr.end(), %rax++) 
        current->next = addr[i];  					// 当前节点的下一节点由 addr[i] 决定
        current = current->next;        			// 移动到下一个节点 
    }
    
    current->next = nullptr;      					// 对应 movq $0x0,0x8(%rdx),作用是终止链表
    return first;									// 得到一个
}

0x6032d0 处的原始链表记为 node1 -> ... -> node6,这一部分程序根据输入的六个数字顺序重新链接了链表

  # Part 6
  4011da:	bd 05 00 00 00       	mov    $0x5,%ebp
  4011df:	48 8b 43 08          	mov    0x8(%rbx),%rax
  4011e3:	8b 00                	mov    (%rax),%eax
  4011e5:	39 03                	cmp    %eax,(%rbx)
  4011e7:	7d 05                	jge    4011ee <phase_6+0xfa>
  4011e9:	e8 4c 02 00 00       	call   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>

这一部分程序的内容十分明显了:重新链接后的链表的存储的值必须降序,否则引爆炸弹。正确的链表连接方式是 3 -> 4 -> 5 -> 6 -> 1 -> 2

考虑到 Part 3 将每一个数 num 改写为 7 - num ,所以整个 Phase 最终的答案就是 4 3 2 1 6 5

  # Part 7
  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                   	ret 

清理栈帧并恢复被调用者保存的寄存器,不需要多分析

Secret Phase

结束六个常规的 Phase 后并没有发现任何触发 Secret Phase 的代码段,这里采用了一个比较投机的操作:搜索出现了 401242 地址的函数

函数 <phase_defused> 的使用场景是:在 main 函数中,在每一个炸弹被拆除时被调用,而 401630 地址处出现了跳转至隐藏阶段的跳转函数,对这个函数的相关部分进行分析:

  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					# (gdb) x/s 0x402619: "%d %d %s"
  4015f5:	bf 70 38 60 00       	mov    $0x603870,%edi					# 0x603870 <input_strings+240>:	""
  4015fa:	e8 f1 f5 ff ff       	call   400bf0 <__isoc99_sscanf@plt>
  4015ff:	83 f8 03             	cmp    $0x3,%eax
  401602:	75 31                	jne    401635 <phase_defused+0x71>
  401604:	be 22 26 40 00       	mov    $0x402622,%esi					# 0x402622:	"DrEvil"
  401609:	48 8d 7c 24 10       	lea    0x10(%rsp),%rdi
  40160e:	e8 25 fd ff ff       	call   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       	call   400b10 <puts@plt>
  401621:	bf 20 25 40 00       	mov    $0x402520,%edi
  401626:	e8 e5 f4 ff ff       	call   400b10 <puts@plt>
  40162b:	b8 00 00 00 00       	mov    $0x0,%eax
  401630:	e8 0d fc ff ff       	call   401242 <secret_phase>

很明显这个特殊的密码是 DrEvil ,而正确输入密码的方式是在某个阶段的输入中输入两个整数 + 字符串(即 DrEvil)。

(具体一点来说:将 0x402619 作为输入格式,将 0x603870 作为输入(一定是之前某一个阶段的输入),提取出 %s 判断是否为 DrEvil

我们知道 Phase 3 和 Phase 4 需要输入的都是两个数字,所以哪一个阶段应该多一个输入呢?我们不妨使用gdb调试,在 Phase 5 开始的时候打一个断点,然后检查 0x603870 的内容

(gdb) break *0x401062
Breakpoint 1 at 0x401062
(gdb) r
Starting program: /home/nopthon/Desktop/bomb/bomb 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Border relations with Canada have never been better.
Phase 1 defused. How about the next one?
1 2 4 8 16 32
That's number 2.  Keep going!
2 707																				# Phase 3 的输入
Halfway there!
1 0																					# Phase 4 的输入
So you got that one.  Try this one.
^_^(a breakpoint here)

Breakpoint 1, 0x0000000000401062 in phase_5 ()
(gdb) x/s 0x603870
0x603870 <input_strings+240>:	"1 0"												# Phase 4 的输入

我们发现 0x603870 存储的是第四阶段的输入,因此在第四阶段的输入中多输入一个字符串 DrEvil 就可以进入隐藏阶段(比如 1 0 DrEvil

下面是隐藏阶段的代码:

0000000000401242 <secret_phase>:
  401242:	53                   	push   %rbx								# 被调用者保存寄存器
  401243:	e8 56 02 00 00       	call   40149e <read_line>				# 读取一行内容,返回值 %rax
  401248:	ba 0a 00 00 00       	mov    $0xa,%edx						# %edx = 10
  40124d:	be 00 00 00 00       	mov    $0x0,%esi						# %esi = 0
  401252:	48 89 c7             	mov    %rax,%rdi						# %rdi = %rax
  401255:	e8 76 f9 ff ff       	call   400bd0 <strtol@plt>				# 调用 C 语言函数 strtol() ,字符串转化为 long 整数
  40125a:	48 89 c3             	mov    %rax,%rbx						# 返回值(整数) %rax 存入 %rbx
  40125d:	8d 40 ff             	lea    -0x1(%rax),%eax					# %rax = %rax - 1 (地址计算)
  401260:	3d e8 03 00 00       	cmp    $0x3e8,%eax						# %eax1000 比较
  401265:	76 05                	jbe    40126c <secret_phase+0x2a>		# %eax <= 1000 则跳转
  401267:	e8 ce 01 00 00       	call   40143a <explode_bomb>			# 否则 BOOM! 
  40126c:	89 de                	mov    %ebx,%esi						# %esi = %ebx (也就是之前的返回值)
  40126e:	bf f0 30 60 00       	mov    $0x6030f0,%edi
  401273:	e8 8c ff ff ff       	call   401204 <fun7>					# 调用 fun7 函数
  401278:	83 f8 02             	cmp    $0x2,%eax						# 返回值 %eax2 比较
  40127b:	74 05                	je     401282 <secret_phase+0x40>		# 如果相等就跳转(代表拆弹成功)
  40127d:	e8 b8 01 00 00       	call   40143a <explode_bomb>			# 否则 BOOM!
  401282:	bf 38 24 40 00       	mov    $0x402438,%edi					# 这里存的是拆弹成功的祝贺语 :)
  401287:	e8 84 f8 ff ff       	call   400b10 <puts@plt>
  40128c:	e8 33 03 00 00       	call   4015c4 <phase_defused>
  401291:	5b                   	pop    %rbx
  401292:	c3                   	ret    
  401293:	90                   	nop
  ... ... # 很多的 nop
  40129f:	90                   	nop

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
  40120f:	39 f2                	cmp    %esi,%edx
  401211:	7e 0d                	jle    401220 <fun7+0x1c>
  401213:	48 8b 7f 08          	mov    0x8(%rdi),%rdi
  401217:	e8 e8 ff ff ff       	call   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       	call   401204 <fun7>
  401232:	8d 44 00 01          	lea    0x1(%rax,%rax,1),%eax
  401236:	eb 05                	jmp    40123d <fun7+0x39>
  401238:	b8 ff ff ff ff       	mov    $0xffffffff,%eax
  40123d:	48 83 c4 08          	add    $0x8,%rsp
  401241:	c3                   	ret 

主函数部分的程序很清晰,主要关注 fun7 函数的内容

为了方便理解绘制出 fun7 的 Mermaid 示意图:

flowchart TD
    A[sub $0x8, %rsp] --> B{"test %rdi, %rdi"}
    B -->|%rdi == 0| C["mov $0xffffffff, %eax"]
    B -->|%rdi != 0| D["mov (%rdi), %edx"]
    D --> E{"cmp %esi, %edx"}
    E -->|%edx <= %esi| F["mov $0x0, %eax"]
    E -->|%edx > %esi| G["mov 0x8(%rdi), %rdi"]
    G --> H["call fun7 (递归)"]
    H --> I["add %eax, %eax"]
    I --> J
    F --> K{"cmp %esi, %edx"}
    K -->|%edx == %esi| J
    K -->|%edx != %esi| L["mov 0x10(%rdi), %rdi"]
    L --> M["call fun7 (递归)"]
    M --> N["lea 0x1(%rax,%rax,1), %eax"]
    N --> J
    C --> J
    J[add $0x8, %rsp]
    J --> P["ret (返回)"]

存在递归,并且有搜索的模式,考虑是对一个数据结构的操作

在首次调用 fun7 前,被存入 %edi 的(0x6030f0)应该是一个数据结构的起点,可以用 gdb 进行访问

# 0x6030f0 处的内容,被存入 %edi 
0x6030f0 <n1>:		0x00000024	0x00000000	0x00603110	0x00000000	0x00603130	0x00000000	0x00000000	0x00000000
0x603110 <n21>:		0x00000008	0x00000000	0x00603190	0x00000000	0x00603150	0x00000000	0x00000000	0x00000000
0x603130 <n22>:		0x00000032	0x00000000	0x00603170	0x00000000	0x006031b0	0x00000000	0x00000000	0x00000000
0x603150 <n32>:		0x00000016	0x00000000	0x00603270	0x00000000	0x00603230	0x00000000	0x00000000	0x00000000
0x603170 <n33>:		0x0000002d	0x00000000	0x006031d0	0x00000000	0x00603290	0x00000000	0x00000000	0x00000000
0x603190 <n31>:		0x00000006	0x00000000	0x006031f0	0x00000000	0x00603250	0x00000000	0x00000000	0x00000000
0x6031b0 <n34>:		0x0000006b	0x00000000	0x00603210	0x00000000	0x006032b0	0x00000000	0x00000000	0x00000000
0x6031d0 <n45>:		0x00000028	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000
0x6031f0 <n41>:		0x00000001	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000
0x603210 <n47>:		0x00000063	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000
0x603230 <n44>:		0x00000023	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000
0x603250 <n42>:		0x00000007	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000
0x603270 <n43>:		0x00000014	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000
0x603290 <n46>:		0x0000002f	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000
0x6032b0 <n48>:		0x000003e9	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000	0x00000000
#					# 存储的值				 # 左分支节点地址			 # 右分支节点地址			# 填充对齐

从标签来看很像是一个二叉树结构,考虑每个节点分别存储的内容依次是:

struct Node{
	int val;
	Node* left;
	Node* right;
}

据此绘制图像:

flowchart TD
    n1["n1 (0x6030f0)<br/>val: 36"]
    
    n1 --> n21["n21 (0x603110)<br/>val: 8"]
    n1 --> n22["n22 (0x603130)<br/>val: 50"]
    
    n21 --> n31["n31 (0x603190)<br/>val: 6"]
    n21 --> n32["n32 (0x603150)<br/>val: 22"]
    
    n22 --> n33["n33 (0x603170)<br/>val: 45"]
    n22 --> n34["n34 (0x6031b0)<br/>val: 107"]
    
    n31 --> n41["n41 (0x6031f0)<br/>val: 1"]
    n31 --> n42["n42 (0x603250)<br/>val: 7"]
    
    n32 --> n43["n43 (0x603270)<br/>val: 20"]
    n32 --> n44["n44 (0x603230)<br/>val: 35"]
    
    n33 --> n45["n45 (0x6031d0)<br/>val: 40"]
    n33 --> n46["n46 (0x603290)<br/>val: 47"]
    
    n34 --> n47["n47 (0x603210)<br/>val: 99"]
    n34 --> n48["n48 (0x6032b0)<br/>val: 1001"]

不难看出这是一个二叉搜索树,而 fun7 的作用可以用 C 语言这样表示:(参见搜索元素,代码非常相近)

int fun7(Node *node, int value) {					// %esi 是一个被查找的值
    if (node == NULL) {
        return -1;									// %rdi == 0 时,返回值 0xffffffff
    }
    // %rdi != 0:
    int node_val = node->value;						// 40120d: mov (%rdi),%edx
    
    if (node_val <= value) {						// 401225: cmp %esi,%edx
        int res = 0;

        if (node_val == value)
            return res;								// node_val == value
       
        res = 2 * fun7(node->right, value) + 1;		// node_val < value
        return res;
    } else {
        res = 2 * fun7(node->left, value);			// node_val > value
        return res;
    }
}

<secret_phase> 主程序要求 fun7 的最终返回值为 2,因此被查找的值应该要从内到外逆推递归,计算2 * (2 * 0 + 1) 作为返回值

比如以 n32 这个节点为例,被查找的值为 22,那么向上逆推递归回到根节点的时候,经历了这样的步骤:

// 第三层递归
return 0;			// 其实这里也可以是 2*0 的结果,对应节点 n43
// 第二层递归
return 2*fun7 + 1;	// 1
// 第一层递归
return 2*fun7;		// 2

所以该阶段的最终答案为 2022


Final Ans

❯ ./bomb
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Border relations with Canada have never been better.			# Phase 1
Phase 1 defused. How about the next one?
1 2 4 8 16 32													# Phase 2
That's number 2.  Keep going!
2 707															# Phase 3
Halfway there!
1 0 DrEvil														# Phase 4
So you got that one.  Try this one.
yonuvw															# Phase 5
Good work!  On to the next...
4 3 2 1 6 5														# Phase 6
Curses, you've found the secret phase!
But finding it and solving it are quite different...
22																# Secret Phase
Wow! You've defused the secret stage!
Congratulations! You've defused the bomb!						# Congrats!