BombLab实验记录

参考

跳转指令

附一张跳转指令表格,方便直接查询:

jX条件描述
jmp1无条件
jeZF相等/零
jne~ZF不等/非零
jsSF负数
jns~SF非负数
jg~ (SF^OF) & ~ZF大于(有符号数)
jge~ (SF^OF)大于或等于(有符号数)
jl(SF^OF)小于 (有符号数)
jle(SF^OF) | ZF小于或等于(有符号数)
ja~CF & ~ZF高于(无符号数)
jbCF低于(无符号数)

一、实验目的

本实验目的是教会学生能够理解汇编语言,使学生学会如何使用调试器(debugger)并在这个经典的实验中感受到实验的乐趣。

二、报告要求

本报告要求学生写出实验中炸弹拆除的推理过程。

三、炸弹拆除

phase_1

拆弹分析:

phase_1处设置断点,使用gdbdisassemble命令反汇编phase_1,结果如下:

Dump of assembler code for function phase_1:
   0x0000000000400e6d <+0>:	sub    $0x8,%rsp
   0x0000000000400e71 <+4>:	mov    $0x4023d0,%esi
   `0x0000000000400e76 <+9>:	callq  0x40134a <strings_not_equal>`
   `0x0000000000400e7b <+14>:	test   %eax,%eax`
   0x0000000000400e7d <+16>:	jne    0x400e84 <phase_1+23>
   0x0000000000400e7f <+18>:	add    $0x8,%rsp
   0x0000000000400e83 <+22>:	retq   
   0x0000000000400e84 <+23>:	callq  0x401447 <explode_bomb>
   0x0000000000400e89 <+28>:	jmp    0x400e7f <phase_1+18>
End of assembler dump.

可以看到,在0x400e76处,也即第3行代码,phase_1调用了函数strings_not_equal,接着就测试%eax是否为0,是则跳转至0x400e84调用explode_bomb(炸弹爆炸),否则将释放空间返回,因此可以判定这是一个判断两个字符串是否相等的过程。
查看strings_not_equal调用前%rsi的设置并结合main函数中关于phase_1处的%rdi设置:

   0x0000000000400e71 <+4>:	mov    $0x4023d0,%esi
   0x0000000000400e76 <+9>:	callq  0x40134a <strings_not_equal>
  400d74:	e8 2f 07 00 00       	callq  4014a8 <read_line>
  400d79:	48 89 c7             	mov    %rax,%rdi
  400d7c:	e8 ec 00 00 00       	callq  400e6d <phase_1>
  400d81:	e8 50 08 00 00       	callq  4015d6 <phase_defused>

可知strings_not_equal函数中比较的两个字符串参数%rdi%rsi分别是read_line的返回结果和内存地址0x4023d0
由此推测0x4023d0极可能为目标字符串的起始地址,利用x/s指令查看0x4023d0对应的字符串得到:Slave,thou hast slain me. Villain, take my purse.
经验证,这确实是phase_1的答案。

phase_2

拆弹分析:

phase_2处设置断点,使用gdbdisassemble命令反汇编phase_2,结果如下:

Dump of assembler code for function phase_2:
   0x0000000000400e8b <+0>:	push   %rbx
   0x0000000000400e8c <+1>:	sub    $0x20,%rsp
   0x0000000000400e90 <+5>:	mov    %fs:0x28,%rax
   0x0000000000400e99 <+14>:	mov    %rax,0x18(%rsp)
   0x0000000000400e9e <+19>:	xor    %eax,%eax
   0x0000000000400ea0 <+21>:	mov    %rsp,%rsi
   `0x0000000000400ea3 <+24>:	callq  0x401469 <read_six_numbers>`
   0x0000000000400ea8 <+29>:	cmpl   $0x0,(%rsp)
   0x0000000000400eac <+33>:	js     0x400eb5 <phase_2+42>
   0x0000000000400eae <+35>:	mov    $0x1,%ebx
   0x0000000000400eb3 <+40>:	jmp    0x400ec6 <phase_2+59>
   0x0000000000400eb5 <+42>:	callq  0x401447 <explode_bomb>
   0x0000000000400eba <+47>:	jmp    0x400eae <phase_2+35>
   0x0000000000400ebc <+49>:	add    $0x1,%rbx
   0x0000000000400ec0 <+53>:	cmp    $0x6,%rbx
   0x0000000000400ec4 <+57>:	je     0x400ed8 <phase_2+77>
   0x0000000000400ec6 <+59>:	mov    %ebx,%eax
   0x0000000000400ec8 <+61>:	add    -0x4(%rsp,%rbx,4),%eax
   0x0000000000400ecc <+65>:	cmp    %eax,(%rsp,%rbx,4)
   0x0000000000400ecf <+68>:	je     0x400ebc <phase_2+49>
   0x0000000000400ed1 <+70>:	callq  0x401447 <explode_bomb>
   0x0000000000400ed6 <+75>:	jmp    0x400ebc <phase_2+49>
   0x0000000000400ed8 <+77>:	mov    0x18(%rsp),%rax
   0x0000000000400edd <+82>:	xor    %fs:0x28,%rax
   0x0000000000400ee6 <+91>:	jne    0x400eee <phase_2+99>
   0x0000000000400ee8 <+93>:	add    $0x20,%rsp
   0x0000000000400eec <+97>:	pop    %rbx
   0x0000000000400eed <+98>:	retq   
   0x0000000000400eee <+99>:	callq  0x400b00 <__stack_chk_fail@plt>
End of assembler dump.

(%fs:0x28相关代码是防止栈溢出的安全保护)
在第七行代码中phase_2调用了函数read_six_numbers,根据函数名称我们不难推断出这个函数的作用是读入6个数(在我仔细查看了read_six_numbers后,发现事实确实如此),在查看了read_six_numbers后,我发现这六个数是按照每四个字节的方式存储的。
另外read_six_numbers中参数寄存器与栈空间之间的关系,最后分析可以得出在返回phase_2后,所输入的6个数字所在位置应该依次为 %rsp, %rsp+0x4, %rsp+0x8, %rsp+0xc,%rsp+0x10,%rsp+0x14

继续查看phase_2

   0x0000000000400ea8 <+29>:	cmpl   $0x0,(%rsp)
   0x0000000000400eac <+33>:	js     0x400eb5 <phase_2+42>
   ......
   0x0000000000400eb5 <+42>:	callq  0x401447 <explode_bomb>

这里phase_2(%rsp)0进行比较,若为负 则跳转调用explode_bomb,说明第一个数不能为负数,这里我们记第一个数为a_0 (a_0>=0).
继续查看代码,通过第一轮比较后,%rbx置为0x1

   0x0000000000400eae <+35>:	mov    $0x1,%ebx
   0x0000000000400eb3 <+40>:	jmp    0x400ec6 <phase_2+59>

所以下列操作为比较(%rsp)+1,(%rsp+0x4)是否相等,相等则跳转,否则引爆,因此可以推出a_1 = a_0+1

   0x0000000000400ec6 <+59>:	mov    %ebx,%eax
   0x0000000000400ec8 <+61>:	add    -0x4(%rsp,%rbx,4),%eax
   0x0000000000400ecc <+65>:	cmp    %eax,(%rsp,%rbx,4)
   0x0000000000400ecf <+68>:	je     0x400ebc <phase_2+49>
   0x0000000000400ed1 <+70>:	callq  0x401447 <explode_bomb>

第二次比较通过后,%rxb累加,比较a_n+1a_n + n+1 (n<5)

   0x0000000000400ebc <+49>:	add    $0x1,%rbx
   0x0000000000400ec0 <+53>:	cmp    $0x6,%rbx
   0x0000000000400ec4 <+57>:	je     0x400ed8 <phase_2+77>
   0x0000000000400ec6 <+59>:	mov    %ebx,%eax
   0x0000000000400ec8 <+61>:	add    -0x4(%rsp,%rbx,4),%eax
   0x0000000000400ecc <+65>:	cmp    %eax,(%rsp,%rbx,4)
   0x0000000000400ecf <+68>:	je     0x400ebc <phase_2+49>
   0x0000000000400ed1 <+70>:	callq  0x401447 <explode_bomb>

