Bootstrap

HNU 计算机系统 bomblab(炸弹实验)

首先声明,这一个报告是按照自己分析的心理路程来撰写的,不一定全对,但只是作为参考的话,应该是可以的,你们能找出小问题、大错误,可以打在评论区,给大家伙们提个醒。

报告可以参考,但请不要全抄喏。

(本人也不会再更改内容)

(第一次写博客,格式可能会比较乱(╥╯^╰╥))

一、bomblab相关介绍

1.简介

       此次实验要求我们使用课程所学知识拆除“binary bombs”,增强对程序的机器级表示、汇编语言、调试器和逆向工程等方面原理与技能的掌握。

      一个“binary bombs”(二进制炸弹)是一个Linux可执行程序,包含了6个阶段。炸弹运行的每个阶段要求你输入一个特定字符串,你的输入符合程序预期的输入,该阶段的炸弹就被拆除引信即解除了,否则炸弹“爆炸”打印输出 "BOOM!!!"。

      实验的目标是拆除尽可能多的炸弹层次。 每个炸弹阶段考察了机器级程序语言的一个不同方面,难度逐级递增:

      ①阶段1:字符串比较

      ②阶段2:循环

      ③阶段3:条件/分支

      ④阶段4:递归调用和栈

      ⑤阶段5:指针

      ⑥阶段6:链表/指针/结构

      ⑦隐藏阶段:只有在第4阶段的解后加特定字符串才会出现

      为完成二进制炸弹拆除任务,我们需要使用gdb调试器和objdump来反汇编炸弹的可执行文件并跟踪调试每一阶段的机器代码,从中理解每一汇编语言代码的行为或作用,进而设法推断拆除炸弹所需的目标字符串。比如在每一阶段的开始代码前和引爆炸弹的函数前设置断点。

      实验语言:C;实验环境:Linux

2.实验步骤

2.1.获取bomb

      ①找到以tar文件的形式bomb187.tar,187是bomb的标识

      ②解压该tar文件(tar -xvf bomb187.tar)

            得到一个目录./bomb187,其中包含如下文件:

            README:标识该bomb和所有者(匹配自己的学号)。

            bomb:bomb的可执行程序。

            bomb.c:bomb程序的main函数。

2.2.拆除bomb

      本实验的任务就是拆除炸弹。一定要在指定的虚拟机上完成作业,在其他的环境上运行有可能导致失败。

      ①运行./bomb可执行程序需要0或1个命令行参数(详见bomb.c源文件中的main()函数)。如果运行时不指定参数,则该程序打印出欢迎信息后,期望按行输入每一阶段用来拆除炸弹的字符串,根据当前输入的字符串决定是通过相应阶段还是炸弹爆炸导致任务失败。

      ②也可将拆除每一阶段炸弹的字符串按行组织在一个文本文件中,然后作为运行程序时的唯一一个命令行参数传给程序,程序读入文件中的每一行直到遇到EOF,再转到从stdin等待输入。这样对于已经拆除的炸弹,就不用每次都重新输入,只用放进文件里即可。

      ③要学会单步跟踪调试汇编代码以及学会设置断点。你还要学会如何检查寄存器和内存状态。很好的使用调试器是你在未来的职业生涯中赚到更多money的一项重要技能!

二、工具使用     

      ①gdb调试工具

      ②objdump反汇编工具

三、准备工作

1.打开bomb.c文件

       发现main函数依次调用了phase_1到phase_6六个函数,但函数的具体代码被隐藏。可以知道从命令行输入的内容必须和phase函数里面的一样,否则炸弹爆炸。

2.反汇编可执行文件bomb

      objdump -d bomb > bomb.asm

      这样就可以在bomb.asm里面看到整个文件的汇编代码。

四、正式开始拆炸弹+分析汇编代码

1、phase_1

(1)汇编代码及逐条分析

①建立栈帧

②存参数到内存,其中应该有我们输入的字符串,还有一个0x804a1c4地址存放的内容,存放到了esp+4处。

③而调用函数之前,一般都是把这个函数要用的参数先存好,而 string_not_equal()函数,顾名思义,作用是判断两个字符串是否相等,那么我们在此处用gdb分别查看两个参数值。

