介绍

Bomb Lab是CMU Introduction to Computer Systems的一个平时作业。这个课程的讲师就是有名的CSAPP书作者,这门课质量也是出奇地高。作为课程的精华,配套的若干个Lab实验也是非常值得一做的。我本科时比较憨,不知道有这书,现在读研才开始看,有种相见恨晚的感觉。这篇文章记录了我花10个小时(CMU指定用时?)做Bomb Lab的过程。因为汇编代码看起来确实挺累的,细节繁琐之处如头发一样难以捋清,所以有一点点bug请勿见怪^_^。

Bomb Lab中的“炸弹”,是六个不同的测试。每个测试要求你输入一个字符串,如果这个字符串和炸弹的密码对应上了,炸弹便被成功拆除。执行

two:$ ./bomb xxx.txt

输入6句解密字符串,即可通过测试。这6个字符串需要自己从bomb二进制文件的反汇编代码中寻找。bombbomb.c编译而成,其中关键的initialize_bombphase_1phase_defused等函数没有调试信息,只能在反编译文件中寻找答案……这里的入手点就是通过

two:$ objdump -t bomb > symbols
two:$ objdump -d bomb > assembly

获得的符号表和汇编文件。symbols符号表最后一列是文件中所有全局变量和调用的函数的名字,第一列是它们的地址。通过这个地址可以在assembly文件中快速定位函数(其实直接搜索函数名称也可以啦……)。assembly中,找到main函数,开始阅读。294-302行是initialize_bombphase_1的对应指令:

# 调用initialize_bomb
400e19:	e8 84 05 00 00       	callq  4013a2 <initialize_bomb>
# printf...read_line...
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>
# 调用phase_1
400e37:	48 89 c7             	mov    %rax,%rdi
400e3a:	e8 a1 00 00 00       	callq  400ee0 <phase_1>
400e3f:	e8 80 07 00 00       	callq  4015c4 <phase_defused>

initialize_bomb对应的地址看,这个函数非常简单:

00000000004013a2 <initialize_bomb>:
  # 在栈帧上分配8字节空间
  4013a2:	48 83 ec 08          	sub    $0x8,%rsp
  # 传参,第一二个参数分别是0x2, $0x4012a0
  4013a6:	be a0 12 40 00       	mov    $0x4012a0,%esi
  4013ab:	bf 02 00 00 00       	mov    $0x2,%edi
  # 调用signal函数
  4013b0:	e8 db f7 ff ff       	callq  400b90 <signal@plt>
  # 释放空间,返回
  4013b5:	48 83 c4 08          	add    $0x8,%rsp
  4013b9:	c3                   	retq   

根据这个回答,signal函数和动态链接做了某些事,但我们无法知道……所以initialize_bomb依然不能提供什么有用的信息。

实用的操作

在这个实验里我尝试了几种操作,比较好用的有以下几种:

# gdb
# 打印地址上的信息,以字符串形式查看
(gdb) p (char*) 0x402400
$2 = 0x402400 "Border relations with Canada have never been better."

# 按指令的地址打断点
(gdb) b *0x400f75
Breakpoint 1 at 0x400f75

# 观察地址处的若干个字(这里一个字为8字节而非16字节)
(gdb) x/8g 0x402470
0x402470:       0x0000000000400f7c      0x0000000000400fb9
0x402480:       0x0000000000400f83      0x0000000000400f8a
0x402490:       0x0000000000400f91      0x0000000000400f98
0x4024a0:       0x0000000000400f9f      0x0000000000400fa6

# 到下一步指令
(gdb) si

# 以十六进制打印寄存器%rax的值
(gdb) p/x $rax
$16 = 0x7ffffffedf08

phase_1

先来看phase_1

0000000000400ee0 <phase_1>:
# 参数:%rdi -- input
  # 分配8字节空间
  400ee0:	48 83 ec 08          	sub    $0x8,%rsp
  # 调用函数strings_not_equal,参数为我们的输入和0x402400。注意在main函数里将输入放到了%rdi里
  400ee4:	be 00 24 40 00       	mov    $0x402400,%esi
  400ee9:	e8 4a 04 00 00       	callq  401338 <strings_not_equal>
  # 判断%eax是否为0,如果是跳转到地址400ef7。这里跳过了explode_bomb,估计在密码成功匹配后,
  # strings_not_equal函数返回0
  400eee:	85 c0                	test   %eax,%eax
  400ef0:	74 05                	je     400ef7 <phase_1+0x17>
  400ef2:	e8 43 05 00 00       	callq  40143a <explode_bomb>
  # 释放空间,返回
  400ef7:	48 83 c4 08          	add    $0x8,%rsp
  400efb:	c3                   	retq  

strings_not_equal的名字来看,大概都可以猜出它是一个用来比较字符串的函数。它接收的参数是用户的第一个字符串输入,以及常量0x402400。这里的密码很有可能就是0x402400。由于C代码里传字符串一般是数组首地址,所以这个0x402400应该也是一个字符数组的首地址。通过gdb可以查看该地址的数据:

(gdb) p (char*) 0x402400
$2 = 0x402400 "Border relations with Canada have never been better."

phase_1的密码应该就是这句话:“Border relations with Canada have never been better.”。将其作为bomb第一个输入,成功解开第一个炸弹(p≧w≦q)。

phase_2

接下来是phase_2

0000000000400efc <phase_2>:
# 参数:%rdi -- input
  # 将两个callee-saved寄存器值暂存在栈中,分配28字节空间
  400efc:	55                   	push   %rbp
  400efd:	53                   	push   %rbx
  400efe:	48 83 ec 28          	sub    $0x28,%rsp
  # 调用函数read_six_numbers,参数为input和%rsp值(栈帧位置)
  400f02:	48 89 e6             	mov    %rsp,%rsi
  400f05:	e8 52 05 00 00       	callq  40145c <read_six_numbers>
  # 如果(%rsp)等于1,跳到地址400f30;否则炸弹爆炸。从下面的解析中可以看出,read_six_numbers
  # 读取了6个整型数放在以%rsp处开始的内存中。这里把(%rsp)和1比较说明第一个数字必须是1
  400f0a:	83 3c 24 01          	cmpl   $0x1,(%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>
  # ----------------------- 循环 ------------------------------
  # 把第x个数移入%eax
  400f17:	8b 43 fc             	mov    -0x4(%rbx),%eax
  # %eax = %eax * 2;
  400f1a:	01 c0                	add    %eax,%eax
  # %eax和第x+1个数比较,不相等则爆炸。可以看出第x+1个数是%eax
  400f1c:	39 03                	cmp    %eax,(%rbx)
  400f1e:	74 05                	je     400f25 <phase_2+0x29>
  400f20:	e8 15 05 00 00       	callq  40143a <explode_bomb>
  # %rbx现在指向下一个数,如果到了6个数后面的地址,则跳出循环,否则回到地址400f17
  400f25:	48 83 c3 04          	add    $0x4,%rbx
  400f29:	48 39 eb             	cmp    %rbp,%rbx
  400f2c:	75 e9                	jne    400f17 <phase_2+0x1b>
  400f2e:	eb 0c                	jmp    400f3c <phase_2+0x40>
  # ----------------------- 循环 ------------------------------
  # %rbx = %rsp + 4, %rbp = %rsp + 0x18。%rbx是第二个整型数的地址,%rbp是6个整型数后的地址
  400f30:	48 8d 5c 24 04       	lea    0x4(%rsp),%rbx
  400f35:	48 8d 6c 24 18       	lea    0x18(%rsp),%rbp
  # 跳转到地址400f17
  400f3a:	eb db                	jmp    400f17 <phase_2+0x1b>
  # 释放空间,还原callee-saved寄存器值,返回
  400f3c:	48 83 c4 28          	add    $0x28,%rsp
  400f40:	5b                   	pop    %rbx
  400f41:	5d                   	pop    %rbp
  400f42:	c3                   	retq
  
000000000040145c <read_six_numbers>:
# 参数:%rdi -- input, %rsi -- 原%rsp值
  # 分配18字节空间
  40145c:	48 83 ec 18          	sub    $0x18,%rsp
  # %rdx = %rsi
  401460:	48 89 f2             	mov    %rsi,%rdx
  # %rcx = %rsi + 4
  401463:	48 8d 4e 04          	lea    0x4(%rsi),%rcx
  # %rax = %rsi + 0x14
  401467:	48 8d 46 14          	lea    0x14(%rsi),%rax
  # 将%rax保存在栈上。这里应该是作为传参而不是局部变量
  40146b:	48 89 44 24 08       	mov    %rax,0x8(%rsp)
  # %rax = %rsi + 0x10
  401470:	48 8d 46 10          	lea    0x10(%rsi),%rax
  # 将%rax保存在栈上
  401474:	48 89 04 24          	mov    %rax,(%rsp)
  # %r9 = %rsi + 0xc, %r8 = %rsi + 0x8
  401478:	4c 8d 4e 0c          	lea    0xc(%rsi),%r9
  40147c:	4c 8d 46 08          	lea    0x8(%rsi),%r8
  # %esi = $0x4025c3, %eax = 0x0
  401480:	be c3 25 40 00       	mov    $0x4025c3,%esi
  401485:	b8 00 00 00 00       	mov    $0x0,%eax
  # 调用sscanf函数
  40148a:	e8 61 f7 ff ff       	callq  400bf0 <__isoc99_sscanf@plt>
  # sscanf函数的返回值是成功读取的变量个数。如果大于5个,跳到后面;否则引爆炸弹
  40148f:	83 f8 05             	cmp    $0x5,%eax
  401492:	7f 05                	jg     401499 <read_six_numbers+0x3d>
  401494:	e8 a1 ff ff ff       	callq  40143a <explode_bomb>
  # 释放空间,返回
  401499:	48 83 c4 18          	add    $0x18,%rsp
  40149d:	c3                   	retq  

在看phase_2之前,先看函数read_six_numbers。前面一大段指令,都是为了给sscanf函数传参。整理后,可以知道传给sscanf的参数依次是:

// 记原%rsp值为p
sscanf(input, 0x4025c3, p, p+4, p+8, p+12, p+16, p+20);

通过gdb查看地址0x4025c3,可以发现是一个字符串"%d %d %d %d %d %d",这样read_six_numbers的作用也就很清晰了:它从input中读取6个整型数,写入caller函数(phase_2)的栈帧空间中。再回头看phase_2,首先可以判断出第一个数必须是1,之后的数在循环体中判断,依次为上一个数的两倍。因此,phase_2的密码应该是1 2 4 8 16 32。成功拆除第二个炸弹~

phase_3

然后是phase_3

0000000000400f43 <phase_3>:
  # 分配18字节空间
  400f43:	48 83 ec 18          	sub    $0x18,%rsp
  # %rcx = %rsp + 12, %rdx = %rsp + 8
  400f47:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx
  400f4c:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx
  # %esi = 0x4025cf, %eax = 0
  400f51:	be cf 25 40 00       	mov    $0x4025cf,%esi
  400f56:	b8 00 00 00 00       	mov    $0x0,%eax
  # 调用sscanf函数
  400f5b:	e8 90 fc ff ff       	callq  400bf0 <__isoc99_sscanf@plt>
  # 比较%eax和1(有符号数),如果1>%eax则跳到地址400f6a,否则引爆炸弹。%eax为sscanf成功读取变量
  # 数,这里要求至少读了1个
  400f60:	83 f8 01             	cmp    $0x1,%eax
  400f63:	7f 05                	jg     400f6a <phase_3+0x27>
  400f65:	e8 d0 04 00 00       	callq  40143a <explode_bomb>
  # 比较0x8(%rsp)和7(无符号),如果8<=0x8(%rsp)则跳到地址400fad(引爆炸弹)。前面分析到读取了两个
  # 整型数填入了0x8(%rsp)和0xc(%rsp)中,这里要求第一个数<8
  400f6a:	83 7c 24 08 07       	cmpl   $0x7,0x8(%rsp)
  400f6f:	77 3c                	ja     400fad <phase_3+0x6a>
  # %eax = 0x8(%rsp),%eax现在为第一个数
  400f71:	8b 44 24 08          	mov    0x8(%rsp),%eax
  # 跳转到位置 %rax*8+0x402470
  400f75:	ff 24 c5 70 24 40 00 	jmpq   *0x402470(,%rax,8)
  # %eax = 0xcf
  400f7c:	b8 cf 00 00 00       	mov    $0xcf,%eax
  400f81:	eb 3b                	jmp    400fbe <phase_3+0x7b>
  # %eax = 0x2c3
  400f83:	b8 c3 02 00 00       	mov    $0x2c3,%eax
  400f88:	eb 34                	jmp    400fbe <phase_3+0x7b>
  # %eax = 0x100
  400f8a:	b8 00 01 00 00       	mov    $0x100,%eax
  400f8f:	eb 2d                	jmp    400fbe <phase_3+0x7b>
  # %eax = 0x185
  400f91:	b8 85 01 00 00       	mov    $0x185,%eax
  400f96:	eb 26                	jmp    400fbe <phase_3+0x7b>
  # %eax = 0xce
  400f98:	b8 ce 00 00 00       	mov    $0xce,%eax
  400f9d:	eb 1f                	jmp    400fbe <phase_3+0x7b>
  # %eax = 0x2aa
  400f9f:	b8 aa 02 00 00       	mov    $0x2aa,%eax
  400fa4:	eb 18                	jmp    400fbe <phase_3+0x7b>
  # %eax = 0x147
  400fa6:	b8 47 01 00 00       	mov    $0x147,%eax
  400fab:	eb 11                	jmp    400fbe <phase_3+0x7b>
  # 前面有一系列跳转操作,都跳到了地址400fbe,下面这四句除了explode_bomb都不会被执行到
  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
  # 比较0xc(%rsp)和%eax,如果相等则跳转到地址400fc9,否则引爆炸弹
  400fbe:	3b 44 24 0c          	cmp    0xc(%rsp),%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   

400f5b行前,传参给sscanf函数,大致可以看出调用函数的参数为:

// 记%rsp值为p
sscanf(input, 0x4025cf, p+8, p+12);

通过gdb查看地址0x4025cf为字符串"%d %d"。从input中读取两个整型数,存入栈帧内存空间中。设这两个数分别为a1a20x8(%rsp)0xc(%rsp)),程序先是对a1做了检查,要求其小于8,然后跳转到地址a1*8+0x402470上存储的地址上去(indirect jump)。接下来有若干个对a1赋值的操作,赋值完都跳转到了地址400fbe。我猜它的用意可能是让人计算一个适合的a1,跳转到一个适合的mov指令上去,然后进入400fbe。在400fbe这里,比较了a1a2,如果它们不相等则会引爆炸弹。参考了这个答案,我在gdb中