通过上述推导过程继续可以得到这六个数的关系是:
a_0, a_1 = a_0+1, a_2= a_1+2 = a_0+3, a_3= a_2+3 = a_0+6, a_4= a_3+4 = a_0+10, a_5= a_4+5 = a_0+15, a_0>=0
因此我取a_0=0,得到一组答案为0, 1, 3, 6, 10, 15

phase_3

拆弹分析:

phase_3处设置断点,使用gdbdisassemble命令反汇编phase_3,结果如下:

Dump of assembler code for function phase_3:
   0x0000000000400ef3 <+0>:	sub    $0x18,%rsp
   0x0000000000400ef7 <+4>:	mov    %fs:0x28,%rax
   0x0000000000400f00 <+13>:	mov    %rax,0x8(%rsp)
   0x0000000000400f05 <+18>:	xor    %eax,%eax
   0x0000000000400f07 <+20>:	lea    0x4(%rsp),%rcx
   0x0000000000400f0c <+25>:	mov    %rsp,%rdx
   0x0000000000400f0f <+28>:	mov    $0x4025cf,%esi
   0x0000000000400f14 <+33>:	callq  0x400ba0 <__isoc99_sscanf@plt>`
   0x0000000000400f19 <+38>:	cmp    $0x1,%eax
   0x0000000000400f1c <+41>:	jle    0x400f2e <phase_3+59>
   0x0000000000400f1e <+43>:	cmpl   $0x7,(%rsp)
   0x0000000000400f22 <+47>:	ja     0x400f66 <phase_3+115>
   0x0000000000400f24 <+49>:	mov    (%rsp),%eax
   0x0000000000400f27 <+52>:	jmpq   *0x402440(,%rax,8)
   0x0000000000400f2e <+59>:	callq  0x401447 <explode_bomb>
   0x0000000000400f33 <+64>:	jmp    0x400f1e <phase_3+43>
   0x0000000000400f35 <+66>:	mov    $0x235,%eax
   0x0000000000400f3a <+71>:	jmp    0x400f77 <phase_3+132>
   0x0000000000400f3c <+73>:	mov    $0x1a7,%eax
   0x0000000000400f41 <+78>:	jmp    0x400f77 <phase_3+132>
   0x0000000000400f43 <+80>:	mov    $0x22b,%eax
   0x0000000000400f48 <+85>:	jmp    0x400f77 <phase_3+132>
   0x0000000000400f4a <+87>:	mov    $0x6c,%eax
   0x0000000000400f4f <+92>:	jmp    0x400f77 <phase_3+132>
   0x0000000000400f51 <+94>:	mov    $0x2f1,%eax
   0x0000000000400f56 <+99>:	jmp    0x400f77 <phase_3+132>
   0x0000000000400f58 <+101>:	mov    $0x3e,%eax
   0x0000000000400f5d <+106>:	jmp    0x400f77 <phase_3+132>
   0x0000000000400f5f <+108>:	mov    $0x248,%eax
   0x0000000000400f64 <+113>:	jmp    0x400f77 <phase_3+132>
   0x0000000000400f66 <+115>:	callq  0x401447 <explode_bomb>
   0x0000000000400f6b <+120>:	mov    $0x0,%eax
   0x0000000000400f70 <+125>:	jmp    0x400f77 <phase_3+132>
   0x0000000000400f72 <+127>:	mov    $0x121,%eax
   0x0000000000400f77 <+132>:	cmp    %eax,0x4(%rsp)
   0x0000000000400f7b <+136>:	je     0x400f82 <phase_3+143>
   0x0000000000400f7d <+138>:	callq  0x401447 <explode_bomb>
   0x0000000000400f82 <+143>:	mov    0x8(%rsp),%rax
   0x0000000000400f87 <+148>:	xor    %fs:0x28,%rax
   0x0000000000400f90 <+157>:	jne    0x400f97 <phase_3+164>
   0x0000000000400f92 <+159>:	add    $0x18,%rsp
   0x0000000000400f96 <+163>:	retq   
   0x0000000000400f97 <+164>:	callq  0x400b00 <__stack_chk_fail@plt>
End of assembler dump.

从以下这部分我们发现phase_3调用了scanf读入数据

   0x0000000000400f0f <+28>:	mov    $0x4025cf,%esi
   0x0000000000400f14 <+33>:	callq  0x400ba0 <__isoc99_sscanf@plt>`
   0x0000000000400f19 <+38>:	cmp    $0x1,%eax
   0x0000000000400f1c <+41>:	jle    0x400f2e <phase_3+59>
   0x0000000000400f1e <+43>:	cmpl   $0x7,(%rsp)
   0x0000000000400f22 <+47>:	ja     0x400f66 <phase_3+115>
   ......
   0x0000000000400f2e <+59>:	callq  0x401447 <explode_bomb>

我们查看%esi可以看到:

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

因此可以知道scanf读入了两个数(记作n_1n_2),所以其下两条语句用于检查数据读入是否正确(scanf返回值为成功读入的数据项数,正确读入则应返回2,若出现上述代码中的小于等于1的情况表示读入不正确),否则爆炸。

   0x0000000000400f1e <+43>:	cmpl   $0x7,(%rsp)
   0x0000000000400f22 <+47>:	ja     0x400f66 <phase_3+115>
   0x0000000000400f24 <+49>:	mov    (%rsp),%eax
   ......
   0x0000000000400f66 <+115>:	callq  0x401447 <explode_bomb>

从上述代码可以发现phase_3要求n_1不能超过 7 并且比较方式(ja)为无符号数比较,因此可以得知 0 <= n_1 <=7

   0x0000000000400f24 <+49>:	mov    (%rsp),%eax
   0x0000000000400f27 <+52>:	jmpq   *0x402440(,%rax,8)

接下来,phase_3又将n_1保存到了%rax中,并间接跳转至了0x402440+8*(%rax)也即0x402440+8*n_1处。因此在这里我们不妨取n_1=2,则phase_3将跳转至0x402450处,查看内存0x402450处的内容,如下:

(gdb) x/x 0x402450
0x402450:	0x00400f3c

phase_3中相应代码段如下:

   0x0000000000400f3c <+73>:	mov    $0x1a7,%eax
   0x0000000000400f41 <+78>:	jmp    0x400f77 <phase_3+132>
   ......
   0x0000000000400f77 <+132>:	cmp    %eax,0x4(%rsp)
   0x0000000000400f7b <+136>:	je     0x400f82 <phase_3+143>
   0x0000000000400f7d <+138>:	callq  0x401447 <explode_bomb>
   0x0000000000400f82 <+143>:	mov    0x8(%rsp),%rax
   0x0000000000400f87 <+148>:	xor    %fs:0x28,%rax
   0x0000000000400f90 <+157>:	jne    0x400f97 <phase_3+164>
   0x0000000000400f92 <+159>:	add    $0x18,%rsp
   0x0000000000400f96 <+163>:	retq   
   0x0000000000400f97 <+164>:	callq  0x400b00 <__stack_chk_fail@plt>

由此可知n_2应当等于0x1a7,也即423。
所以我们可以得出phase_3的一个解为2 423

phase_4

拆弹分析:

phase_4处设置断点,使用gdbdisassemble命令反汇编phase_4,结果如下:

Dump of assembler code for function phase_4:
   0x0000000000400fdb <+0>:	sub    $0x18,%rsp
   0x0000000000400fdf <+4>:	mov    %fs:0x28,%rax
   0x0000000000400fe8 <+13>:	mov    %rax,0x8(%rsp)
   0x0000000000400fed <+18>:	xor    %eax,%eax
   0x0000000000400fef <+20>:	lea    0x4(%rsp),%rcx
   0x0000000000400ff4 <+25>:	mov    %rsp,%rdx
   0x0000000000400ff7 <+28>:	mov    $0x4025cf,%esi
   0x0000000000400ffc <+33>:	callq  0x400ba0 <__isoc99_sscanf@plt>
   0x0000000000401001 <+38>:	cmp    $0x2,%eax
   0x0000000000401004 <+41>:	jne    0x40100c <phase_4+49>
   0x0000000000401006 <+43>:	cmpl   $0xe,(%rsp)
   0x000000000040100a <+47>:	jbe    0x401011 <phase_4+54>
   0x000000000040100c <+49>:	callq  0x401447 <explode_bomb>
   0x0000000000401011 <+54>:	mov    $0xe,%edx
   0x0000000000401016 <+59>:	mov    $0x0,%esi
   0x000000000040101b <+64>:	mov    (%rsp),%edi
   0x000000000040101e <+67>:	callq  0x400f9c <func4>
   0x0000000000401023 <+72>:	cmp    $0x3,%eax			/*%eax = 3*/
   0x0000000000401026 <+75>:	jne    0x40102f <phase_4+84>
   0x0000000000401028 <+77>:	cmpl   $0x3,0x4(%rsp)			/*n_2=3*/
   0x000000000040102d <+82>:	je     0x401034 <phase_4+89>
   0x000000000040102f <+84>:	callq  0x401447 <explode_bomb>
   0x0000000000401034 <+89>:	mov    0x8(%rsp),%rax
   0x0000000000401039 <+94>:	xor    %fs:0x28,%rax
   0x0000000000401042 <+103>:	jne    0x401049 <phase_4+110>
   0x0000000000401044 <+105>:	add    $0x18,%rsp
   0x0000000000401048 <+109>:	retq   
   0x0000000000401049 <+110>:	callq  0x400b00 <__stack_chk_fail@plt>
End of assembler dump.

可以发现,phase_4phase_3类似,也是读入两个数字,并检查是否输入是否正确匹配:

   0x0000000000400ff7 <+28>:	mov    $0x4025cf,%esi
   0x0000000000400ffc <+33>:	callq  0x400ba0 <__isoc99_sscanf@plt>
   0x0000000000401001 <+38>:	cmp    $0x2,%eax
   0x0000000000401004 <+41>:	jne    0x40100c <phase_4+49>
   ......
   0x000000000040100c <+49>:	callq  0x401447 <explode_bomb>

为了便于描述,我将上述读入的两个数记作n_1n_2
接着分析下一段代码:

   0x0000000000401006 <+43>:	cmpl   $0xe,(%rsp)
   0x000000000040100a <+47>:	jbe    0x401011 <phase_4+54>  /*0<=n_1<=14*/
   0x000000000040100c <+49>:	callq  0x401447 <explode_bomb>
   0x0000000000401011 <+54>:	mov    $0xe,%edx
   0x0000000000401016 <+59>:	mov    $0x0,%esi
   0x000000000040101b <+64>:	mov    (%rsp),%edi
   0x000000000040101e <+67>:	callq  0x400f9c <func4>
   0x0000000000401023 <+72>:	cmp    $0x3,%eax		/ *%eax=3 */
   0x0000000000401026 <+75>:	jne    0x40102f <phase_4+84>
   0x0000000000401028 <+77>:	cmpl   $0x3,0x4(%rsp)		/* n_2=3 */
   0x000000000040102d <+82>:	je     0x401034 <phase_4+89>
   0x000000000040102f <+84>:	callq  0x401447 <explode_bomb>

<+43><+54>可以推断出phase_4要求0<=n_1<=14(因为采用的无符号比较,所以大于0);之后phase_4在做好相应的参数准备工作后,就调用了函数func4;继续往下看可以发现phase_4要求func4的返回值应当为3,否则爆炸;同样的,也可以推出phase_4要求n_2=3
接下来我们进一步查看func4

   0x0000000000401011 <+54>:	mov    $0xe,%edx
   0x0000000000401016 <+59>:	mov    $0x0,%esi
   0x000000000040101b <+64>:	mov    (%rsp),%edi
   0x000000000040101e <+67>:	callq  0x400f9c <func4>

在调用func4前,phase_4将%edx的值置为0xe(即 14),并传入参数n_10
func4的反汇编结果如下:

Dump of assembler code for function func4:
   0x0000000000400f9c <+0>:	sub    $0x8,%rsp
   0x0000000000400fa0 <+4>:	mov    %edx,%eax
   0x0000000000400fa2 <+6>:	sub    %esi,%eax
   0x0000000000400fa4 <+8>:	mov    %eax,%ecx
   0x0000000000400fa6 <+10>:	shr    $0x1f,%ecx
   0x0000000000400fa9 <+13>:	add    %eax,%ecx
   0x0000000000400fab <+15>:	sar    %ecx
   0x0000000000400fad <+17>:	add    %esi,%ecx
   0x0000000000400faf <+19>:	cmp    %edi,%ecx
   0x0000000000400fb1 <+21>:	jg     0x400fc1 <func4+37>
   0x0000000000400fb3 <+23>:	mov    $0x0,%eax
   0x0000000000400fb8 <+28>:	cmp    %edi,%ecx
   0x0000000000400fba <+30>:	jl     0x400fcd <func4+49>
   0x0000000000400fbc <+32>:	add    $0x8,%rsp
   0x0000000000400fc0 <+36>:	retq   
   0x0000000000400fc1 <+37>:	lea    -0x1(%rcx),%edx
   0x0000000000400fc4 <+40>:	callq  0x400f9c <func4>
   0x0000000000400fc9 <+45>:	add    %eax,%eax
   0x0000000000400fcb <+47>:	jmp    0x400fbc <func4+32>
   0x0000000000400fcd <+49>:	lea    0x1(%rcx),%esi
   0x0000000000400fd0 <+52>:	callq  0x400f9c <func4>
   0x0000000000400fd5 <+57>:	lea    0x1(%rax,%rax,1),%eax
   0x0000000000400fd9 <+61>:	jmp    0x400fbc <func4+32>
End of assembler dump.

不难看出func4是个递归函数,为了便于阅读理解和计算,将其转换成C语言,代码如下:

int func4 ( int edi, int esi, int edx )//初始值:edi=n_1,esi=0x0,edx=0xe
{// 返回值为eax
    eax = edx - esi;  
    ecx = (eax + (eax >> 31(shr))) >> 1(sar);  
    ecx = ecx + esi;  
    if(edi < ecx) 
		return 2*func4(edi, esi, ecx - 1);
    else if (edi > ecx)
        return 2 * func4(edi, ecx + 1, edx) + 1; 
    else
        return 0;
}

phase_4要求在传入%edi=n_1,%esi=0,%edx=14的情况下,func4的返回值为3,我们可以计算知道需要两次达成edi > ecx条件,具体计算过程这里不赘述,最后我算出了n_1的两种可能取值12(4次调用func4edi(即n_1)和ecx比较结果依次为>><=)和13(3次调用func4ediecx比较结果依次为>>=)。
(tips:如果不想计算,其实n_1从1遍历到14也可以得出结果)
所以最后得到的答案可以为12 3或者13 3

phase_5

拆弹分析:

phase_5处设置断点,使用gdbdisassemble命令反汇编phase_5,结果如下:

Dump of assembler code for function phase_5:
   0x000000000040104e <+0>:	sub    $0x18,%rsp
   0x0000000000401052 <+4>:	mov    %fs:0x28,%rax
   0x000000000040105b <+13>:	mov    %rax,0x8(%rsp)
   0x0000000000401060 <+18>:	xor    %eax,%eax
   0x0000000000401062 <+20>:	lea    0x4(%rsp),%rcx
   0x0000000000401067 <+25>:	mov    %rsp,%rdx
   0x000000000040106a <+28>:	mov    $0x4025cf,%esi
   0x000000000040106f <+33>:	callq  0x400ba0 <__isoc99_sscanf@plt>
   0x0000000000401074 <+38>:	cmp    $0x1,%eax
   0x0000000000401077 <+41>:	jle    0x4010d0 <phase_5+130>
   0x0000000000401079 <+43>:	mov    (%rsp),%eax
   0x000000000040107c <+46>:	and    $0xf,%eax
   0x000000000040107f <+49>:	mov    %eax,(%rsp)
   0x0000000000401082 <+52>:	cmp    $0xf,%eax
   0x0000000000401085 <+55>:	je     0x4010b6 <phase_5+104>
   0x0000000000401087 <+57>:	mov    $0x0,%ecx
   0x000000000040108c <+62>:	mov    $0x0,%edx
   0x0000000000401091 <+67>:	add    $0x1,%edx
   0x0000000000401094 <+70>:	cltq   
   0x0000000000401096 <+72>:	mov    0x402480(,%rax,4),%eax
   0x000000000040109d <+79>:	add    %eax,%ecx
   0x000000000040109f <+81>:	cmp    $0xf,%eax
   0x00000000004010a2 <+84>:	jne    0x401091 <phase_5+67>
   0x00000000004010a4 <+86>:	movl   $0xf,(%rsp)
   0x00000000004010ab <+93>:	cmp    $0x3,%edx
   0x00000000004010ae <+96>:	jne    0x4010b6 <phase_5+104>
   0x00000000004010b0 <+98>:	cmp    %ecx,0x4(%rsp)
   0x00000000004010b4 <+102>:	je     0x4010bb <phase_5+109>
   0x00000000004010b6 <+104>:	callq  0x401447 <explode_bomb>
   0x00000000004010bb <+109>:	mov    0x8(%rsp),%rax
   0x00000000004010c0 <+114>:	xor    %fs:0x28,%rax
   0x00000000004010c9 <+123>:	jne    0x4010d7 <phase_5+137>
   0x00000000004010cb <+125>:	add    $0x18,%rsp
   0x00000000004010cf <+129>:	retq   
   0x00000000004010d0 <+130>:	callq  0x401447 <explode_bomb>
   0x00000000004010d5 <+135>:	jmp    0x401079 <phase_5+43>
   0x00000000004010d7 <+137>:	callq  0x400b00 <__stack_chk_fail@plt>
End of assembler dump.

看到代码前段,与phase_4相同,读入两个数,并检查输入。这里同样记作n_1,n_2
继续往下看:

   0x0000000000401079 <+43>:	mov    (%rsp),%eax
   0x000000000040107c <+46>:	and    $0xf,%eax
   0x000000000040107f <+49>:	mov    %eax,(%rsp)
   0x0000000000401082 <+52>:	cmp    $0xf,%eax
   0x0000000000401085 <+55>:	je     0x4010b6 <phase_5+104>
   ......
   0x00000000004010b6 <+104>:	callq  0x401447 <explode_bomb>

这里实际上就是对于第一个数n_1的高60位进行了位清除操作,且要求其低四位不能为全1,换而言之,要求对n_1截断至低4位后,其有效值为0x0到0xf之间,并将其复制到寄存器%rax中。为了方便起见,我们不妨直接限制n_1为0到14的数,这样截断之后仍然为n_1
继续看下一段代码:

   0x0000000000401087 <+57>:	mov    $0x0,%ecx
   0x000000000040108c <+62>:	mov    $0x0,%edx
   0x0000000000401091 <+67>:	add    $0x1,%edx
   0x0000000000401094 <+70>:	cltq   
   0x0000000000401096 <+72>:	mov    0x402480(,%rax,4),%eax
   0x000000000040109d <+79>:	add    %eax,%ecx
   0x000000000040109f <+81>:	cmp    $0xf,%eax
   0x00000000004010a2 <+84>:	jne    0x401091 <phase_5+67>

这段代码实际上构建了一个循环体,将其转换为C语言表述如下:

int ecx = 0;
int edx = 0;
int eax = n_1;
do{
	edx += 1;
	eax = *(0x402480 + 4*eax);
	ecx += eax;
}while(eax!=0xf)

我们查看退出循环后的汇编代码(如下),可知退出循环后%edx=3,且n_2=%ecx,也就是说,n_1可以使上述循环体循环三次,在第三次的时候跳转到使%rax0xf内存位置,退出循环。

   0x00000000004010a4 <+86>:	movl   $0xf,(%rsp)
   0x00000000004010ab <+93>:	cmp    $0x3,%edx		/* %edx=3 */
   0x00000000004010ae <+96>:	jne    0x4010b6 <phase_5+104>
   0x00000000004010b0 <+98>:	cmp    %ecx,0x4(%rsp)		/* n_2=%ecx */
   0x00000000004010b4 <+102>:	je     0x4010bb <phase_5+109>
   0x00000000004010b6 <+104>:	callq  0x401447 <explode_bomb>
   0x00000000004010bb <+109>:	mov    0x8(%rsp),%rax

为了能够模拟循环体中具体运行过程,我们需要了解其所有可能跳转的位置的内容,也即查看以0x402480为首地址的15个字节:

(gdb) x/15x 0x402480
0x402480 <array.3415>:	0x0000000a	0x00000002	0x0000000e	0x00000007
0x402490 <array.3415+16>:	0x00000008	0x0000000c	0x0000000f	0x0000000b
0x4024a0 <array.3415+32>:	0x00000000	0x00000004	0x00000001	0x0000000d
0x4024b0 <array.3415+48>:	0x00000003	0x00000009	0x00000006

可以看出上述地址空间内存储的是一个以0x402480为首地址的数组(记作a),每个元素占4个字节,每次跳转至0x402480 + 4*eax也即取出a[eax]
因此我们将这一部分合并到上述循环体中,写成C语言就是:

int a[15] = {10, 2, 14, 7, 8, 12, 15, 11, 0, 4, 1, 13, 3, 9, 6};
int ecx = 0;
int edx = 0;
int eax = n_1;
do{
	edx += 1;
	eax = a[eax];
	ecx += eax;
}while(eax!=15)

这里我们可以采用倒推的方法得到最终结果,记第一轮循环时的变量初始值为ecx_0 = 0,eax_0 = n_1,经过n次循环后变量为ecx_n,eax_n,那么ecx_n= ecx_(n-1) + eax_n,具体推理过程如下:

  1. rdx=3时,eax_3=15 ,ecx_3 = ecx_2+15;因此a[eax_2]=15,所以eax_2=6
  2. rdx=2时,eax_2=6 ,ecx_2 = ecx_1+6;因此a[eax_1]=6,所以eax_1=14
  3. rdx=1时,eax_1=14 ,ecx_1 = 14;因此a[eax_0]=14,所以eax_0=2

最终得到 n_1=eax_0=2,n_2= ecx_3 = 0+14+6+15 =35。(当然,n_1可以为16x+2x为自然数)

phase_6

拆弹分析:

phase_6处设置断点,使用gdbdisassemble命令反汇编phase_6,结果如下:

Dump of assembler code for function phase_6:
   0x00000000004010dc <+0>:	push   %r14
   0x00000000004010de <+2>:	push   %r13
   0x00000000004010e0 <+4>:	push   %r12
   0x00000000004010e2 <+6>:	push   %rbp
   0x00000000004010e3 <+7>:	push   %rbx
   0x00000000004010e4 <+8>:	sub    $0x60,%rsp
   0x00000000004010e8 <+12>:	mov    %fs:0x28,%rax
   0x00000000004010f1 <+21>:	mov    %rax,0x58(%rsp)
   0x00000000004010f6 <+26>:	xor    %eax,%eax
   0x00000000004010f8 <+28>:	mov    %rsp,%rsi
   0x00000000004010fb <+31>:	callq  0x401469 <read_six_numbers>
   // 第一个循环(嵌套),推出答案1-6的排列
   0x0000000000401100 <+36>:	mov    %rsp,%r12
   0x0000000000401103 <+39>:	mov    %rsp,%r13
   0x0000000000401106 <+42>:	mov    $0x0,%r14d
   0x000000000040110c <+48>:	jmp    0x401133 <phase_6+87>
   0x000000000040110e <+50>:	callq  0x401447 <explode_bomb>
   0x0000000000401113 <+55>:	jmp    0x401142 <phase_6+102>
   0x0000000000401115 <+57>:	add    $0x1,%ebx
   0x0000000000401118 <+60>:	cmp    $0x5,%ebx
   0x000000000040111b <+63>:	jg     0x40112f <phase_6+83>
   0x000000000040111d <+65>:	movslq %ebx,%rax
   0x0000000000401120 <+68>:	mov    (%rsp,%rax,4),%eax
   0x0000000000401123 <+71>:	cmp    %eax,0x0(%rbp)
   0x0000000000401126 <+74>:	jne    0x401115 <phase_6+57>
   0x0000000000401128 <+76>:	callq  0x401447 <explode_bomb>
   0x000000000040112d <+81>:	jmp    0x401115 <phase_6+57>
   0x000000000040112f <+83>:	add    $0x4,%r13
   0x0000000000401133 <+87>:	mov    %r13,%rbp
   0x0000000000401136 <+90>:	mov    0x0(%r13),%eax
   0x000000000040113a <+94>:	sub    $0x1,%eax
   0x000000000040113d <+97>:	cmp    $0x5,%eax
   0x0000000000401140 <+100>:	ja     0x40110e <phase_6+50>
   0x0000000000401142 <+102>:	add    $0x1,%r14d
   0x0000000000401146 <+106>:	cmp    $0x6,%r14d
   0x000000000040114a <+110>:	je     0x401151 <phase_6+117>
   0x000000000040114c <+112>:	mov    %r14d,%ebx
   0x000000000040114f <+115>:	jmp    0x40111d <phase_6+65>
   // 第二个循环
   0x0000000000401151 <+117>:	lea    0x18(%r12),%rcx
   0x0000000000401156 <+122>:	mov    $0x7,%edx
   0x000000000040115b <+127>:	mov    %edx,%eax
   0x000000000040115d <+129>:	sub    (%r12),%eax
   0x0000000000401161 <+133>:	mov    %eax,(%r12)
   0x0000000000401165 <+137>:	add    $0x4,%r12
   0x0000000000401169 <+141>:	cmp    %r12,%rcx
   0x000000000040116c <+144>:	jne    0x40115b <phase_6+127>
   // 第三个循环(嵌套)
   0x000000000040116e <+146>:	mov    $0x0,%esi
   0x0000000000401173 <+151>:	jmp    0x40118f <phase_6+179>
   0x0000000000401175 <+153>:	mov    0x8(%rdx),%rdx
   0x0000000000401179 <+157>:	add    $0x1,%eax
   0x000000000040117c <+160>:	cmp    %ecx,%eax
   0x000000000040117e <+162>:	jne    0x401175 <phase_6+153>
   0x0000000000401180 <+164>:	mov    %rdx,0x20(%rsp,%rsi,8)
   0x0000000000401185 <+169>:	add    $0x1,%rsi
   0x0000000000401189 <+173>:	cmp    $0x6,%rsi
   0x000000000040118d <+177>:	je     0x4011a3 <phase_6+199>
   0x000000000040118f <+179>:	mov    (%rsp,%rsi,4),%ecx
   0x0000000000401192 <+182>:	mov    $0x1,%eax
   0x0000000000401197 <+187>:	mov    $0x6032d0,%edx		/*0x6032d0这是一个单链表的首地址*/
   0x000000000040119c <+192>:	cmp    $0x1,%ecx
   0x000000000040119f <+195>:	jg     0x401175 <phase_6+153>
   0x00000000004011a1 <+197>:	jmp    0x401180 <phase_6+164>
   // 第四个循环
   0x00000000004011a3 <+199>:	mov    0x20(%rsp),%rbx
   0x00000000004011a8 <+204>:	mov    0x28(%rsp),%rax
   0x00000000004011ad <+209>:	mov    %rax,0x8(%rbx)
   0x00000000004011b1 <+213>:	mov    0x30(%rsp),%rdx
   0x00000000004011b6 <+218>:	mov    %rdx,0x8(%rax)
   0x00000000004011ba <+222>:	mov    0x38(%rsp),%rax
   0x00000000004011bf <+227>:	mov    %rax,0x8(%rdx)
   0x00000000004011c3 <+231>:	mov    0x40(%rsp),%rdx
   0x00000000004011c8 <+236>:	mov    %rdx,0x8(%rax)
   0x00000000004011cc <+240>:	mov    0x48(%rsp),%rax
   0x00000000004011d1 <+245>:	mov    %rax,0x8(%rdx)
   0x00000000004011d5 <+249>:	movq   $0x0,0x8(%rax)
   // 第五个循环
   0x00000000004011dd <+257>:	mov    $0x5,%ebp
   0x00000000004011e2 <+262>:	jmp    0x4011ed <phase_6+273>
   0x00000000004011e4 <+264>:	mov    0x8(%rbx),%rbx
   0x00000000004011e8 <+268>:	sub    $0x1,%ebp
   0x00000000004011eb <+271>:	je     0x4011fe <phase_6+290>
   0x00000000004011ed <+273>:	mov    0x8(%rbx),%rax
   0x00000000004011f1 <+277>:	mov    (%rax),%eax
   0x00000000004011f3 <+279>:	cmp    %eax,(%rbx)
   0x00000000004011f5 <+281>:	jge    0x4011e4 <phase_6+264>
   0x00000000004011f7 <+283>:	callq  0x401447 <explode_bomb>
   0x00000000004011fc <+288>:	jmp    0x4011e4 <phase_6+264>
   0x00000000004011fe <+290>:	mov    0x58(%rsp),%rax
   0x0000000000401203 <+295>:	xor    %fs:0x28,%rax
   0x000000000040120c <+304>:	jne    0x40121b <phase_6+319>
   0x000000000040120e <+306>:	add    $0x60,%rsp
   0x0000000000401212 <+310>:	pop    %rbx
   0x0000000000401213 <+311>:	pop    %rbp
   0x0000000000401214 <+312>:	pop    %r12
   0x0000000000401216 <+314>:	pop    %r13
   0x0000000000401218 <+316>:	pop    %r14
   0x000000000040121a <+318>:	retq   
   0x000000000040121b <+319>:	callq  0x400b00 <__stack_chk_fail@plt>
End of assembler dump.

由于上述代码过于冗长,且其中嵌套循环很多,每个循环采用汇编写法还不太一样,对数据进行了较多操作,(光靠大脑记不住)为了便于分析和理解,我通过逐行理解将其翻译为了如下C语言代码(写的比较粗糙):