设置断点,运行函数,先输入“abcd”试探一下

然后利用“x/s”查看eax和0x804a1c4处的内容

④可以发现,正好一个是存放的我们输入的,一个存放的是一串不认识的字符,到这里其实答案已经显而易见了,可以直接去试试答案,发现炸弹拆除了 。

⑤但是我们不止步于此,我们再进一步追踪它调用的函数的汇编代码,如下:

 

⑥可以发现,它的作用的确如我们猜想的那样,是比较两个字符串是否相等(具体分析理解汇编代码:就是先比较x和y的长度,相等则进一步取x和y的每一个字符进行比较,最后相等则返回0,反之返回1),然后它的确也传入且使用了两个参数,分别放在ebx和esi寄存器中,为了保险,我们再来查看一下: 

⑦到这里,答案已经被确认了两次,是它!是它!就是它!

“The moon unit will be divided into two divisions.”

那么phase_1炸弹已经拆除了

(2)验证答案

(3)伪代码

2、phase_2

(1)汇编代码及逐条分析

①首先,我们发现汇编代码鬼鬼祟祟地保存了一个地址,按理来说,一般调用其他函数的准备工作都是保存值,那么我们大胆猜测,这里肯定有个什么数据结构

②紧跟着,调用了read_six_numbers,再次顾名思义,是读取六个数字,为确保不出错,我们追踪一下汇编代码:

没冤枉它,它的确是call完sscanf()之后,立马判断eax和5的大小,>5则满足,≤5爆炸

