参考
- 深入理解计算机系统–bomblab
- CSAPP | Lab2-Bomb Lab 深入解析: https://zhuanlan.zhihu.com/p/472178808
- gdb ---- x命令详解: https://www.jianshu.com/p/589308dd36dc
- 汇编跳转指令: JMP、JECXZ、JA、JB、JG、JL、JE、JZ、JS、JC、JO、JP 等
- https://www.bookstack.cn/read/mpe/zh-cn-markdown-basics.md
- https://markdown.com.cn/
跳转指令
附一张跳转指令表格,方便直接查询:
jX | 条件 | 描述 |
---|---|---|
jmp | 1 | 无条件 |
je | ZF | 相等/零 |
jne | ~ZF | 不等/非零 |
js | SF | 负数 |
jns | ~SF | 非负数 |
jg | ~ (SF^OF) & ~ZF | 大于(有符号数) |
jge | ~ (SF^OF) | 大于或等于(有符号数) |
jl | (SF^OF) | 小于 (有符号数) |
jle | (SF^OF) | ZF | 小于或等于(有符号数) |
ja | ~CF & ~ZF | 高于(无符号数) |
jb | CF | 低于(无符号数) |
一、实验目的
本实验目的是教会学生能够理解汇编语言,使学生学会如何使用调试器(debugger)并在这个经典的实验中感受到实验的乐趣。
二、报告要求
本报告要求学生写出实验中炸弹拆除的推理过程。
三、炸弹拆除
phase_1
拆弹分析:
在phase_1
处设置断点,使用gdb
的disassemble
命令反汇编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
处设置断点,使用gdb
的disassemble
命令反汇编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+1
和 a_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
处设置断点,使用gdb
的disassemble
命令反汇编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_1
,n_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
处设置断点,使用gdb
的disassemble
命令反汇编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_4
与phase_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_1
,n_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_1
和0
。
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次调用func4
,edi
(即n_1
)和ecx
比较结果依次为>><=
)和13
(3次调用func4
,edi
和ecx
比较结果依次为>>=
)。
(tips:如果不想计算,其实n_1
从1遍历到14也可以得出结果)
所以最后得到的答案可以为12 3
或者13 3
。
phase_5
拆弹分析:
在phase_5
处设置断点,使用gdb
的disassemble
命令反汇编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
可以使上述循环体循环三次,在第三次的时候跳转到使%rax
为0xf
内存位置,退出循环。
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
,具体推理过程如下:
rdx=3
时,eax_3=15
,ecx_3 = ecx_2+15
;因此a[eax_2]=15
,所以eax_2=6
。rdx=2
时,eax_2=6
,ecx_2 = ecx_1+6
;因此a[eax_1]=6
,所以eax_1=14
。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+2
,x
为自然数)
phase_6
拆弹分析:
在phase_6
处设置断点,使用gdb
的disassemble
命令反汇编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].x
和nodes[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个phase
,phase_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
处设置断点,使用gdb
的disassemble
命令反汇编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时,若%edi
以0x/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位后传入了%esi
(fun7
的第一个参数),同时还将地址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
):
据此我们可以写出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
。
至此,炸弹全部拆完。
四、实验总结
通过这次实验,我对于数据在内存中的存储方式有了更加深入的了解,同时汇编代码的阅读和理解能力也得到了极大的提升。
这次实验也让我意识到了我们使用高级语言所编写的简单程序,及其需要如此多的指令才能实现,对于编程语言的发展也有了更深刻的理解。
高级语言编译为汇编语言过程中,编译器对于指令的优化也让我对于如何在编程中改善程序的运行性能有了更为切实的感受和体悟。