// 代码开始和phase_3一样利用read_six_numbers读入六个数
// 输入的数组,依次存储在%rsp + i*4(i=0,1,2,3,4,5)处
int a[6] = {n_0, n_1, n_2, n_3, n_4, n_5};
// 爆炸
void explode_bomb();
#######################################################################################################
// 第一个嵌套循环,从<+36>一直到<+115>
// 检查每个数的值,大于6,小于1就爆炸
for(int i=0;i<6;i++){
   // 因为跳转爆炸的比较采用的是 ja(无符号) 比较n-1和5,说明0<=n-1<=5,所以1<=n<=6
	if(1<=a[i]&& a[i]<=6) 
	{
      // 内层循环比较 要求每个数各不相同,否则爆炸
		for(int j=i+1;j<6;j++){
			if(a[j]==a[i]) explode_bomb();
		}
	}
	else explode_bomb();
}
// 从第一个嵌套循环可以知道这6个数是1到6的一个排列
#######################################################################################################
// 第二个循环,从<+117>一直到<+144>
for(int i=0;i<6;i++){
	a[i] = 7-a[i];
}
// 这里对于数组里的值重新进行了赋值,后面的操作在这个基础上进行,
// 所以从后面推得的结果还要用7减才能得到真正结果。
// 这里处理完得到的a[]的值还是一个1-6的排列
#######################################################################################################
// 接下来的操作涉及了一个内存中已有的单链表,这个单链表的节点结构如下,其具体内容和分析将在下文写到。
/*单链表节点的结构*/
struct node{
	long int x;
	*node next;
}node;
// head指向单链表首地址,对于链表中其他节点的内容这里省略。可以见下文的内存展示的结果。
struct node *head = (node*)0x6032d0;
struct node *p;
// 栈中存储单链表节点的空间,在代码中这部分空间对应的位置其实是从%rsp+0x20开始的6*8个字节
struct node *nodes[6];
// 第三个循环,从<+146>一直到<+197>
// 这个循环其实就是根据数组a[i]的值把点链表中的相应节点存到nodes[i]中,
// 并且如果将单链表的节点编号为1-6,那么node[i]中存储的节点的编号就等于a[i]的值。
for(int i=0;i<6;i++){
   int cnt = 1;
	int num = a[i];
   p = head;
	while(num>1&&num!=cnt){
      p = p->next;
      cnt++;
	}
   nodes[i] = p;
}
#######################################################################################################
// 第四个循环, 从<+199>一直到<+249>
// 这个循环将nodes中的节点依次连接
for(int i=1;i<6;i++){
   nodes[i-1]->next = nodes[i];
}
nodes[5]->next = NULL;
#######################################################################################################
// 第五个循环,从<+257>一直到<+288>
// 这个循环检查nodes中的节点的值必须是降序的。
int count=5;
int i=0;
while(count){
   if(nodes[i]->x<nodes[i+1]->x){
         explode_bomb();
      }
   count--;
   i++;
}
// ps:实际上最后一个循环的操作不只是这样的,
// 如果没注意到的这点,会导致答案错误,炸弹爆炸!这里我后续展开讲述

在上述的代码中涉及到一个首地址为0x6032d0的内存内容,通过查看内存,我们可以看到如下结果:

(gdb) x/12xg 0x6032d0
0x6032d0 <node1>:	0x000000010000027a	0x00000000006032e0
0x6032e0 <node2>:	0x0000000200000353	0x00000000006032f0
0x6032f0 <node3>:	0x0000000300000399	0x0000000000603300
0x603300 <node4>:	0x0000000400000136	0x0000000000603310
0x603310 <node5>:	0x0000000500000249	0x0000000000603320
0x603320 <node6>:	0x000000060000008a	0x0000000000000000

我么可以发现从0x6032d0开始的6*8*2个字节内存储的是6个node,每一个node占了8*2个字节,后8个字节内容刚好是下一个node的首地址,联系代码中的%rdx=(%rdx+0x8)操作,我们不难推出这相当于一个 next指针,且代码后续还对每个next指向地址(不包含最后一个,最后一个指向NULL)的内容(也即每个node的前8个字节)进行了大小比较操作,因此我们可以知道每个node是一个结构体,它们作为节点构成了这一条单链表,node数据结构如上述C语言代码所示。

根据上面写出的代码和执行逻辑,我按照降序排列的单链表节点得到编号为6 5 4 3 2 1,根据其与最初数组的对应关系我得到一个答案为1 2 3 4 5 6,But最后炸弹爆炸了。
在我百思不得其解,反复比对C语言代码和实际反汇编代码后,我终于发现了问题所在(哭死,眼睛都快看瞎了才发现这个问题,一直在纠结是不是逻辑搞错了):

   0x00000000004011ed <+273>:	mov    0x8(%rbx),%rax
   0x00000000004011f1 <+277>:	mov    (%rax),%eax
   0x00000000004011f3 <+279>:	cmp    %eax,(%rbx)
   0x00000000004011f5 <+281>:	jge    0x4011e4 <phase_6+264>
   0x00000000004011f7 <+283>:	callq  0x401447 <explode_bomb>

这里比较nodes[i].xnodes[i+1].x时,程序取出nodes[i+1].x时,采用的是%eax,也即会将nodes[i+1].x的真实值(64位)截断至32位(高32位置0),所以cmp %eax,(%rbx)到底会如何比较这个32位数和64位数呢?
实际上显然不会是将32位数扩展至64位后再进行比较 (因为nodes[i].x高32位不全为0,nodes[i+1].x扩展至32位后高位全0,这时候一定会有nodes[i].x>nodes[i+1].x,这样我上面的实验也不会爆炸了) ,所以我猜测在本实验环境下,计算机应该是将64位数截断至32位后再进行比较。
据此,我们首先将链表节点截断至32位得到如下结果:

0x6032d0 <node1>:	0x0000027a	
0x6032e0 <node2>:	0x00000353
0x6032f0 <node3>:	0x00000399
0x603300 <node4>:	0x00000136
0x603310 <node5>:	0x00000249
0x603320 <node6>:	0x0000008a

降序排序得到编号为3 2 1 5 4 6,得到最终答案为4 5 6 2 3 1,炸弹破解!

secret_phase

触发:

想要解决隐藏关,我们需要先找到隐藏关,成功触发隐藏关。在上述6个phase的代码中显然是没有触发secret_phase的设置的;通过研究bomb.c我们可以发现除了这6个phasephase_defused()这个函数每次破解炸弹都会调用,而我们正好没看过这个函数内部。因此我们可以试探性地查看phase_defused()是否有可能是触发隐藏关的机关。
反汇编phase_defused()结果如下:

Dump of assembler code for function phase_defused:
   0x00000000004015d6 <+0>:	sub    $0x78,%rsp
   0x00000000004015da <+4>:	mov    %fs:0x28,%rax
   0x00000000004015e3 <+13>:	mov    %rax,0x68(%rsp)
   0x00000000004015e8 <+18>:	xor    %eax,%eax
   0x00000000004015ea <+20>:	cmpl   $0x6,0x20217b(%rip)        # 0x60376c <num_input_strings> 
   0x00000000004015f1 <+27>:	je     0x401608 <phase_defused+50>
   0x00000000004015f3 <+29>:	mov    0x68(%rsp),%rax
   0x00000000004015f8 <+34>:	xor    %fs:0x28,%rax
   0x0000000000401601 <+43>:	jne    0x40166a <phase_defused+148>
   0x0000000000401603 <+45>:	add    $0x78,%rsp
   0x0000000000401607 <+49>:	retq   
   0x0000000000401608 <+50>:	lea    0x10(%rsp),%r8
   0x000000000040160d <+55>:	lea    0xc(%rsp),%rcx
   0x0000000000401612 <+60>:	lea    0x8(%rsp),%rdx
   0x0000000000401617 <+65>:	mov    $0x402619,%esi      // %d %d %s
   0x000000000040161c <+70>:	mov    $0x603870,%edi      //定位到第四关后面
   0x0000000000401621 <+75>:	callq  0x400ba0 <__isoc99_sscanf@plt>
   0x0000000000401626 <+80>:	cmp    $0x3,%eax
   0x0000000000401629 <+83>:	je     0x401637 <phase_defused+97>     // 读入3个参数,触发隐藏机制
   0x000000000040162b <+85>:	mov    $0x402558,%edi
   0x0000000000401630 <+90>:	callq  0x400ae0 <puts@plt>
   0x0000000000401635 <+95>:	jmp    0x4015f3 <phase_defused+29>
   0x0000000000401637 <+97>:	mov    $0x402622,%esi
   0x000000000040163c <+102>:	lea    0x10(%rsp),%rdi
   0x0000000000401641 <+107>:	callq  0x40134a <strings_not_equal>    //%rsp+0x10处存储内容为"urxvt"
   0x0000000000401646 <+112>:	test   %eax,%eax
   0x0000000000401648 <+114>:	jne    0x40162b <phase_defused+85>
   0x000000000040164a <+116>:	mov    $0x4024f8,%edi
   0x000000000040164f <+121>:	callq  0x400ae0 <puts@plt>
   0x0000000000401654 <+126>:	mov    $0x402520,%edi
   0x0000000000401659 <+131>:	callq  0x400ae0 <puts@plt>
   0x000000000040165e <+136>:	mov    $0x0,%eax
   0x0000000000401663 <+141>:	callq  0x40125f <secret_phase>
   0x0000000000401668 <+146>:	jmp    0x40162b <phase_defused+85>
   0x000000000040166a <+148>:	callq  0x400b00 <__stack_chk_fail@plt>