# 在跳转语句那里打点
(gdb) b *0x400f75
Breakpoint 1 at 0x400f75
(gdb) r
# ...
# 观察地址0x402470处的8个字(这里一个字为8字节而非16字节)
(gdb) x/8g 0x402470
0x402470:       0x0000000000400f7c      0x0000000000400fb9
0x402480:       0x0000000000400f83      0x0000000000400f8a
0x402490:       0x0000000000400f91      0x0000000000400f98
0x4024a0:       0x0000000000400f9f      0x0000000000400fa6

0x402470地址上连续的一块都是地址信息。其中0x402470就是一个有效可跳转的简洁地址了,所以让a1=0,跳转到0400f7c,将a1设为0xcf。要让a1a2相等,只需要将a2也设为0xcf(207)。因此第三个密码是0 207(答案可能不唯一,因为能跳到其他地址上去)。

phase_4

第四个phase_4

000000000040100c <phase_4>:
  # 分配18字节空间
  40100c:	48 83 ec 18          	sub    $0x18,%rsp
  # %rcx = %rsp + 0xc, %rdx = %rsp + 0x8
  401010:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx
  401015:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx
  # %esi = 0x4025cf, %eax = 0
  40101a:	be cf 25 40 00       	mov    $0x4025cf,%esi
  40101f:	b8 00 00 00 00       	mov    $0x0,%eax
  # 调用sscanf函数
  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>
  # 如果0x8(%rsp)<=14,跳转到地址40103a。这里要求第一个读取的数<=14,否则炸弹爆炸
  40102e:	83 7c 24 08 0e       	cmpl   $0xe,0x8(%rsp)
  401033:	76 05                	jbe    40103a <phase_4+0x2e>
  401035:	e8 00 04 00 00       	callq  40143a <explode_bomb>
  # %edx = 14, %esi = 0, %edi = a1(第一个数),传参给函数func4
  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       	callq  400fce <func4>
  # 判断func4的返回值是否为0,如果不是则跳到地址401058,引爆炸弹
  40104d:	85 c0                	test   %eax,%eax
  40104f:	75 07                	jne    401058 <phase_4+0x4c>
  # 如果第二个数等于0则跳到尾部,否则炸弹爆炸。这里要求第二个读取的数等于0
  401051:	83 7c 24 0c 00       	cmpl   $0x0,0xc(%rsp)
  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  
  
