CSAPP:拆炸弹实验笔记
拆炸弹实验介绍
拆炸弹实验是CSAPP书附赠的一个有趣的实验。“炸弹”是一个可执行文件。用户执行“炸弹”后需要进行6个输入,只要输入错误一次,炸弹就会爆炸。用户需要将“炸弹”反汇编成汇编代码,并用GDB进行调试,推测出6个正确的输入分别是什么。
本门课程的助教给我们搭建了自动化的测试平台。同学可以在平台上领取专属于自己的炸弹(每个人的炸弹都有些小不同)和查看自己的分数。我领到的炸弹是39号。下载地址:
链接: https://pan.baidu.com/s/1FP2YkbnjtO_-pu9IkOmMqQ 提取码: ms8m
拆炸弹前需要知道的
需要用到的GDB命令
disas
这个命令是用于反汇编的。
disas 函数名用于反汇编一个函数,并输出得到的汇编代码
break
用于设置断点
- break 函数名:在这个函数开始的地方设置一个断点
- break *指令的地址:在这一行指令处设置断点。
例:break *0x0000000000400ef0
x
查看内存地址内容的指令
info breakpoints
查看已经设置好的断点信息
ni
单步调试。让程序运行到下一行
continue或c
让程序运行到下一个断点处停下来
如何防止炸弹爆炸
炸弹每爆炸一次会从总成绩中扣除0.5分,最多扣除20分。(这条是网上看到的,不知道对不对)
通过对phase_1,phase_2,phase_3,phase_4,phase_5,phase_6这六个函数进行disas操作以后可以发现,炸弹的爆炸行为是由explode_bomb这个函数启动的。故我们只需要在explode_bomb这个函数打上断点,如下图所示
之后,如果炸弹“准备爆炸”(即:程序进入explode_bomb这个函数),程序就会停下来。这时候就可以用run这个操作从头再来,而不会导致扣分。
拆炸弹的过程
phase_1(字符串比较)
第一关是最简单的一关。首先用 break phase_1在第一关的函数开始处打上断点,然后运行程序。随便进行一个输入
程序停下来后,用disas phase_1查看第一关函数的汇编代码。
先使用两次ni指令让程序运行到<+9>的位置。
可以看到这里程序调用了<string_not_equal>这个函数,这个函数很显然是判断两个字符串是否是相等的。%eax这个寄存器应该是存储了这个函数的返回值。
再看<+14>和<+16>的位置。如果 %eax & %eax的结果是0的话(即<string_not_equal>返回0),则跳到23行,任务完成。否则(即<string_not_equal>返回1),炸弹就会爆炸。
因此,我们发现这关是要求输入一个字符串。如果我们输入的这个字符串和炸弹中的答案一样的话,就可以拆除这一关,否则炸弹就会爆炸。
我们使用info registers指令查看当前所有的寄存器的内容。
可以看到 %rsi和%rdi这两个寄存器都存储了一个地址。这两个地址应该是被当做实参,传到了<string_not_equal>这个函数里。因此我们对这两个地址的内容进行查看。
可以看到%rdi中的地址存储的是我们的输入,而%rsi中的地址存储的就是正确答案了。
因此,本关的答案是I can see Russia from my house!
phase_2(循环)
第二关的汇编代码如下
=> 0x0000000000400f0c <+0>: push %rbp
0x0000000000400f0d <+1>: push %rbx
0x0000000000400f0e <+2>: sub $0x28,%rsp
0x0000000000400f12 <+6>: mov %rsp,%rsi
0x0000000000400f15 <+9>: callq 0x4015bf <read_six_numbers>
0x0000000000400f1a <+14>: cmpl $0x0,(%rsp)
0x0000000000400f1e <+18>: jns 0x400f44 <phase_2+56>
0x0000000000400f20 <+20>: callq 0x401589 <explode_bomb>
0x0000000000400f25 <+25>: jmp 0x400f44 <phase_2+56>
0x0000000000400f27 <+27>: mov %ebx,%eax
0x0000000000400f29 <+29>: add -0x4(%rbp),%eax
0x0000000000400f2c <+32>: cmp %eax,0x0(%rbp)
0x0000000000400f2f <+35>: je 0x400f36 <phase_2+42>
0x0000000000400f31 <+37>: callq 0x401589 <explode_bomb>
0x0000000000400f36 <+42>: add $0x1,%ebx
0x0000000000400f39 <+45>: add $0x4,%rbp
0x0000000000400f3d <+49>: cmp $0x6,%ebx
0x0000000000400f40 <+52>: jne 0x400f27 <phase_2+27>
0x0000000000400f42 <+54>: jmp 0x400f50 <phase_2+68>
0x0000000000400f44 <+56>: lea 0x4(%rsp),%rbp
0x0000000000400f49 <+61>: mov $0x1,%ebx
0x0000000000400f4e <+66>: jmp 0x400f27 <phase_2+27>
0x0000000000400f50 <+68>: add $0x28,%rsp
0x0000000000400f54 <+72>: pop %rbx
--Type <RET> for more, q to quit, c to continue without paging--c
0x0000000000400f55 <+73>: pop %rbp
0x0000000000400f56 <+74>: retq
End of assembler dump.
我们可以在<+9>处看到这个程序调用了<read_six_numbers>这个函数,即读取了6个数字。因此我们可以知道第二关的输入应该是6个数字。我们这里把这六个输入的数字记为a1,a2,a3,a4,a5,a6。
使用几次ni指令使程序运行到<+14>的位置。我们可以发现,我们的a1,a2,a3
a4,a5,a6这六个数字分别存在%rsp,%rsp+0x4,%rsp+0x8,%rsp+0xc,%rsp+0x10,%rsp+0x14这六个栈上的位置中(不确定的话,可以输入几个123,456,789这样的独特数字来确定)
接下来我们看这一段代码
=> 0x0000000000400f1a <+14>: cmpl $0x0,(%rsp)
0x0000000000400f1e <+18>: jns 0x400f44 <phase_2+56>
0x0000000000400f20 <+20>: callq 0x401589 <explode_bomb>
第14行将我们输入的第一个数字a1与0x0进行比较,如果第一个输入a1比0小的话,炸弹就会直接爆炸!
因此,我们输入一个大于等于0的a1后,程序跳转到56行。
我们观察这一段代码,这应该是一段循环。
0x0000000000400f27 <+27>: mov %ebx,%eax
0x0000000000400f29 <+29>: add -0x4(%rbp),%eax
0x0000000000400f2c <+32>: cmp %eax,0x0(%rbp)
0x0000000000400f2f <+35>: je 0x400f36 <phase_2+42>
0x0000000000400f31 <+37>: callq 0x401589 <explode_bomb>
0x0000000000400f36 <+42>: add $0x1,%ebx
0x0000000000400f39 <+45>: add