前言
在皓哥的鼓励下,磕磕绊绊断断续续终于做完了BombLab,这个实验确实很有趣而且对我帮助很大,做完也非常的有成就感(HGNB)👏,因此决定写一篇博客记录一下学习的过程
首先作几点说明
1、由于每个学生的Bomb是随机的,而我是从网上其他人的github下载的lab,所以有可能你的Bomb与我并不一样,导致每个阶段的答案可能是不一样的,但是方法应该还是可以参考的
2、这个实验对提升汇编语言的理解能力以及自己调试代码的能力真的有很大帮助,所以强烈建议你自己独立思考并完成这个实验,即使多花些时间我觉得也是值得的
常用gdb命令
常用的命令可以在CSAPP书上的3.10.2节查阅,也可以在课程的pdf上找到,下面是我在拆弹过程中用的较多的命令:(首先在终端输入gdb bomb启动gdb)
run: 运行程序。较为有用的方式是带参数启动,例如run solutions.txt
,其中solutions.txt
存有你已经完成的阶段的答案,这样可以避免在攻略后阶段时每次都要输入前面阶段的答案。另外如果程序已经启动,gdb会提示你是否要从头开始运行,可以用这个方法避免我们被炸死(虽然我们不会因此扣分,但紧张感还是要有的 😃)b: 设置断点。常用形式是b functionName
和b *0xffffffff
,分别用来在函数入口处设置断点和在某个地址设置断点。这条命令执行后会提示breakpoint x at xxxxxxx
,然后可以用delete x
来删除该断点,disable/enable x
来禁用和启用断点。直接输入delete
可以一次性清除所有断点continue: (在程序停下时)继续程序disas:disas functionName
查看某个函数的汇编代码stepi/stepi n: 单步运行1步/n步(会进入函数)nexti/nexti n: 单步运行1步/n步(不会进入函数)info registers: 显示所有寄存器的值print: 打印信息。print $rdx
打印出rdx寄存器的值,print *(int*)0xffffffff
打印出0xffffffff
处的整数值x: 检查信息。x /s 0xffffffff
检查0xffffffff
处的字节,x /20d 0xffffffff
检查0xffffffff
开始的20个4字节并用十进制输出
在开始拆弹前,我们可以先查看函数的源代码bomb.c
可以看到这个程序一共有六个阶段,每一阶段会读取我们的输入并作为参数传递给当前阶段的函数,因此我们可以分别查看phase_1~phase_6的汇编代码来推测每一阶段的答案
Phase 1
函数phase_1
的汇编代码如下:
可以看出,它将%esi
置为0x402400
然后调用了strings_not_equal
函数,并比较结果,如果为0则返回,否则引爆。那么很自然的推测我们只需要输入这个炸弹相同的字符串即可,这里使用x /s
命令有奇效:
所以第一阶段直接输入这个句子即可
Phase 2
函数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
函数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
函数phase_4
的汇编代码如下:
这一阶段我们还是先来分析输入是什么格式,根据上一阶段的经验,这里我们从第29行就能看出,输入应该也是两个字符,而且如果第一个数大于14就会直接引爆
然后后面调用了一个叫func4
的函数,接着测试eax
是否为0,如果不为0则引爆。所以我们的目标就是要让func4
得到的结果是0,这里可以试着查看func4
的汇编代码,并分析怎样的输入可以得到0。不过我并没有看懂它的逻辑,所以我是直接通过一个个尝试发现,输入为7、3、0的时候返回结果为0。最后还有个比较0xc(%rsp)
是否等于0的语句,所以第二个输入为0即可
于是这一阶段也解决了,感兴趣的朋友可以试着分析一下func4
的映射关系究竟是什么样的
Phase 5
函数phase_5
的汇编代码如下:
这一阶段的函数也比较长,我们还是一步步分析。首先从29行以及调用了string_length
可以知道,我们需要输入的字符长度应该是6。接下来的41到70行进行了循环,把我们输入的字符串作为索引,从0x4024b0
这个地址取了一些字符存放到了rsp+16
到rsp+22
的位置,然后又调用了strings_not_equal
比较两个字符串,所以这里我们先看看0x4024b0
放的是啥:
可以看到是DrEvil的一句垃圾话
然后再看看另一个字符串是什么:
所以我们需要用上面那个很长的字符串拼出下面这个单词,很容易可以知道9对应f,567对应ers。但是l和y都不能用数字得到,所以我是将小写字母一个个输入看看会得到什么来求出ly对应的源,这里我的答案是9on567,应该也是不止一种答案
Phase 6
终于到了最后一个阶段,而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个地址:
rcx初始为rsp+32所指的节点,rax初始为rsp+40rdx=rax所指的节点rcx的next=rdxrax+=8rcx=rdx
总结一下就是:按照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
。至此,最后一阶段也成功解决!
Secret Phase
其实一开始看到这句话的时候我就在想,会不会真的像游戏彩蛋一样,有什么隐藏的东西没有发现,没想到真的有!不得不说CMU太强了
进入方法
那么如何进入隐藏阶段呢,首先我们查看phase_defused
的汇编代码:
注意到,第108行有一个对secret_phase
的调用,也就是说解除每个阶段后都会进行判断是否进入隐藏阶段,那么我们再来分析进入的方法。首先第一个条件就是第20行,0x81(%rip)
必须是6,否则的话函数就直接返回了,所以我们可以在这个地方打个断点,看看每次解除完之后这个值分别是什么:
可以看到是一个地址,而且后面有一个注释,我们猜测这个是一个函数的返回结果。先看看这个地址里是什么:
不难看出,这个函数返回当前解除的阶段,也就是说,隐藏阶段只有解除第6个阶段后才会进入。继续看后面,我们重点关注0x402619
和0x603870
这两个地址即可,打印出来发现是这样:
第一个结果应该是对scanf
的输入格式控制,而第二个结果发现是第4阶段的输入,结合第59行,我们发现这一阶段其实可以读取两个整数和一个字符串,而我们第4阶段只输入了两个整数。那么隐藏阶段的进入方法就应该是在第4阶段的答案后面再加上一个字符串,而这个字符串在后面的代码中可以得知:
所以我们只需要在第4阶段输入7 0 DrEvil就可以进入隐藏阶段了,我们先在secret_phase
打一个断点然后试试:
果然,程序提示我们已经找到了隐藏阶段,那么下一步就是解除它!
解除方法
首先查看secret_phase
的汇编代码
可以看出0~24
行读取了我们的输入并将它转换成了一个整数,容易推测rax
存放的就是我们的输入,之后令rax=rax-1
并且跟0x3e8
进行比较,如果超过就引爆炸弹,否则就将我们的输入作为第一个参数,将0x6030f0
作为第二个参数调用fun7
,最后看fun7
返回的结果是不是2,如果不是2就引爆炸弹,所以关键还是fun7
这个函数。这里我们就先打个断点然后随便输入一个数字,查看一下fun7
的汇编代码
其实看着还是挺简单的,这里值得注意的就是第15行这个代码,跟第6阶段用到的链表结构是一样的,很自然的我们就想到查看一下这一片0x6030f0
这片内存
我们会发现,这一大片内存中,每个结构大概分成3个部分,前4个字节是一个整数,然后4个字节是一个地址,接下来4个字节是另一个地址,最后4个字节是补齐的空字节,也就是说大概是一个这样的结构
struct Node{int val;Node* first;Node* second;};
再结合fun7
的汇编代码,我相信将它翻译成C语言并不困难,这里我就直接展示我自己反汇编的代码
其中我省略了第一个参数x
直接作为全局变量使用了,因为我们输入的那个数字在这个函数中是不会改变的。然后分析fun7
得到2的方法,最后一步肯定是从下面的分支返回2,所以倒数第二步就是从第一个分支返回1,再上面一步就是从第一个分支返回0,那么我们就依次分析
初始节点是0x6030f0
,第一次要进入下面的分支,所以会对node->first
调用fun7
。那么看下一个节点0x603110
,这一步我们要走第一个分支,所以会对node->second
调用fun7
。于是再看下一个节点0d603150
,它的值是0x16
,而我们在这一步就需要返回0了,所以我们的x
应该跟它相等,也就是x=22
。综上,隐藏阶段输入22
即可成功解决!
总结
这次实验累积的经验如下(不一定正确,欢迎探讨)
看汇编代码时,可以根据循环、函数调用等等把整体分成一个个小的部分再去分析有的地方看不懂可以先不去探究细节,把某些代码看作一个整体再去思考没有头绪时多试试各种输入多使用print
和x
命令查看内存的状态
附上最终答案和运行截图:
如果觉得《《深入理解计算机系统》--BombLab学习笔记(含隐藏阶段)》对你有帮助,请点赞、收藏,并留下你的观点哦!