Bomb Lab解析
前置知识:
- 汇编
- GDB
Bomb Lab解析
前言
Bomb lab是《深入理解计算机系统》配套实验之一。可执行程序包含着“炸弹”,必须输入六个正确的字符串来进行拆弹。字符串必须通过对程序的汇编码进行分析来得到。本文通过分析拆弹流程,对反汇编技术技巧进行总结。
准备
进入CSAPP LAB 官网进行文件的下载。自学者点击self-study handout下载tar压缩包。解压后得到3个文件,Bomb可执行文件,Bomb.c主函数文件,与README。
安装gdb进行反汇编准备。
观察主函数文件,根据函数来打断点。例如在第一阶段主函数有:
1 | input = read_line(); |
read_line()该行在文件第73行,就可以先命令行执行gdb bomb,再break 73来打断点,在执行run之后,就会在该地方停顿下来,便于调试。
常用gdb指令:
i r xxx 查看xxx寄存器存储的信息。
x xxx查看xxx内存位置的值。该指令可以带参数来改变值的输出格式。
disas xxx将xxx函数或者xxx内存位置的函数反汇编。可以查看对应函数的汇编码。
phase_1
第一阶段的炸弹,主函数代码如上所示。对phase_1函数进行反汇编,可以得到如下:
1 | 0x0000000000400ee0 <+0>: sub $0x8,%rsp |
rsp为栈顶指针,第一行减去栈顶指针是为函数留出栈空间,可以忽略。
第二行mov了一行数据,并且后面调用了strings_not_equal函数。eax为函数返回值,test函数用于检测返回值是否为0。后面je指令为0则跳转到+23即add行,不为0则爆炸。
那么我们猜测0x402400位置存放了字符串。当输入字符串和该字符串相同则不爆炸。通过x指令可以看出该内存位置为:
1 | (gdb) x /s 0x402400 |
输入该字符串,即可拆弹成功。
phase_2
同上,对phase_2反汇编可以得到如下:
1 | Dump of assembler code for function phase_2: |
汇编码较上个阶段明显变长,这里分段解析。
可以看到首先调用了read_six_numbers来进行数据的读取。可以猜测并证实该函数作用为读取6个数,并放入栈中。
第一个引爆出现在cmpl 0x1 (rsp)后。即将栈顶元素与1相比较,不相等则爆炸。那么可以得到,第一个数为1。
在jmp之后将0x4(%rsp)与0x18(%rsp)存放了起来。根据一个数占4个字节可以得出,前者为存放下一个数值到rbx,后者为六个数的边界。
再次jmp之后可以看出,eax存放了rbx的前一个数,并且乘以2了。cmp即将前一个数×2与本数字相比较,不相等则爆炸。那么可以得到后一个数是前一个数的2倍的规律。
后面的代码不言而喻,依次进行数的比较,最后边界检查,到达边界则退出。那么可以得到拆弹代码:
1 | 1 2 4 8 16 32 |
phase_3
首先看代码:
1 | 0x0000000000400f43 <+0>: sub $0x18,%rsp |
代码依旧很长,分部分进行解析。
最开始调用了sscanf,sscanf的字符串可以通过前面的mov指令猜出,位于0x4025cf。查看可以得知,该字符串为:
“%d %d”
读取了两个数,加上前面两个lea指令,可以猜出数存放在0x8(rsp)和0xc(rsp)。
第一个引爆在cmp 0x1 eax,可以得知这是根据sscanf返回值来爆炸。当sscanf匹配数字大于1则不爆炸。
跳转后进行了cmpl $0x7,0x8(%rsp),即第一个值和7进行比较。大于则爆炸。即第一个输入值需要小于等于7。
下一个将第一个值存入eax,并使用jmp跳转。jmpq star0x402470(,%rax,8)。我们可以先看看0x402470存放的是什么:
1 | (gdb) print *(0x402470) |
4798268 = 0x400f7c,即下一行。那么根据地址可以得到,该跳转是利用第一个输入的值作为索引进行跳转,跳到对应指令。
下面结构都是类似的,将一个常数放入eax,并且跳转到与0xc(rsp)进行比较。相等则不会爆炸。以f7c行举例,对应值为0xcf=207那么可以得到一个解为:
1 | 0 207 |
phase_4
同上,看代码:
1 | 0x000000000040100c <+0>: sub $0x18,%rsp |
sscanf与上个阶段一样,比较返回值并且跳转。同时将第一个数与0xe进行比较,可以得到不大于0xe才能不爆炸。
然后将0xe,0x0和数组第一个值放入相应寄存器,调用func4,最后检查返回值是否为0。可以看出需要继续解析func4。func4代码如下:
1 | 0x0000000000400fce <+0>: sub $0x8,%rsp |
func4代码使用了递归,不是很容易理解,这里我对着每一行翻译了一下,写了个c语言程序,代码如下:
1 | int func4(int edx,int esi,int edi) |
由于是一行一行翻译的,理解不困难。第一个数必定小于等于11,那么可以直接带入值遍历进行计算,实践得出返回值为0的情况。当输入为0 1 3 7时候返回值为0。
接下来代码不需要过多解释,即验证第二个数是不是为0。那么可以得到一个解:
1 | 0 0 |
phase_5
代码如下:
1 | 0x0000000000401062 <+0>: push %rbx |
分步骤来进行解析。首先可以看到调用了string_length,后面检测了输入长度是否为6。即输入长度为6的字符串。
经过一番跳转,到达了movzbl (%rbx,%rax,1),%ecx。经过查看可以得知rbx存放的是输入的字符串,rax为索引,最开始为0,然后经过传输后,到达了and 0xf edx。即将该字符ascii码与0xf取与,即取后四位。
然后movzbl 0x4024b0(%rdx),%edx,即以and后的值索引并覆盖该寄存器,在该内存位置可以看到字符串为:
1 | (gdb) print (char*) 0x4024b0 |
覆盖寄存器后另一个mov,即将该字符放入栈。在之后指进行六次循环,得到新的六个字符。
在得到新字符串后又调用了string_not_equal,而目标字符串则在0x40245e,查看可以得知:
1 | (gdb) print (char*) 0x40245e |
那么该输入的字符串,后四位所形成的索引需要构成flyers新字符串。以0开始计数,flyer分别在原字符串的位置是9 15 14 5 6 7 。
经过查表可以得知,要使ascii后四位等于上述数列,一个可行的答案为:
1 | )/.%&' |
phase_6
最长以及六个炸弹中最难的一个。我写了一套完整的注释来方便理解。如下:
1 | 0x00000000004010f4 <+0>: push %r14 |
具体注释如上,该阶段使用了类似于链表的操作。通过输入六个数字,然后使用7减去每个数字,再将数字的索引顺序匹配为链表的连接顺序,再观察链表里的值实现降序排列。
再本次中,链表的值如下所示:
1 | (gdb) x /12xg 0x6032d0 |
由于使用32位寄存器,只用看后八位进行排序。39c>2b3>1dd>1bb>14c>0a8 即为3 4 5 6 1 2 。在进行7-之后可以得到答案:
1 | 4 3 2 1 6 5 |
隐藏关卡
实在写不动了,据说是个二叉树。我放弃了。
总结与心得
本次实验主要是熟悉了汇编语言以及反汇编流程,熟悉了gdb的使用。最开始做lab的时候,我对汇编完全没有任何了解,从最开始的每一行都需要百度,到最后理解了每一行的含义,我确实了解了很多计算机相关知识。总而言之我自己认为这次收获还是很大的。有人说汇编是最后的底线,我之前一直觉得汇编对于我这种写高级语言的没什么关系,毕竟编译器优化后的汇编本来就很难读得懂,但现在觉得只要默认不优化的情况下,多了一种找bug的方法。