0000000000400fce <func4>:
# 参数:%edi -- a1, %esi -- 0, %edx -- 14
  # 分配8字节空间
  400fce:	48 83 ec 08          	sub    $0x8,%rsp
  # %eax = %edx - %esi, %ecx = %eax >>> 0x1f
  400fd2:	89 d0                	mov    %edx,%eax
  400fd4:	29 f0                	sub    %esi,%eax
  400fd6:	89 c1                	mov    %eax,%ecx
  400fd8:	c1 e9 1f             	shr    $0x1f,%ecx
  # %eax += %ecx
  400fdb:	01 c8                	add    %ecx,%eax
  # %eax >>= 1
  400fdd:	d1 f8                	sar    %eax
  # %ecx = %rax + %rsi
  400fdf:	8d 0c 30             	lea    (%rax,%rsi,1),%ecx
  # 如果%ecx<=%edi,跳转到地址400ff2
  400fe2:	39 f9                	cmp    %edi,%ecx
  400fe4:	7e 0c                	jle    400ff2 <func4+0x24>
  # %edx = %rcx - 1
  400fe6:	8d 51 ff             	lea    -0x1(%rcx),%edx
  # 递归……
  400fe9:	e8 e0 ff ff ff       	callq  400fce <func4>
  # %eax *= 2
  400fee:	01 c0                	add    %eax,%eax
  # 跳转到末尾
  400ff0:	eb 15                	jmp    401007 <func4+0x39>
  # %eax = 0
  400ff2:	b8 00 00 00 00       	mov    $0x0,%eax
  # 如果%ecx>=%edi,跳转到地址401007(末尾)
  400ff7:	39 f9                	cmp    %edi,%ecx
  400ff9:	7d 0c                	jge    401007 <func4+0x39>
  # %esi = %rcx + 1
  400ffb:	8d 71 01             	lea    0x1(%rcx),%esi
  # 递归
  400ffe:	e8 cb ff ff ff       	callq  400fce <func4>
  # %eax = %rax * 2 + 1
  401003:	8d 44 00 01          	lea    0x1(%rax,%rax,1),%eax
  # 释放空间,返回
  401007:	48 83 c4 08          	add    $0x8,%rsp
  40100b:	c3                   	retq  

