深入理解计算机系统(CSAPP)的实验二是Bomb Lab。实验中有六道关卡,我们的任务是通过查看反汇编代码,在程序运行时,从键盘输入六条正确的字符串,才能通过这六道关卡。
第一关:phase_1
在Ubuntu终端下通过gdb bomb
指令开始调试bomb程序。
使用disas指令查看phase_1程序的反汇编代码:
从上述反汇编代码中看出phase_1
函数调用了strings_not_equal
函数和explode_bomb
,使用disas指令分辨查看这两个函数的作用,反汇编代码如下:
strings_not_euqal:
从strings_not_equal函数的反汇编代码可以看出,这个函数有两个输入参数,分别存在rdi和rsi寄存器中,这两个寄存器分别存的是两个字符串的起始地址。函数判断两个字符串是否相等:若相等,则返回参数寄存器eax为0,否则寄存器eax值为1。
explode_bomb:
从explode_bomb的反汇编代码不难看出这个函数就是产生boom的函数了,这样我们就知道,后续任务的主要目的就是不让程序进入这个函数即可。
重新回到phase_1
函数的反汇编代码,我们就能很容易知道这个函数的任务就是输入一个字符串,与存在地址($0x402400)中的字符串比较,若两个字符串相等则不会产生爆炸(将炸弹拆除)。在gdb中使用x/s 0x402400
指令查看该地址对应的字符串:
退出gdb,重新运行bomb
,第一条输入Border relations with Canada have never been better.
得到结果如下,表示第一关已经通过。
第二关:phase_2
同样的方法:首先进入gdb,使用disas指令查看phase_2
的反汇编代码:
从函数read_six_numbers
不难看出phase_2需要输入六个数字,存放于栈帧的前六个32位字中,之后程序运行分为下面几个步骤:
- 从
cmpl $0x1,(%rsp)
中看出第一个输入的数字是1,若不为1,会跳转到explode_bomb
函数产生爆炸; - 程序依次运行到
<+52>:
,执行指令lea 0x4(%rsp),%rbx
把第二个参数的地址传给rbx寄存器,指令lea 0x18(%rsp),%rbp
把最后一个参数的下一位地址传给rbp寄存器; - 之后程序运行到
jmp 0x400f17 <phase_2+27>
,跳转回<+27>
开始进行新的运算; - 函数段
<+27>
到<+32>
将上一个参数乘以2与下一个参数比较,若不等则产生爆炸; - 函数段
<+41>
到<+48>
判断六个参数是否全部读完,若读完则函数返回,否则跳转回第四步继续执行; - 若前五步没有产生爆炸,那么这一关成功通过。
由上述分析可以看出,我们只要输入一个以1开始,之后参数每次乘2的六个参数即可,也就是1 2 4 8 16 32
第二关phase_2通关结果如下:
第三关:phase_3
使用disas查看phase_3
函数的反汇编代码:
这个代码有点长,我们一步步来看。指令<+14>
将一个立即数传给了esi寄存器,接下来就调用了sscanf
函数,所以我们可以使用x/s
查看$0x4025cf这个地址上的内容:
可以看出这一关需要输入两个整数。可是之后就没有头绪了,我们可以在gdb中调试运行,查看各阶段寄存器的值,来判断这一关该怎样通过。
首先在phase_3位置设置断点:
之后运行run
,依次输入头两关的正确答案,到第三关时,通过上述的分析,我们先随便输入两个整数:
之后使用disas
指令可以查看断点后的一部分汇编代码,就是phase_3函数的反汇编代码。
使用stepi
指令可以单步调试,由于我们已经不需要知道<__isoc99_sscanf@plt>
函数的内容,我们直接在0x0000000000400f60 <+29>:
处设置断点,再continue
即可:
此时我们查看后续代码可以知道主要使用的(rsp+0x8)和(rsp+0xc)地址的数据,可以怀疑这里存的就是第三关输入的两个数。使用下面的一系列指令查看这两个位置的值:
可以看出我们的猜测是正确的。接下来单步运行,从以下几条指令可以看出,输入的第一个数必须小于7。
继续单步运行,由于我第一个数输入的是一,<+50>: jmpq *0x402470(,%rax,8)
这条指令使程序运行到了<+118>
,从下面的汇编代码可以看出,输入的第二个数必须等于0x137(也就是311)。
所以我们就得到了一个答案1 311
,重新运行bomb
,得到结果如下,表示第三关通过:
这一关应该还有很多答案,我这是通过单步调试找出来的答案,其实仔细回头看phase_3
函数,可以发现指令<+46>
到<+104>
类似于C++中的switch结构,分别第一个输入数的对应八种情况,表示当第一个输入数为0,1,2,3,4,5,6,7
时,第二个输入数应该分别为207,311,707,256,389,206,682,327
第四关:phase_4
使用disas查看phase_4
函数的反汇编代码:
这一关输入部分很像第三关,同样是输入两个整数。但是第四关主要的运算过程在一个新的函数func4
中,使用disas查看这个的反汇编代码:
从反汇编代码中看出函数自己调用了自己,应该是一个递归函数,但是查看下列反汇编代码,发现好像可以投机不需要进入递归,就能安全完成这个函数:
这一段代码的意思其实就是,如果%ecx==%edi,就直接返回phase_4,通过分析func4函数前面的赋值过程,可以很容易知道edi寄存器存的是输入的第一个整数,ecx寄存器得出的值0xe除以2,也就是7,所以我们就确定了第一个输入的整数应该为7。
再回到phase_4
的最后一部分代码,如下:
可以看出第二个输入数应该为0,所以phase_4
的一个答案为7 0
,由于我没有具体查看递归函数func
,所以应该还有其他正确答案可以通关。第四关通关结果如下:
第五关:phase_5
使用disas查看phase_5
函数的反汇编代码:
从下面一段代码可以看出,这一关的输入应该是一个长为6的字符串:
这样我们就可以先随便输入一个字符串:abcdef
,方便后面单步调试运行:
首先设置断点phase_5
,在运行run
,输入前四关的正确答案和第五关的abcdef
:
之后的代码中,下面这一段是一个loop:
我们在0x40108b
处设置断点,然后输入continue
继续运行程序,再单步运行,并查看寄存器的值,全部操作如下所示:
上述测试结果可以看出:在这个循环中rbx寄存器是输入的字符串的基址,循环依次取一个字符,获取该字符ASCII码的低四位存放在edx寄存器中。继续单步运行,出现一个地址0x4024b0
,查看如下:
可以看出这也是一个字符串(也可以看作字符数组)。
这样就能理解这一段循环的具体意义:按照输入的六个字符的ASCII码低四位为索引,从一个字符串中取出六个新的字符,存入栈帧(rsp+0x0)~(rsp+0x5)
处。
继续运行会发现一个新的地址,通过命令查看存放的是一个六位的字符串:
|
|
接下来要比较这个字符串和上述索引所得的新字符串,也就是说我们需要输入一个字符串,通过一个索引,或者一个新的字符串”flyers”,这个通过计算可以知道输入的字符串是ionefg
,我们重新运行bomb
程序测试第五关是否通过:
第五关也正确通过。
第六关:phase_6
这一关的反汇编代码有点长,我们分段来看:
|
|
最开始调用了一个read_six_numbers
代表输入是六个数字,这样就可以采用单步调试并查看寄存器内存的方式,来理解反汇编代码(先输入1 2 3 4 5 6
):
|
|
可以看出输入为六个32位数,且存入栈帧(rsp+0x0)~(rsp+0x14)
处。
之后一段是一个循环,我把每一句都换成类似C语言的注释,能更好的理解这段代码的作用:
|
|
根据翻译来的注释可以看出,这是一个二重循环:第一重循环确定每个输入都不大于6;第二重循环确定所有输入两两不相等。
接下来也是一段循环:
|
|
这段代码用7减去每一个输入,作为新的输入,运行程序再查看rsp寄存器地址存的六个32位数即可确认:
|
|
下一段代码中有一个地址0x6032d0
,我们先查看其中的内容:
|
|
结果类似一个结构体,下一段代码的作用就是使用我们输入的六位数对这个结构体重新排序,存入(rsp+0x18)处,可以设置断点查看重新排序的结果:
|
|
可以看出,是按照6 5 4 3 2 1
的顺序重新排列了,原因是之前经过了7-input的过程。
最后一段代码判断结构体中的第一个数字是否是降序排列,要排序的六个数是332,168,924,691,477,443
要达到降序排列,需要把第三个结点放在第一位,第四个放在第二位……这样得出的输入序列为3 4 5 6 1 2
,由于之前有一个7-input的过程,故正确答案应该是4 3 2 1 6 5
。
最终结果为:
|
|
所有炸弹都被拆除!