End of assembler dump.

在上述代码的<+141>处,我们果真看到了secret_phase的调用。
逐段阅读代码:

   0x00000000004015ea <+20>:	cmpl   $0x6,0x20217b(%rip)        # 0x60376c <num_input_strings> 
   0x00000000004015f1 <+27>:	je     0x401608 <phase_defused+50>
   0x00000000004015f3 <+29>:	mov    0x68(%rsp),%rax
   0x00000000004015f8 <+34>:	xor    %fs:0x28,%rax
   0x0000000000401601 <+43>:	jne    0x40166a <phase_defused+148>
   0x0000000000401603 <+45>:	add    $0x78,%rsp
   0x0000000000401607 <+49>:	retq   
   0x0000000000401608 <+50>:	lea    0x10(%rsp),%r8
   0x000000000040160d <+55>:	lea    0xc(%rsp),%rcx

可以发现phase_defused首先比较了0x6(0x20217b+%rip)也即 (0x60376c)的大小,相等则跳转至<+50>处继续运行,否则返回;为了触发隐藏关,我们肯定需要让(0x60376c)=6,实际上这里的就是要求触发隐藏关必须先破解前6个phase(通过在每个关卡设置断点并查看0x60376c处的值,我们可以发现它是随着破解关卡数目增长的,6关都破解时增长至6)。显然已经满足条件。
继续看下一段:

   0x0000000000401608 <+50>:	lea    0x10(%rsp),%r8
   0x000000000040160d <+55>:	lea    0xc(%rsp),%rcx
   0x0000000000401612 <+60>:	lea    0x8(%rsp),%rdx
   0x0000000000401617 <+65>:	mov    $0x402619,%esi      // %d %d %s
   0x000000000040161c <+70>:	mov    $0x603870,%edi      // 定位到第四关后面
   0x0000000000401621 <+75>:	callq  0x400ba0 <__isoc99_sscanf@plt>
   0x0000000000401626 <+80>:	cmp    $0x3,%eax
   0x0000000000401629 <+83>:	je     0x401637 <phase_defused+97>     // 读入3个参数,触发隐藏机制
   0x000000000040162b <+85>:	mov    $0x402558,%edi
   0x0000000000401630 <+90>:	callq  0x400ae0 <puts@plt>
   0x0000000000401635 <+95>:	jmp    0x4015f3 <phase_defused+29>
   0x0000000000401637 <+97>:	mov    $0x402622,%esi

在这里,函数再次调用了scanf并要求一定要读入3个参数,否则跳转至<+29>函数返回,无法触发隐藏关。
那么scanf读入的是什么呢?在调试过程中我通过在6个phase上都设置断点,逐步运行到第6个phase时,通过查看内存得到传入scanf的两个参数的地址应为如下结果:

(gdb) x/s 0x402619
0x402619:	"%d %d %s"
(gdb) x/s 0x603870
0x603870 <input_strings+240>:	"13 3"

%esi指示了scanf读入数据类型为两个整数和一个字符串,%edi处显示的内容为"13 3",这正是调试过程中我传入phase_4的答案,因此我猜测我们可能需要在第四个phase_4的答案后面输入一个特定的字符串以解锁隐藏关。(这并不会影响phase_4的破解,因为程序在解phase_4时,只会读入前两个整数就会停止读入)
这个字符串是什么呢,继续往下看代码:

   0x000000000040162b <+85>:	mov    $0x402558,%edi
   0x0000000000401630 <+90>:	callq  0x400ae0 <puts@plt>
   0x0000000000401635 <+95>:	jmp    0x4015f3 <phase_defused+29>
   0x0000000000401637 <+97>:	mov    $0x402622,%esi
   0x000000000040163c <+102>:	lea    0x10(%rsp),%rdi
   0x0000000000401641 <+107>:	callq  0x40134a <strings_not_equal>    //%rsp+0x10处存储内容为"urxvt"
   0x0000000000401646 <+112>:	test   %eax,%eax
   0x0000000000401648 <+114>:	jne    0x40162b <phase_defused+85>

在这里,函数调用了strings_not_equal来比较%esi=0x402622指向的字符串和0x10(%rsp)指向的字符串。若不相同则跳转至<+85>处,之后还会继续跳转至<+29>处函数返回,相等则向后继续执行最终将触发隐藏关。
0x10(%rsp)在之前调用scanf时层作为参数传入,(在不想仔细推敲位置存放的情况下)我直觉这里存放的就是我们输入的字符串,查看内存得到$0x402622结果如下:

(gdb) x/s 0x402622
0x402622:	"urxvt"

phase_4的答案后加上字符串urxvt,成功触发secret_phase!

拆弹分析:

secret_phase处设置断点,使用gdbdisassemble命令反汇编secret_phase,结果如下:

Dump of assembler code for function secret_phase:
   0x000000000040125f <+0>:	push   %rbx
   0x0000000000401260 <+1>:	callq  0x4014a8 <read_line>
   0x0000000000401265 <+6>:	mov    $0xa,%edx
   0x000000000040126a <+11>:	mov    $0x0,%esi
   0x000000000040126f <+16>:	mov    %rax,%rdi
   0x0000000000401272 <+19>:	callq  0x400b80 <strtol@plt>
   0x0000000000401277 <+24>:	mov    %rax,%rbx
   0x000000000040127a <+27>:	lea    -0x1(%rax),%eax
   0x000000000040127d <+30>:	cmp    $0x3e8,%eax
   0x0000000000401282 <+35>:	ja     0x4012ab <secret_phase+76>
   0x0000000000401284 <+37>:	mov    %ebx,%esi
   0x0000000000401286 <+39>:	mov    $0x6030f0,%edi
   0x000000000040128b <+44>:	callq  0x401220 <fun7>
   0x0000000000401290 <+49>:	cmp    $0x4,%eax
   0x0000000000401293 <+52>:	je     0x40129a <secret_phase+59>
   0x0000000000401295 <+54>:	callq  0x401447 <explode_bomb>
   0x000000000040129a <+59>:	mov    $0x402408,%edi
   0x000000000040129f <+64>:	callq  0x400ae0 <puts@plt>
   0x00000000004012a4 <+69>:	callq  0x4015d6 <phase_defused>
   0x00000000004012a9 <+74>:	pop    %rbx
   0x00000000004012aa <+75>:	retq   
   0x00000000004012ab <+76>:	callq  0x401447 <explode_bomb>
   0x00000000004012b0 <+81>:	jmp    0x401284 <secret_phase+37>
End of assembler dump.

在上述代码在读入一行字符后将其和0作为参数传入了一个C语言的库函数strtol,经查阅,了解到strcol作用是将传入的字符串%edi转化为以%esi为基数的数字(eg. %esi=2,则将%edi转化为二进制数字),当传入数值为0时,若%edi0x/0X开头,为16进制,以0开头为8进制,否则为10进制。所以在这里secret_phase将传入的字符串转化为了1个数。