phase_4的函数体比较容易懂。首先从input中读取两个整型数,然后将用第一个数a1去调用函数func4,要求函数的返回值为0;然后检查第二个数a2是否也为0。这里难的地方就是要读懂函数func4。它是一个递归函数,将它翻译成高级语言更容易看懂一点:

// rsi和esi看做是等价的,其他也是
# define rsi esi
# define rcx ecx
# define rax eax
int func4(int edi, int esi, int edx) {
    int eax = edx - esi;
    int ecx = (unsigned int)eax >> 31;	// 逻辑右移
    eax += ecx;
    eax >>= 1;
    ecx = rax + rsi;
    if (ecx > edi) {
        edx = rcx - 1;
        eax = func4(edi, esi, edx);
        eax *= 2;
    } else {
        eax = 0;
        if (ecx < edi) {
            esi = rcx + 1;
            eax = func4(edi, esi, edx);
            eax = rax * 2 + 1;
        }
    }
    return eax;
}

// 更精简一些
int func4(int a1, int y, int z) {
    // p是z和y的平均数
    int r = (z - y) >> 1;
    int p = r + y;
    if (p > a1) {
        r = func4(a1, y, p - 1);
        r *= 2;
    } else {
        r = 0;
        if (p < a1) {
            r = func4(a1, p + 1, z);
    		r = r * 2 + 1;
        }
    }
    return r;
}

a1yz的平均数比较,在相等的时候返回0。其他时候,会递归调用func4,修改%rax上的值(即r)。phase_4调用func4使用的参数是0和14,所以令a1=7,即可让func4一步返回0。因此,phase_4的答案是7 0。这个答案不唯一,有兴趣也可以尝试进入递归模式,经过我的尝试a1是1 3 7都可以。第四个炸弹也成功被拆除,Dr.Evil的阴谋即将破灭……

phase_5

倒数第二个炸弹phase_5:

0000000000401062 <phase_5>:
  # 保存callee-saved寄存器值,分配32字节空间
  401062:	53                   	push   %rbx
  401063:	48 83 ec 20          	sub    $0x20,%rsp
  # %rbx = %rdi
  401067:	48 89 fb             	mov    %rdi,%rbx
  # 将canary值移入%rax,再保存到栈上,最后清空%rax。栈上存的canary值用于最后和实际canary值对比,
  # 检查是否有越界操作。phase_5可能创建了一个数组对象,记为arr,从下面的操作来看数组元素大小为1字节
  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
  # 调用函数string_length,从名字看应该是获取字符串长度的函数(看它的汇编代码告诉我是这样)
  40107a:	e8 9c 02 00 00       	callq  40131b <string_length>
  # input字符串的长度如果为6,就跳转到地址4010d2,否则炸弹爆炸
  40107f:	83 f8 06             	cmp    $0x6,%eax
  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>
  # ------------------------- 循环 ---------------------------------------
  # %ecx = *(%rbx + %rax)
  40108b:	0f b6 0c 03          	movzbl (%rbx,%rax,1),%ecx
  # arr[0] = %cl
  40108f:	88 0c 24             	mov    %cl,(%rsp)
  # %rdx = arr[0]
  401092:	48 8b 14 24          	mov    (%rsp),%rdx
  # %edx &= 0xf
  401096:	83 e2 0f             	and    $0xf,%edx
  # %edx = *(%rdx + 0x4024b0)
  401099:	0f b6 92 b0 24 40 00 	movzbl 0x4024b0(%rdx),%edx
  # *(%rsp + %rax + 16) = %dl, %rax++
  4010a0:	88 54 04 10          	mov    %dl,0x10(%rsp,%rax,1)
  4010a4:	48 83 c0 01          	add    $0x1,%rax
  # 如果%rax不等于6,就跳转到地址40108b。这里应该是个循环
  4010a8:	48 83 f8 06          	cmp    $0x6,%rax
  4010ac:	75 dd                	jne    40108b <phase_5+0x29>
  # ------------------------- 循环 ---------------------------------------
  # arr[22] = 0, %esi = 0x40245e, %rdi = %rsp + 16
  4010ae:	c6 44 24 16 00       	movb   $0x0,0x16(%rsp)
  4010b3:	be 5e 24 40 00       	mov    $0x40245e,%esi
  4010b8:	48 8d 7c 24 10       	lea    0x10(%rsp),%rdi
  # 调用函数strings_not_equal。参数为(%rsp + 16)和0x40245e。如果返回值为0则跳转到末尾,否则引爆
  # 炸弹。这里要求地址(%rsp + 16)上的字符串和0x40245e上的字符串相等
  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>
  # %eax = 0,跳转到地址40108b
  4010d2:	b8 00 00 00 00       	mov    $0x0,%eax
  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>
  # 释放空间,还原callee-saved寄存器值,返回
  4010ee:	48 83 c4 20          	add    $0x20,%rsp
  4010f2:	5b                   	pop    %rbx
  4010f3:	c3                   	retq   