(但是这里有一个小小的漏洞,就是如果输入7、8、9…个数,只要前6个数是对的,后面输入什么都能拆除炸弹,试了试,的确如此,如果改成判断eax是否=6,就可以解决啦

③回到phase_2的汇编代码,继续往下看,发现它先把esp+0x18处的值和0比较,=才不会爆炸,然后又把esp+0x1c处的值和1比较,=才不会爆炸。

到此,我们根据它向上移动4位访问下一个值的方式,可以知道,这应该是分配了一个连续的空间,还相邻差4(其他数据结构的key访问不止+4),且未受到小端法存储的影响,这种方式说的是谁?

我知道!数组!

并且我们还确定了,数组第一位和第二位为0和1。

④继续看,它开始求第一位和第二位的和,并且将它和第三位的值进行比较,=才不爆炸,从而我们得出了第三位为1。

⑤esi寄存器则存我们当前抵达了哪一位,判断是否超出了6位。未超出,则继续循环比较下一位是否等于前两位的和,全部满足才不会爆炸。

⑥公式总结出来:a[n]=a[n-1]+a[n-2](n≥3)

聪明或者愚蠢的小朋友都可以发现,这不就是兔子数列嘛!

真相只有一个 :0 1 1 2 3 5(后面的数加啥都行)

(2)答案验证 

(3)伪代码

 

3、phase_3

(1)汇编代码及逐条分析

①紧急观察!巨多跳转指令,肯定地猜猜,是Switch跳转表,好家伙才不久之前做了跳转表的小班,还热乎着。

②又神秘地保存了一个地址内容,联想①,应该是跳转表的首地址,先不管对不对,gdb查查看:

 

全是汇编代码中指令的地址,罪证齐全,是跳转表无疑了

③函数调用完sscanf()后,立即将返回值eax和1比较,≤爆炸,>存活。由此可见,所以我们需要输入的是两个数

④然后,它又将0x18+esp处的值和7比较,可以推断这里存放的就是i,将i和7比较,>7则爆炸,<0也会爆炸,在此控制i∈[0,7]。

⑤然后又因为跳转中的地址是按照i的大小,递增排序的,所以可以一一对应上,当i=?时,应该跳转到的指令位置。

⑥发现一个问题,这个汇编代码里,基本没有条件跳转,全是一堆jmp,具体计算发现,每一个case分支都没有break,全都是continue继续执行下一个case分支。

⑦匹配指令,且计算,可得对应关系:

0 346;1 -255;2 -158;3 -735;4 0;5 -735;6 0;7 -735

且判断当前我们输入的第一位数,算出来的结果,是否等于我们输入的第二位数,也就是要求输入有对应关系的两位数。

⑧然后它又来搞事情了,之前还说i=6、i=7可以,现在判断i>5,则爆炸,那么我们痛失两组答案

6 0;7 -735

(2)答案验证

正确的6组答案:

被过河拆桥的两组答案: 

(3)伪代码

4、phase_4

(1)汇编代码及逐条分析

①刚开始就存了两个内存地址,以及把0x804a3e3中的内容存放在栈中(这一个有猫腻),经验以及知识让我们推出,应该要输入两个数,且两个数存在esp+c和esp+8处。 0x804a3e3中的内容不知道,马不停蹄查看一下:

“%d %d”不就是两个整型的意思嘛,那应该就是输入两个整型数了。

③接着就调用了sscanf(),返回值eax为输入的个数。果不其然,立马和2比较,=才不爆炸;接着又把eax的值和1、4比较,控制eax的值∈[2,4]。

④调用func4之前,它保存了eax(此时存的是输入的第一位数),以及保存了数字“7”,那说明func4要用到这两个参数,接下来继续看func4的汇编代码:

 

⑤总结func4的功能:就是求根节点的值,且树高为7,但是根节点的值是由左子树和右子树得来,那么就进行了一个深度优先遍历,最终得出我们的根节点的值,如图所示:

图中用i=3举例,画出二叉树,求出根节点的值为99

递归公式为f(n,i)=f(n-1,i)+f(n-2,i)+i,i∈[2,4]

同理当i=2时,得到66,i=4时,得到132;

⑥回到phase_4,比较返回值(根节点的值)和我输入的第一位数比较,相等才不爆炸,从而得出三组答案:66 2;99 3;132 4

(2)答案验证

(3)伪代码

 

5、phase_5

(1)汇编代码及逐条分析

①首先调用了sscanf判断输入了几个数,>1则跳转,≤1则爆炸,并且由前面汇编代码中保存了两个内存地址,那么可以猜测应该是输入了两个数字。

②把x传入eax,并只保留低四位,若此时eax=15,则爆炸,!=15则不爆炸,edx自加一,用来记录循环的次数,防止超过15次。

③然后把0x804a240+4*eax(把上一次访问的值做为下一次访问的偏移量)处的值保存在eax里,并且用ecx来累加每一次访问的值,若当前访问的值!=15,则继续循环。

④直到当前访问的值=15,退出循环,再判断edx的值,进而判断是否访问了15次,最后再判断15个值累加的结果ecx是否等于我输入的第二个参数,等于则顺利结束函数,否则爆炸。

⑤查看0x804a240地址所存放的数,并且用gdb调试,从而画出对应的数组如下: 

我们发现里面没有5,且当6号位置为15,循环退出点。

我们之前说了,访问的时候,是把当前访问的值,作为下一次访问的偏移量

总结公式

实际上就是把a[n-1]的值,作为访问a[n]的偏移

比如说最后两位数a[n]=a[a[n-1]]=a[6]=15

⑥其次,我们发现其每一位累加起来为115,且数组里没有5,所以可以推断出我们输入的两个值,为5 115。

(2)答案验证

(3)伪代码

6、phase_6

(1)汇编代码及逐条分析

 ①第一个阶段:控制输入6个数,并且逐步循环访问每一个数,比较判断是否∈[1,6],然后又开始不断地循环比较6个数中,每两个数都不能相等,一旦不满足这两个条件,则会爆炸,esi和ebx的值用于控制访问不越界。

 ②第二个阶段:我们的眼睛像尺子一样,立马就发现了0x804c13c这个孤零零的地址数,那么必然得查看一波:

咦,这是啥?噢!链表!结构如下:

了解了这个之后,我们再看看第二阶段的作用,其实就是根据我们的6个数的顺序,把每一个节点自己的地址存到一个新开辟的地方去。

③第三个阶段:就是根据我们新开辟的空间里面存放地址的顺序,来重新构建链表的节点顺序,实际上的操作就是更换每一个节点中,存放的下一个节点的地址,来重构链表顺序,注意末尾节点存放的下一个节点的地址为0。 

④第四个阶段:功能是检查此时的链表是否权值从前往后,是按照递增的顺序进行的,只有这样才不会爆炸,那么怎么办?当然是控制我们输入的6个数的顺序,从而控制链表的重构排序。

⑤方法就是,根据原本链表的权值,进行排序,得到打乱的编号序列,那个就是我们需要的答案:2 1 3 5 6 4

(2)答案验证:

(3)伪代码

此处略方法就是:

  1. 用结构体实现链表,权值和编号已确定,根据输入的六个数顺序,把每个节点的地址存放到一个新开辟的数组里。
  2. 然后遍历这个数组,修改链表中每一个节点存放的下一个节点的地址,进一步修改我们的链表顺序
  3. 最后遍历判断链表的权值key是否有递增关系,不是则BOOM!

7、secret_phase

(1)进入方法:

 ①我们发现每次在phase函数里,写入命令finish,结束phase,则会立马进入phase_defused(),我们去康康它在弄什么幺蛾子。

先只看前面一部分,我们发现了一个孤零零的地址,老规矩,查看!

它好像在悄咪咪记录什么,结果我试了试每次输入一条字符串,它每次的值就+1,好了,它就是个在小本子上记录我当前输入了几条指令的家伙。

而且如果指令数!=6,它就会默默地跳转退出这个函数,不让你运行中间的汇编代码,但是以往我们输入了6条字符串直接结束了,那看来还有什么条件需要满足,继续往下看。

②哟,又来了个地址,0x804a3e9,看看:  

Emm,看来我们要输入两个整型,一个字符串才能进入。

再次来了个地址,0x804c4d0,看看:

这个就熟悉了,这不是我们phase_4输入的两个数嘛,和上面对比一下,少了最后一个字符串!

确实,最后调用sscanf之后,判断了一下eax,从而判断输入的个数是否是3个,紧接着调用string_not_equal判断了我们输入的第三个字符串,是否和0x804a3f2处存的内容一致,那查看一下:

 

 那就说明,我们phase_4少输入了一个字符串,为“DrEvil”,尝试输入一下:

 

好的,可喜可贺,我们成功进入隐藏关卡——secret_phase。

(2)汇编代码及逐条分析

 

①首先它调用read_line函数读取我们输入的一行字符串,且调用strtol函数,将其转化为长整型。

②且这个长整型数-1得≤0x3e8,接着传了两个参数,调用了func7,什么!?传了个孤零零的地址进去?好的,数据结构,我来了:

什么东西3个一组?第一个存权值key,第二个和第三个都存地址?

对的,是二叉树!

③紧接着,它调用了func7,我们也转移阵地:

用伪代码总结功能:

 (这个图好像我当时直接截屏的某个博主的图)

其实就是不断寻找,直到addr=0或val=x(我们输入的),并不断计算,函数最后的返回值放在eax里。

④我们回到secret_phase里:

我们发现,它只允许func7返回1,其他值则会爆炸,那就好办了,现在只需要知道func7如何生成1,那就只有右子树可以得到2*(0)+1,那么可以知道了,根节点的右子树必须返回0,什么时候返回0,就是我们输入的长整型=当前节点中的val。

顺着这一堆数据寻找:

如上图所示,我们找到了根节点的右子树它的key值,为0x32,转换为10进制,为50,这个就是我们需要输入的答案。

(3)答案验证

喵的,喜极而泣 ,全部完成,退出bomb的运行。

(4)伪代码

前面已经附上了,这里不再赘附。

五、心得感悟

花了两三天才弄完整个汇编代码的分析以及报告的撰写:

  1. 对汇编代码的理解可谓是更上7层楼(为啥是7 ? 因为有7个phase咯!)
  2. 虽然拆炸弹超级有趣!!!但是真的就是在崩溃和继续之间挣扎!!
  3. 对递归、Switch跳转表、条件分支、二叉树、链表等的汇编代码如何进行而已有了更为深刻的理解,勾起了我数据结构的回忆。
  4. 其实在分析前几个汇编代码的时候,我有的地方还没太搞懂,但是逐渐做到后面的phase,前面边边角角的小问题也完全弄懂了,所以说实践出真知这句话太对了!!
  5. 还有,要学会利用同学这个资源,一旦分析陷入了什么牛角尖或者实在被绕进去了,那就问问同学或老师,让问题不再问题。

;