在皓哥的鼓励下,磕磕绊绊断断续续终于做完了BombLab,这个实验确实很有趣而且对我帮助很大,做完也非常的有成就感(HGNB),因此决定写一篇博客记录一下学习的过程
首先作几点说明
1、由于每个学生的Bomb是随机的,而我是从网上其他人的github下载的lab,所以有可能你的Bomb与我并不一样,导致每个阶段的答案可能是不一样的,但是方法应该还是可以参考的
2、这个实验对提升汇编语言的理解能力以及自己调试代码的能力真的有很大帮助,所以强烈建议你自己独立思考并完成这个实验,即使多花些时间我觉得也是值得的
常用的命令可以在CSAPP书上的3.10.2节查阅,也可以在课程的pdf上找到,下面是我在拆弹过程中用的较多的命令:(首先在终端输入gdb bomb启动gdb)
run solutions.txt
,其中solutions.txt
存有你已经完成的阶段的答案,这样可以避免在攻略后阶段时每次都要输入前面阶段的答案。另外如果程序已经启动,gdb会提示你是否要从头开始运行,可以用这个方法避免我们被炸死(虽然我们不会因此扣分,但紧张感还是要有的 )b functionName
和b *0xffffffff
,分别用来在函数入口处设置断点和在某个地址设置断点。这条命令执行后会提示breakpoint x at xxxxxxx
,然后可以用delete x
来删除该断点,disable/enable x
来禁用和启用断点。直接输入delete
可以一次性清除所有断点disas functionName
查看某个函数的汇编代码print $rdx
打印出rdx寄存器的值,print *(int*)0xffffffff
打印出0xffffffff
处的整数值x /s 0xffffffff
检查0xfffffff
处的字节,x /20d 0xffffffff
检查0xffffffff
开始的20个4字节并用十进制输出在开始拆弹前,我们可以先查看函数的源代码bomb.c
可以看到这个程序一共有六个阶段,每一阶段会读取我们的输入并作为参数传递给当前阶段的函数,因此我们可以分别查看phase_1~phase_6的汇编代码来推测每一阶段的答案
函数phase_1
的汇编代码如下:
可以看出,它将%esi
置为0x402400
然后调用了strings_not_equal
函数,并比较结果,如果为0则返回,否则引爆。那么很自然的推测我们只需要输入这个炸弹相同的字符串即可,这里使用x /s
命令有奇效:
所以第一阶段直接输入这个句子即可
函数phase_2
的汇编代码如下:
观察这个函数,我们会发现第九行调用了一个函数read_six_numbers
,那么可以推测这一阶段需要输入6个数字,我们可以先随便输入6个数字试试(我输入了1 2 3 4 5 6)然后将程序运行到0x0000000000400f0a
这一行,我们看到第14行拿rsp
所保存的地址对应的数与1进行了比较,因此我们可以先查看一下这里放的是些什么:
可以看到,从栈指针地址往上连续存放了我们输入的六个数的地址(这个发现会多次用到),因此(%rsp)
对应的就是输入的第一个数,由此断定第一个数必须是1,否则就会跳转到引爆炸弹的函数。
确定了第一个数再继续看后面的部分,跳转到52行之后程序将rbx
置为rsp+4
的地址,rsp
置为rsp+24
的地址,正好是第二个数的起始地址到第六个数的地址的最后,可以猜测这里应该是在为循环做准备
接着程序跳转到27行,将eax
置为-0x4(rbx)
,也就是第一个数,下一个指令将eax
的值翻倍,再下面一条指令将eax
的值与rbx
对应的数进行了比较,如果不相等则引爆炸弹
到这里已经大概可以猜到,这六个数应该是以1为首项,2为公比的等比数列,所以输入1 2 4 8 16 32,第二阶段完成
函数phase_3
的汇编代码如下:
第三阶段的函数看起来有点长,我们可以慢慢分析一下。首先看到第24行调用了sscanf
来读取输入,然后判断eax
是否大于1,如果不是则引爆,我们可以根据这个确定应该输入多少个字符。先输入1个数字然后运行到第29行查看一下:
可以看到输入一个数的时候eax
的值为1,不满足要求,那么我们可以推测这一阶段应该是要输入两个数字
再看后面,首先比较了0x8(%rsp)
与7的大小,如果超过7会直接引爆,然后程序跳转到了一个不知名的地方,并且以输入的第一个数*8为索引的偏移量,而且查看下面的代码我们会发现有很多行对eax
的赋值语句以及一个跳转语句,结合上面所有的线索可以联想到这一大段应该是一个switch语句,会根据不同的分支给eax
赋不同的值,最后都会走到拿它跟0xc(%rsp)
比较。
根据上一题的经验,这个地方的值应该是我们输入的第二个数。所以这一阶段,我们只需要任选某一个分支,找到这个分支对应的值就行了。比如输入1之后,我们逐步单步运行,会发现程序走到了0x0000000000400fb9 <+118>: mov $0x137,%eax
这一行,那么第二个数就应该输入0x137
也就是311
,当然用别的分支也是可以的
函数phase_4
的汇编代码如下:
这一阶段我们还是先来分析输入是什么格式,根据上一阶段的经验,这里我们从第29行就能看出,输入应该也是两个字符,而且如果第一个数大于14就会直接引爆。
然后后面调用了一个叫func4
的函数,接着测试eax
是否为0,如果不为0则引爆。所以我们的目标就是要让func4
得到的结果是0,这里可以试着查看func4
的汇编代码,并分析怎样的输入可以得到0。不过我并没有看懂它的逻辑,所以我是直接通过一个个尝试发现,输入为7、3、0的时候返回结果为0。最后还有个比较0xc(%rsp)
是否等于0的语句,所以第二个输入为0即可
于是这一阶段也解决了,感兴趣的朋友可以试着分析一下func4
的映射关系究竟是什么样的
函数phase_5
的汇编代码如下:
这一阶段的函数也比较长,我们还是一步步分析。首先从29行以及调用了string_length
可以知道,我们需要输入的字符长度应该是6。接下来的41到70行进行了循环,把我们输入的字符串作为索引,从0x4024b0
这个地址取了一些字符存放到了rsp+16
到rsp+22
的位置,然后又调用了strings_not_equal
比较两个字符串,所以这里我们先看看0x4024b0
放的是啥:
可以看到是Dr.Devil的一句垃圾话
然后再看看另一个字符串是什么:
所以我们需要用上面那个很长的字符串拼出下面这个单词,很容易可以知道9对应f,567对应ers。但是l和y都不能用数字得到,所以我是将小写字母一个个输入看看会得到什么来求出ly对应的源,这里我的答案是9on567,应该也是不止一种答案
终于到了最后一个阶段,而phase_6
的代码也没有让我们失望:
——长度甚至超出了一页。这一阶段也是最为困难和花时间的一个阶段,我们可以根据各种jmp
的循环,将函数大致分成几个部分,然后逐个分析
首先很明显,第18行告诉我们输入应该是6个数字。根据前面的经验,我们输入的6个数字应该位于rsp
~ rsp+24
这块区域内
然后通过观察,我们可以发现,32行到93行应该是一个循环,这个循环让r13d
每次加4从而用eax
遍历输入的6个数。而且对每个数字减1之后和5进行了比较,如果大于5就会引爆,所以这6个数都要小于等于6。r12d
作为索引每次加一,到6跳出循环
接下来从62行到87行可以看出应该是更深层次的一个循环,这里用ebx
对当前元素后面的元素进行了遍历,并且跟当前数进行了比较,如果相等则引爆炸弹,也就是说每个数后面的数都不等于这个数
所以这几行总结一下就是:输入的6个数≤6且互不相等,即它们是1到6的一个排列
接着100到121行又是一个循环,这几行比较容易,可以看出是用7-x替换了x。到这里可以看作是第一个部分,也就是对输入的限制和处理
第二部分大体上还是一个循环,依然是用ecx
去遍历了栈上的6个数,rsi
每次增加4,如果到了24则跳出循环,这个循环做的事情就是在rsp+32
开始每8个字节存一个地址。我们可以重点关注一下这行指令:
0x0000000000401176 <+130>: mov 0x8(%rdx),%rdx
它取了rdx+8
这块内存,又赋给了rdx
,是不是觉得很像链表的node = node->next
?实际上我们可以查看一下这块内存来验证一下:
注意到,0x6032d8
存放的数值0xe0
0x32
0x60
正好就是第三行的地址0x6032e0
,因此这块内存其实就是一个链表的结构!前4个字节是一个整数,下面4个字节可能是id,最后8个字节是下一个节点的地址。有了这个发现之后,我们剩下需要做的就是分析代码存放地址的规律
从163 166行可以知道,这段程序先将当前值v与1进行比较,如果是1就直接将起始地址0x6032d0
放到rsp+32
开始偏移量为索引*8的地址去(143,148行。索引表示的是当前在处理第几个数字),否则就找链表的下一个,一直到第v个节点(130~137行)。
总结一下:假设当前的6个数分别为x1, x2, x3, x4, x5, x6,那么rsp+32
开始的这段空间分别存放第x1个节点的地址,第x2个节点的地址……第x6个节点的地址。到这里可以看作第二个部分,也就是节点地址的存放
最后一部分就是从183行开始到程序的结束,也就是我们需要满足的条件了。不难看出rax
和rsi
被用作了循环的起始和终止条件,183到212行是一个循环,用rax
来遍历后面5个地址:
总结一下就是:按照6个地址的顺序依次给链表排了序,也就是说rsp+32
所指向的节点将成为头节点,它的下一个节点是rsp+40
所指向的节点,以此类推
230到257行是最后一个循环,依次将后一个节点的数值跟当前节点的数值进行了比较,如果比当前节点大就引爆炸弹,因此我们构造好的链表应该是降序排列。根据之前检查的节点的值,它们的大小顺序应该是:3(0x39c)->4(0x2b3)->5(0x1dd)->6(0x1bb)->1(0x14c)->2(0xa8)
,反推我们的x1~x6分别为:3,4,5,6,1,2
,所以我们最初的输入应该是7减去它们,也就是4,3,2,1,6,5
。至此,最后一阶段也成功解决!
这次实验累积的经验如下(不一定正确,欢迎探讨)
print
和x
命令查看内存的状态