phase_5出现了canary和__stack_chk_fail,极有可能是创建了数组对象。从指令来看,数组中的元素大小为1字节,很可能是char类型。在后半部,程序将地址(%rsp + 16)上的数据和地址0x40245e上的数据以字符串的形式进行了对比,以char*形式查看0x40245e的数据,看到是"flyers",同时注意到前面将(%rsp + 22)设为了0,(%rsp + 16)(%rsp + 22)的距离刚好是7字节,不算\0的话刚好和"flyers"一样,所以我觉得phase_5是想让arr[16:]这段字符串为"flyers"phase_5一开始接受一个input字符串,并要求它的长度为6。然后有一段循环,写成高级语言为:

eax = 0;
rbx = rdi;		// 在前面的
do {
    ecx = *(rbx + rax);
    arr[0] = cl;	// 只保留ecx后八位,一个char
    rdx = arr[0];
    edx &= 0xf;
    edx = *(rdx + 0x4024b0);
    arr[rax+16] = dl;
    ++rax;
} while (rax != 6);

// 更简洁一点!--->

for (i = 0, p = input; i < 6; ++i) {
    arr[0] = *(p + i);
    arr[i+16] = *(arr[0] & 0xf + 0x4024b0);
}

这段循环每次读取输入字符串中的一个字符,然后将arr[16:]中对应位置的字符设为*(chr & 0xf + 0x4024b0)。不得不感叹,这个炸弹的设计充满了想象力……我去查看了内存地址0x4024b0的数据,发现是这样的:

(gdb) p (char*) 0x4024b0
$7 = 0x4024b0 <array> "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"

为了让arr[16:]等于"flyers",我们需要让chr&0xf的六个值分别为9 15 14 5 6 7。因此我们的输入各个字符的低四位应该是这些数,答案也不唯一。不过为了方便书写,我还是找了一组字母来作为输入^_^,比如IONEFG。又一个炸弹被拆除,感觉这一整个实验做下来也挺累的啊……

phase_6

最后的炸弹(好长呀)……