接下来,secret_phase对于这个输入的数进行了限制,要求其-1并截断至32位后不大于0x3e8(大于0),实际上这个条件很好满足,我们可以直接看下一段代码。

   0x0000000000401277 <+24>:	mov    %rax,%rbx
   0x000000000040127a <+27>:	lea    -0x1(%rax),%eax
   0x000000000040127d <+30>:	cmp    $0x3e8,%eax
   0x0000000000401282 <+35>:	ja     0x4012ab <secret_phase+76>
   ......
   0x00000000004012ab <+76>:	callq  0x401447 <explode_bomb>

在接下来的代码中调用了函数fun7,并要求fun7的返回值为4,否则炸弹爆炸,只要我们能让fun7的返回值为4,我们就能1破解这个炸弹(因为后续已经没有其他会触发炸弹的操作了)。

   0x0000000000401284 <+37>:	mov    %ebx,%esi
   0x0000000000401286 <+39>:	mov    $0x6030f0,%edi
   0x000000000040128b <+44>:	callq  0x401220 <fun7>
   0x0000000000401290 <+49>:	cmp    $0x4,%eax
   0x0000000000401293 <+52>:	je     0x40129a <secret_phase+59>
   0x0000000000401295 <+54>:	callq  0x401447 <explode_bomb>

fun7的反汇编结果如下:

Dump of assembler code for function fun7:
   0x0000000000401220 <+0>:	test   %rdi,%rdi
   0x0000000000401223 <+3>:	je     0x401259 <fun7+57>
   0x0000000000401225 <+5>:	sub    $0x8,%rsp
   0x0000000000401229 <+9>:	mov    (%rdi),%edx
   0x000000000040122b <+11>:	cmp    %esi,%edx
   0x000000000040122d <+13>:	jg     0x40123d <fun7+29>
   0x000000000040122f <+15>:	mov    $0x0,%eax
   0x0000000000401234 <+20>:	cmp    %esi,%edx
   0x0000000000401236 <+22>:	jne    0x40124a <fun7+42>
   0x0000000000401238 <+24>:	add    $0x8,%rsp
   0x000000000040123c <+28>:	retq   
   0x000000000040123d <+29>:	mov    0x8(%rdi),%rdi
   0x0000000000401241 <+33>:	callq  0x401220 <fun7>
   0x0000000000401246 <+38>:	add    %eax,%eax
   0x0000000000401248 <+40>:	jmp    0x401238 <fun7+24>
   0x000000000040124a <+42>:	mov    0x10(%rdi),%rdi
   0x000000000040124e <+46>:	callq  0x401220 <fun7>
   0x0000000000401253 <+51>:	lea    0x1(%rax,%rax,1),%eax
   0x0000000000401257 <+55>:	jmp    0x401238 <fun7+24>
   0x0000000000401259 <+57>:	mov    $0xffffffff,%eax
   0x000000000040125e <+62>:	retq   
End of assembler dump.

secret_phase中,函数将读入的数截断至32位后传入了%esifun7的第一个参数),同时还将地址0x6030f0传入了%edi(第二个参数)。fun7很明显是一个递归函数,其中反复对%rdi进行了修改,操作类似指针,我们查看自0x6030f0起的60*8个字节(因为其中涉及的后继节点一直到持续到0x6032c0处),如下:

(gdb) x/60hg 0x6030f0
0x6030f0 <n1>:	0x0000000000000024	0x0000000000603110
0x603100 <n1+16>:	0x0000000000603130	0x0000000000000000
0x603110 <n21>:	0x0000000000000008	0x0000000000603190
0x603120 <n21+16>:	0x0000000000603150	0x0000000000000000
0x603130 <n22>:	0x0000000000000032	0x0000000000603170
0x603140 <n22+16>:	0x00000000006031b0	0x0000000000000000
0x603150 <n32>:	0x0000000000000016	0x0000000000603270
0x603160 <n32+16>:	0x0000000000603230	0x0000000000000000
0x603170 <n33>:	0x000000000000002d	0x00000000006031d0
0x603180 <n33+16>:	0x0000000000603290	0x0000000000000000
0x603190 <n31>:	0x0000000000000006	0x00000000006031f0
0x6031a0 <n31+16>:	0x0000000000603250	0x0000000000000000
0x6031b0 <n34>:	0x000000000000006b	0x0000000000603210
0x6031c0 <n34+16>:	0x00000000006032b0	0x0000000000000000
0x6031d0 <n45>:	0x0000000000000028	0x0000000000000000
0x6031e0 <n45+16>:	0x0000000000000000	0x0000000000000000
0x6031f0 <n41>:	0x0000000000000001	0x0000000000000000
0x603200 <n41+16>:	0x0000000000000000	0x0000000000000000
0x603210 <n47>:	0x0000000000000063	0x0000000000000000
0x603220 <n47+16>:	0x0000000000000000	0x0000000000000000
0x603230 <n44>:	0x0000000000000023	0x0000000000000000
0x603240 <n44+16>:	0x0000000000000000	0x0000000000000000
0x603250 <n42>:	0x0000000000000007	0x0000000000000000
0x603260 <n42+16>:	0x0000000000000000	0x0000000000000000
0x603270 <n43>:	0x0000000000000014	0x0000000000000000
0x603280 <n43+16>:	0x0000000000000000	0x0000000000000000
0x603290 <n46>:	0x000000000000002f	0x0000000000000000
0x6032a0 <n46+16>:	0x0000000000000000	0x0000000000000000
0x6032b0 <n48>:	0x00000000000003e9	0x0000000000000000
0x6032c0 <n48+16>:	0x0000000000000000	0x0000000000000000

通过观察,我们发现这是15个相同的结构,每个结构有8*4个字节,前8个字节为一个数,中间8*2个字节是存储的是后续内存的地址(所以是指向其他结构的指针),最后8个字节总是为0(可能是填充?),且根据节点之间的链接关系和编号,我们可以知道这是一个深度为4的二叉树,其结构如下所示(从这里我们也可以知道为什么要求输入的数减1截断后不能大于0x3e8,因为二叉树中能与之匹配的最大数为0x3e9):

36
8
50
6
22
45
107
1
7
20
35
40
47
99
1001

据此我们可以写出C语言代码如下:

// 二叉树节点结构
struct node{
   long int val;
   struct node* leftchild;
   struct node* rightchild;
}node;
// 二叉树根节点首地址为0x6030f0
struct node* root = 0x6030f0;
int fun7(struct node* rdi,int esi){
   if(rdi==NULL) return 0xffffffff;
   if(esi==rdi->val){
      return 0;
   }
   else if(esi > rdi->val){
      rdi = rdi->rightchild;
      return 2*fun7(rdi, esi)+1;
   }
   else{
      // esi < rdi->val
      rdi = rdi->leftchild;
      return 2*fun7(rdi, esi);
   }
}

要使fun7最终返回值为4,只能通过表达式4=2*(2*(2*0+1))得到,因此知道函数fun7总共调用了4次,从最内层到最外层%esi%rdi (由于数值不超过二进制32位表示范围,所以截断至32位也不会对实际数值造成改变,我们直接比就好了) 的比较结果依次为= > < <,最后倒推可以得出,与%esi相等的节点应该是二叉树根结点的左孩子结点的左孩子节点的右孩子节点,即结点n42,所以%esi=7
因此对于最后的答案我们可以直接取 7
至此,炸弹全部拆完。

四、实验总结

通过这次实验,我对于数据在内存中的存储方式有了更加深入的了解,同时汇编代码的阅读和理解能力也得到了极大的提升。
这次实验也让我意识到了我们使用高级语言所编写的简单程序,及其需要如此多的指令才能实现,对于编程语言的发展也有了更深刻的理解。
高级语言编译为汇编语言过程中,编译器对于指令的优化也让我对于如何在编程中改善程序的运行性能有了更为切实的感受和体悟。

  • 43
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值