CSAPP 实验2 : Bomb Lab
一、实验目的
本实验要求你使用课程所学知识拆除“binary bombs(二进制炸弹,下文将简称为炸弹)”,增强对程序的机器级表示、汇编语言、调试器和逆向工程等方面原理与技能的掌握。 这里的炸弹是一个Linux可执行程序,包含了6个阶段(或层次、关卡)。炸弹运行的每个阶段要求你输入一个特定字符串,你的输入符合程序预期的输入,该阶段的炸弹就被拆除引信即解除了,否则炸弹“爆炸”打印输出 “BOOM!!!”。实验的目标是拆除尽可能多的炸弹关卡。
每个炸弹阶段考察了机器级程序语言的一个不同方面,难度逐级递增:
阶段1:字符串比较
阶段2:循环
阶段3:条件/分支
阶段4:递归调用和栈
阶段5:指针
阶段6:链表/指针/结构
另外还有一个隐藏阶段,只有当你在第4阶段的解后附加一特定字符串后才会出现。
需要使用一些工具来反汇编炸弹的可执行文件并跟踪调试每一阶段的机器代码,从中理解每一汇编语言代码的行为或作用,进而设法推断拆除炸弹所需的目标字符串。比如在每一阶段的开始代码前和引爆炸弹的函数前设置断点。
二、实验过程及结果
phase 1
phase_1函数汇编代码:
第一阶段的汇编代码还是比较短的,很容易分析。通过call <strings_not_equal> 可以推测出是调用了字符串比较函数,那么是和什么比较呢?可以看到 mov $0x4023d0, %esi 这里的 0x4023d0 通过gdb的 x/s 0x4023d0 转化为
Slave, thou hast slain me. Villain, take my purse.
很明显这就是本题的答案,阶段一结束。
phase 2
phase_2函数汇编代码:
观察这段代码,我们可以注意到"phase_2"函数还引用了一个名为"read_six_numbers"的函数,很明显是想让我们输入六个数字作为密码,于是我决定查看该函数的汇编代码:
经过阅读,果然这段代码有对输入的判断,而且也有调用"explode_bomb"引爆炸弹的代码。
所以"read_six_numbers"的任务是读取6个整数,所以我尝试输入:0 1 2 3 4 5,完成call read_six_numbers ,使用layout regs查看寄存器值,layout asm 查看汇编代码,发现一些寄存器的值发生了改变,证明数据存到了对应的位置。
读取六个数字开始前:
读取六个数字结束后:
我们可以知道rsp 是栈指针,读取的六个数字分别存在了对应的地址空间中。
所以接下来回到函数"phase_2",程序会对输入的第一个值进行判断,如果小于0,炸弹直接爆炸,所以第一个数字的要求是大于等于0,我选择了0,因此可以通过。
因为rbx(ebx)是循环的计数器,以下用i代指rbx的值
将ebx的值赋给eax, 即eax = i
0x400ec6 <+59>: mov %ebx, %eax
M[R(rsp)+4*(i-1)] => nums[i] , eax += nums[i]
0x400ec8 <+61>: add -0x4(%rsp,%rbx,4), %eax
判断eax 是否 与 nums[i + 1]相等
0x400ecc <+65>: cmp %eax,(%rsp,%rbx,4)
总结, 最后判断的是: nums[i + 1] 是否与 (i + nums[i]) 相等
所以,只要我们的输入第一个值大于等于0, 有"0 1 3 6 10 15" (我的答案)这样规律的即可。
阶段二结束。
phase 3
phase_3函数汇编代码:
调用了sscanf函数,所以可以判断在这里读取了某些东西,向前看到了0x4025cf 转换成字符串是:
说明这道题我们要输入两个整数,重新输入: "1 3"进行尝试。
同时,在400f27处有一个跳转表:那么根据我的推测,这应该是类似 if else 的功能,根据第一个值的不同输入决定第二个值应该是多少。
400f27: ff 24 c5 40 24 40 00 jmpq *0x402440(,%rax,8)
分别在scanf之前和scanf之后使用layout regs读取寄存器中的值
scanf前:
scanf后:
下一步是rsp 指向的地址存的值与7比较 如果大于7就爆炸,所以我们看看rsp 指向的是什么:
很明显是我们第一个输入的数字,所以我们得到了结论: 第一个输入值要小于等于7.
经过stepi逐步调试,我们跳转到这里,发现 ( r s p + 0 x 4 ) 存储的是第二个数字。与 rsp + 0x4)存储的是第二个数字。与 rsp+0x4)存储的是第二个数字。与eax 进行比较,经过查看regs可知是565.
而当我们第一个输入其他值的时候,发现第二个数字比较对象也变化了。因此我们之前判断是正确的,第二个输入与第一个输入是有关的。
使用命令x /32xh 0x402400访问跳转表,得到:
在上面的图中我们看到有8个地址,分别对应0-7共8个数;第一个输入就限制在0-7。所以,根据我们的第一个输入可以知道,要跳转的地址,跳转之后会有一个值赋给寄存器eax,这个值必须要与我们输入的第二个值一样才能使得炸弹不爆炸,这与我们的猜想是一致的。
所以,这道题一共有8个不同的答案。在这里,我提供出我的其中一个答案:
5 753
阶段三结束。
phase 4
phase_4函数汇编代码:
我们发现有sscanf 和 func4 两个调用, 那么我们还是先看要求输入的格式:
得知该阶段期望输入两个整数,那么尝试输入"1 2",而且与之前一样,我们的输入被存储在rsp寄存器中存储的地址所指向的内存。
我们看到在0x401006 看到 ($rsp) 和 14比较。 如果大于14就要爆炸。
所以第一个输入小于等于14,符合,进入下一步。
观察可得,第一个输入赋到了edi上。再往下看,发现若eax不等于3,则炸弹直接爆炸;观察0x401028,发现若M[4+%rsp]也就是我们输入的第二个值不等于3则炸弹爆炸。所以输入的第二个数可以确定下来必须为3. 对于输入的第一个数,要求小于15并且要让func4输出eax=3。
func4的汇编代码如下:
观察可知,func4中在: 0x400fa0, 0x400fa2, 0x400fb3, 0x400fc9, 0x400fd5的位置上,对eax的值有影响。
很明显这是和递归有关的函数。所以我采取倒着看的方法。从retq指令往前看,因为0x400fbc是一个跳转点,所以再向前看到0x400fc9和0x400fd5可以跳转到这里。对于0x400fc9,相当于将eax2,不可能出现eax = 3的情况,所以func4在结束的时候不可能在这里退出;对于0x400fd5,这里相当于eax = 2eax+1,若eax=1,则eax会输出为3,我推测很大可能从这里退出;再分析其他可能发现都不符合eax = 3 的要求。
那么接下来我们的目标就是使得程序第二次进入func4时的最终结果有eax=1。同样的分析方法,仍然去看0x400fc9和0x400fd5,发现对于0x400fd5,若eax输入时为0则有eax输出时为1。那么我们的目标再次变成程序第三次进入func4时最终结果有eax=0。最后分析得出对于第一个输入:13 和 12 都是可以的。
我的答案是:13 3
阶段四结束。
phase 5
函数"phase_5"汇编代码:
和之前一样,通过sscanf读取,先查看格式:
所以phase_5要求我们的输入为两个整数,尝试输入"1 5".输入的值被存储在M[R(%rsp)]和M[R(%rsp)+4]。
0x401079-0x40107f,将我们输入的第一个值mod16并将其存到eax寄存器
0x401082检查第一个输入mod16后的结果是否是15,若是15则直接爆炸
0x40108c-0x4010a2是一个循环函数,当eax=0xf时退出循环;edx是循环的计数器,循环几次edx就等于几;ecx是eax的一个累加器,刚进入循环时初值为0,eax每次循环的值都会加到ecx上。这道题关键的关键在于0x402480这个地址,读取它你会发现这是一个数组:
这个数组为:
a[0] | a[1] | a[2] | a[3] | a[4] | a[5] | a[6] | a[7] | a[8] | a[9] | a[10] | a[11] | a[12] | a[13] | a[14] | a[15] | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
a | 2 | e | 7 | 8 | c | f | b | 0 | 4 | 1 | d | 3 | 9 | 6 | 5 |
后面判断的是edx是否等于3,若不等于3则直接爆炸,代表循环次数要求是3。也就是第三次循环结束的时候eax = 0xf 才可以。后面的逻辑是输入的数字得到对应的数组的值,那个值作为下一次数组的对应索引,所以顺序应该是a[2] -> a[14] -> a[6] ,第二个输入的值要和ecx比较,也就是这三个值的和,经过计算得到了是35.
所以答案是 2 35
阶段五结束。
phase 6
函数"phase_6"汇编代码:
开始看到调用的函数"read_six_numbers",所以可以知道要求输入是6个整数,我输入"1 2 3 4 5 6"。输入仍存在M[R(%rsp)]开始的24字节。
读取完输入跳转到0x401133之后程序会进入一个两重循环,他的作用是判断每个数字都要小于等于6,并且每个数字都各不相同。
在结束上面的二重循环后程序会跳转到0x401151,这里是一重循环,会遍历所有输入变量对其进行 nums[i] => 7 - nums[i]的处理。
之后,程序在0x40118f再次进入循环,这段循环中出现了一个地址0x6032d0,这个地址存储的是一个链表,其中存储一个值和一个指针,初始情况下指针指向下一个node。而我们输入的值决定着他们的排列顺序。
对于最后这部分代码:
要求的是比较两两相临的node 值,最终要求整个链表降序排列,所以答案是4 5 6 2 3 1.
阶段六结束。
phase secret
经过查找,发现函数"secret_phase"的唯一入口在函数"phase_defused",汇编代码如下:
直接看sscanf前面要求的格式,发现需要三个值,%d,%d,%s 所以我输出以下三个地址的值,很明显输入的答案是urxvt.
这样我们就进入了secret_phase 阶段。
查看函数"secret_phase"的汇编代码:
出现了"read_line", "strtoI"和"fun7"这三个调用函数,"read_line"和"strtoI"的功能很好理解,分别是读取输入和将输入的字符串转换成int类型的整数。
从函数"fun7"返回后,程序立即对寄存器eax保存的值进行判断,若其不为4则直接爆炸;若eax为4那么会跳到0x40129a,其中的地址0x402488在使用gdb读取后发现其中的存储的是一个字符串:“Wow! You’ve defused the secret stage!”。我们需要让程序经过函数"fun7"使得eax的值变为4即可
在进入函数"fun7"之前,有一个地址0x6030f0,我猜测也存了一些东西,所以打印出来,如下图:
这个很明显是二叉树,n1 存了两个子节点的地址,和自身的值,其他的也都类似。
接下来看函数"fun7"的汇编代码:
发现很明显是递归,和phase_4的很像,所以我还是倒着推的方式。先看return。发现在401246和401253两个位置进行eax的运算,分别是eax * 2 和 eax * 2 + 1, 和之前的一样。再对逻辑分析,得到结论是:输入值和当前顶点值比较,如果大于顶点就向右子树,小于顶点就向左子树。如果要求是4,所以上一次是2,再上一次是1,再上一次是0.那么逆推回来就是 右-> 右-> 左去遍历。
那么很明显答案是7.
secret_phase就此结束。
三、总结
一切解释权归ymc所有。
参考文献:
【0】【csapp,你该死啊!!!】https://www.bilibili.com/video/BV1XD4y1k7w7?vd_source=03969aa3a037addca80583bb48c484c2
【1】https://wdxtub.com/csapp/thick-csapp-lab-2/2016/04/16/
【2】https://arthals.ink/blog/bomb-lab
【3】https://www.bilibili.com/video/BV1vu411o7QP?vd_source=03969aa3a037addca80583bb48c484c2
【4】https://www.bilibili.com/video/BV1iX4y1N74k?vd_source=03969aa3a037addca80583bb48c484c2
【5】本作者