00000000004010f4 <phase_6>:
  # 保存callee-saved寄存器值,分配50字节空间
  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
  # %r13 = %rsp, %rsi = %rsp
  401100:	49 89 e5             	mov    %rsp,%r13
  401103:	48 89 e6             	mov    %rsp,%rsi
  # 调用函数read_six_numbers,从input中读取6个整型数到栈上
  401106:	e8 51 03 00 00       	callq  40145c <read_six_numbers>
  # %r14 = %rsp, %r12d = 0
  40110b:	49 89 e6             	mov    %rsp,%r14
  40110e:	41 bc 00 00 00 00    	mov    $0x0,%r12d
  # %rbp = %r13, %eax = *%r13, %eax--
  401114:	4c 89 ed             	mov    %r13,%rbp
  401117:	41 8b 45 00          	mov    0x0(%r13),%eax
  40111b:	83 e8 01             	sub    $0x1,%eax
  # 如果%eax<=5,跳转到地址401128,否则引爆炸弹
  40111e:	83 f8 05             	cmp    $0x5,%eax
  401121:	76 05                	jbe    401128 <phase_6+0x34>
  401123:	e8 12 03 00 00       	callq  40143a <explode_bomb>
  # %r12d++
  401128:	41 83 c4 01          	add    $0x1,%r12d
  # 如果%r12d=6,跳转到地址401153
  40112c:	41 83 fc 06          	cmp    $0x6,%r12d
  401130:	74 21                	je     401153 <phase_6+0x5f>
  # %ebx = %r12d
  401132:	44 89 e3             	mov    %r12d,%ebx
  # %eax = %ebx (sign extended), %eax = *(%rsp+4*%rax)
  401135:	48 63 c3             	movslq %ebx,%rax
  401138:	8b 04 84             	mov    (%rsp,%rax,4),%eax
  # 如果*(%rbp)不等于%eax,跳转到地址401145,否则引爆炸弹
  40113b:	39 45 00             	cmp    %eax,0x0(%rbp)
  40113e:	75 05                	jne    401145 <phase_6+0x51>
  401140:	e8 f5 02 00 00       	callq  40143a <explode_bomb>
  # %ebx++
  401145:	83 c3 01             	add    $0x1,%ebx
  # 如果%ebx<=5,跳转到地址401135
  401148:	83 fb 05             	cmp    $0x5,%ebx
  40114b:	7e e8                	jle    401135 <phase_6+0x41>
  # %r13 += 4,跳转到地址401114
  40114d:	49 83 c5 04          	add    $0x4,%r13
  401151:	eb c1                	jmp    401114 <phase_6+0x20>
  # %rsi = %rsp + 24, %rax = %r14, %ecx = 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
  # %edx = %ecx, %edx -= *%rax, *%rax = %edx, %rax += 4
  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
  # 如果%rsi不等于%rax,跳转到地址401160
  40116a:	48 39 f0             	cmp    %rsi,%rax
  40116d:	75 f1                	jne    401160 <phase_6+0x6c>
  # %esi = 0,跳转到地址401197
  40116f:	be 00 00 00 00       	mov    $0x0,%esi
  401174:	eb 21                	jmp    401197 <phase_6+0xa3>
  # %rdx = *(%rdx + 8), %eax++
  401176:	48 8b 52 08          	mov    0x8(%rdx),%rdx
  40117a:	83 c0 01             	add    $0x1,%eax
  # 如果%ecx不等于%eax,跳转到地址401176,否则跳转到401188
  40117d:	39 c8                	cmp    %ecx,%eax
  40117f:	75 f5                	jne    401176 <phase_6+0x82>
  401181:	eb 05                	jmp    401188 <phase_6+0x94>
  # %edx = 0x6032d0, *(%rsp+%rsi*2+32) = %rdx, %rsi += 4
  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
  # 如果%rsi等于24,跳转到地址4011ab
  401191:	48 83 fe 18          	cmp    $0x18,%rsi
  401195:	74 14                	je     4011ab <phase_6+0xb7>
  # %ecx = *(%rsp + %rsi)
  401197:	8b 0c 34             	mov    (%rsp,%rsi,1),%ecx
  # 如果%ecx<=1,跳转到地址401183
  40119a:	83 f9 01             	cmp    $0x1,%ecx
  40119d:	7e e4                	jle    401183 <phase_6+0x8f>
  # %eax = 1, %edx = 0x6032d0
  40119f:	b8 01 00 00 00       	mov    $0x1,%eax
  4011a4:	ba d0 32 60 00       	mov    $0x6032d0,%edx
  # 跳转到401176
  4011a9:	eb cb                	jmp    401176 <phase_6+0x82>
  # %rbx = *(%rsp + 32), %rax = %rsp + 40, %rsi = %rsp + 80, %rcx = %rbx
  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
  # %rdx = *(%rax), *(%rcx + 8) = %rdx, %rax += 8
  4011bd:	48 8b 10             	mov    (%rax),%rdx
  4011c0:	48 89 51 08          	mov    %rdx,0x8(%rcx)
  4011c4:	48 83 c0 08          	add    $0x8,%rax
  # 如果%rax等于%rsi,跳转到地址4011d2
  4011c8:	48 39 f0             	cmp    %rsi,%rax
  4011cb:	74 05                	je     4011d2 <phase_6+0xde>
  # %rcx = %rdx
  4011cd:	48 89 d1             	mov    %rdx,%rcx
  # 跳转到地址4011bd
  4011d0:	eb eb                	jmp    4011bd <phase_6+0xc9>
  # *(%rdx+8) = 0, %ebp = 5
  4011d2:	48 c7 42 08 00 00 00 	movq   $0x0,0x8(%rdx)
  4011d9:	00 
  4011da:	bd 05 00 00 00       	mov    $0x5,%ebp
  # %rax = *(%rbx + 8), %eax = *%rax
  4011df:	48 8b 43 08          	mov    0x8(%rbx),%rax
  4011e3:	8b 00                	mov    (%rax),%eax
  # 如果*%rbx>=%eax,跳转到地址4011ee,否则引爆炸弹
  4011e5:	39 03                	cmp    %eax,(%rbx)
  4011e7:	7d 05                	jge    4011ee <phase_6+0xfa>
  4011e9:	e8 4c 02 00 00       	callq  40143a <explode_bomb>
  # %rbx = *(%rbx + 8), %ebp--
  4011ee:	48 8b 5b 08          	mov    0x8(%rbx),%rbx
  4011f2:	83 ed 01             	sub    $0x1,%ebp
  # 如果*rbx不等于%eax,跳转到地址4011df
  4011f5:	75 e8                	jne    4011df <phase_6+0xeb>
  # 释放空间,还原callee-saved寄存器值,返回
  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   

因为phase_6的逻辑过于复杂,我觉得最好还是把它翻译成高级语言:

// 记%rsp值为arr. arr[24]表示arr首地址后第24个字节的地方,而不是第24个元素
void phase_6(const char *input) {
    int arr[6];
    r13 = arr, rsi = arr;
    read_six_numbers(input, arr);
    r14 = arr, r12d = 0;
    
    while (true) {
        rbp = r13, eax = *r13;
        eax--;
        if (eax > 5) explode_bomb();
        r12d++;
        if (r12d == 6) break;
        ebx = r12d;
        do {
            eax = ebx;		// sign extended
            eax = arr[rax*4];
            if (*rbp == eax) explode_bomb();
            ebx++;
        } while (ebx <= 5);
        r13 += 4;
    }
    
    rsi = arr + 24, rax = r14, ecx = 7;
    do {
    	edx = ecx, edx -= *rax, *rax = edx, rax += 4;
    } while (rsi != rax);
    
    esi = 0;
    goto L1;
L4:
    do {
        rdx = *(rdx+8), eax++;
    } while (ecx != eax);
    goto L5;
L2:
    edx = 0x6032d0;
L5:
    arr[rsi*2+32] = rdx, rsi += 4;
    if (rsi == 24) goto L3;
L1:
    ecx = arr[rsi];
    if (ecx <= 1) goto L2;
    eax = 1, edx = 0x6032d0;
    goto L4;
L3:
    rbx = arr[32], rax = arr + 40, rsi = arr + 80, rcx = rbx;
    while (true) {
        rdx = *rax, *(rcx + 8) = rdx, rax += 8;
        if (rax == rsi) break;
        rcx = rdx;
    }
    
    *(rdx+8) = 0, ebp = 5;
    do {
        rax = *(rbx+8), eax = *rax;
        if (*rbx < eax) explode_bomb();
        rbx = *(rbx + 8), ebp--;
    } while (*rbx != eax);
}

完全没有办法懂啊……继续精简(我已经尽我所能了,如果还是有bug真对不起……):

void phase_6(const char *input) {
    int arr[6];
    int *p1 = arr;
    read_six_numbers(input, arr);

    int i = 0;
    while (true) {
        // 输入的6个数都不大于6
        if (*p1 > 6) explode_bomb();
        i++;
        if (i == 6) break;
        // 输入的6个数各不相同
        for (int j = i; j <= 5; ++j) {
            if (*p1 == arr[j]) explode_bomb();
        }
        p1++;
    }
    
    // 每个输入的数都变为7-each
    for (int &each: arr) {
        each = 7 - each;
    }
    
    int s = 0;
    goto L1;

L4:
    int u = 1;
    node *rdx = 0x6032d0;       // 第一个节点
    do {
        rdx = rdx->next;
        u++;
    } while (arr[s] != u);
    goto L5;
L2:
    node *rdx = 0x6032d0;
L5:
    arr_node[s] = rdx;
    s++;
    if (s == 6) goto L3;
L1:
    // rsp+32到rsp+80有东西,是一堆大小为8字节的变量,应该是node*
    node *arr_node[6];
    // 这里根据arr[s]的值跳转:<=1时到L2,否则去L4
    // L2+L5和L4+L5的作用都是把arr_node[s]设为第arr[s]个节点,只是L4有链表找节点的操作
    // 每次经过L5后s++
    if (arr[s] <= 1) goto L2;
    goto L4;

L3:
    // 把六个节点按arr的顺序串起来
    node *current = arr_node[0];
    for (int i = 1; i < 6; ++i) {
        current->next = *arr_node[i];
        current = *arr_node[i];
    }
    arr[5]->next = 0;

    // 要求6个节点值递减。直到相邻两个节点值相同才会停下
    // 从指令来看,每个节点占了16字节的空间,其中后面8个字节是next指针
    // 前面的8字节只使用了后半部分,可能作为一个4字节整型数
    node *np1, *np = arr_node[0];
    do {
        np1 = np->next;
        if (np->val < np1->val) explode_bomb();
        np = np->next;
    } while (np->val != np1->val);
}

这些应该好懂多了……代码中存在一个神秘地址,在大约0x401174地址处打断点,到达后在gdb中查看内存信息:

(gdb) x/12g 0x6032d0
0x6032d0 <node1>:       0x000000010000014c      0x00000000006032e0
0x6032e0 <node2>:       0x00000002000000a8      0x00000000006032f0
0x6032f0 <node3>:       0x000000030000039c      0x0000000000603300
0x603300 <node4>:       0x00000004000002b3      0x0000000000603310
0x603310 <node5>:       0x00000005000001dd      0x0000000000603320
0x603320 <node6>:       0x00000006000001bb      0x0000000000000000

非常像是一个链表……每个node的第一个字节应该是自己的数据,第二个字节应该是下一个节点,下一个节点的地址刚好也和实际的地址对上了。总共有6个节点。phase_6前半部分读取6个整型数,然后将它们变成7减自己,再把这6个节点按输入数排列。最后会检查这6个重排的节点的数据(或许是个int)是否递减。从gdb显示的节点信息来看,6个节点存储的信息分别是0x14c0x0a80x39c……它们按从大到小排的顺序是:3 4 5 6 1 2。由于代码中将输入的数做了变化(7减自己),所以输入的数应该是4 3 2 1 6 5。这就是正确答案(p≧w≦q),至此6个炸弹都已成功拆除,我们拯救了Lab……

总结

这六个炸弹拆下来确实挺累人的,CMU作业真多……在汇编文件里我发现最后还有一个secret_phase(算是隐藏彩蛋吗???),但我现在已经没有力气解谜了……留给以后吧,再见^_^。