CSAPP_bomb_lab_report

来源:互联网 发布:银行 知乎 编辑:程序博客网 时间:2024/06/16 11:27

二进制炸弹实验报告

一、 各关卡解题思路
1、 第一关
第一关的主要汇编代码在phase_1函数中。程序将一个32位立即数和一个形参放到栈顶然后调用了strings_not_equal,可见这两个数据是strings_not_equal的实参。然而完全解读strings_not_equal函数是比较困难且得不偿失的。事实上,从名字上我们就可以推断这个函数的主要功能是比较两个字符串是否相等,如果相等返回0,不等则返回1。这样,我们可以判断出立即数$0x8049a0c和0x20(%esp)

是两个字符串的地址,其中0x20(%esp)是我们要输入的字符串,0x8049a0cgdbchar0x8049a0c处存放的字符串。

输入这句话,移除了炸弹

2、 第二关
可以直观的看见函数调用了函数,而这个函数中又调用了<__isoc99_sscanf@plt>函数,猜测这个关卡需要我们输入六个数字。

以上是主要的循环部分。循环次数为五次,用自然语言描述就是,依次比较了六个数与循环次数加一的乘积和下一个数字是否相等。且这六个数存储在0x18(%esp)开始的六个单元中。

从这三行代码可知,第一个数为1。所以六个数依次是1,2,6,24,120,720。

3、 第三关
中调用了<__isoc99_sscanf@plt>函数,实参中包含了地址为$0x8049dd2的数据,以char *打印可见:

因此,要求输入两个十进制数。根据上一题的经验,已知输入的数据将会放在0x18(%esp)开始的位置上。那么这里的两个数就是放在0x18(%esp)和0x1c(%esp)。
再往后可以看见:

这是本题的关键。程序将转向地址为0x8049a68 + 4 * %eax的指令继续执行,代表以下为switch语句。%eax或者说0x18(%esp),即输入的第一个数字是判断的标准。

虽然程序前半提示必须输入小于等于7的数,但最后又缩小了范围到5及以下。所以实际可以输入的数是0,1,2,3,4,5。以16进制打印了输入0~5时将会转到的程序地址。

观察了这些指令,发现这个switch语句中的情况都是fall through型的。于是为了方便和稳妥,我选择了地址最大的指令,也就是%eax为5的情况(可惜结果还是算错然后boom了)。具体算式是-0x20d+0x20d-0x387=-903,存在%eax中。

最后,程序将计算结果与我们输入的第二个数字比较,如果一致就解除炸弹。因此,5和-903是本关卡的一组解。

4、 第四关
第四关一开始输入的部分与前两关类似,查询$0x8049dd5处的字符串可知输入一个十进制数。继续观察代码,输入的这个数可能被存放在0x1c(%esp)。随后调用了函数。这个函数在函数体中继续调用了自己,可见它是一个递归函数。

递归函数的逻辑如上图所示,返回的数应该是7的n次幂,n就是传递给的参数。调用结束后,将返回的数字与0x157比较,如果相等,炸弹解除。所以,n为3,也就是说这一关的密码是3。

5、 第五关
可以看见,调用了,等函数,猜测需要输入的是一个字符串,且这个字符串存放在0x30(%esp)开始的单元中。另外程序要求字符串长度必须为6。

程序头尾包含了一些让人费解的语句,通过查阅资料发现,它们设定了一个哨兵值,防止输入的字符串太长溢出,破坏栈的结构,导致程序无法正常运行,返回。虽然难懂,但是并不影响函数主要功能的解读,可以暂时忽略它们。

继续看程序,可以发现有一个明显的循环结构。它的循环次数是6次,用自然语言解释就是,将0x30(%esp)~ 0x35(%esp)处存放的取最低四位,加上0x8049a88,得到的结果作为地址取得的数据再写入0x15(%esp)~ 0x1a(%esp)。

很自然的,我们会好奇0x8049a88处究竟放了什么数据,如果char *的形式打印,结果如下图所示。忽略后面恶意的调侃,实际有用的部分是So前面的一堆乱码。那么,可以想象程序实际上是利用了我们输入的字符的ascii码值的最低位得到了这串乱码中的六个字符。

接下来一段程序是已经非常熟悉的比较字符串,比较的是0x15(%esp)开始的六个字符组成的字符串和地址为$0x8049a5e的字符串。打印这个字符串:

giants这六个字符在乱码中分别为第f、0、5、b、d、1位,所以我们输入的六个 字符的最低位就必须是f、0、5、b、d、1。答案不是唯一的,我选择的六个字符是opekma。

6、 第六关
这一关的程序是比较长的,分几个部分来看。首先是熟悉的,输入六个数,并且存放在0x10(%esp)开始的单元中。接下去是两层循环。主要功能是判断输入的六个数是否都小于6,且各不相同。

循环结束后,程序跳转到0x8048e9b处继续执行。这一段程序开始是理解的关键。我们可以假设memory里0x804c1740x804c174处的数据发现是一个整数,也就是说这个链表结构包括一个整数和下一个节点的地址(指针)。两个数据块的地址相差8个字节。

为了方便说明,将它转化为c代码,如下所示。此处a[ i ]表示栈中存储的输入的六个数,ptr表示一个地址为$0x804c174的指针。b[ i ]表示0x28(%esp)开始的六个单元。换言之,如果设第n个数的值为xn,那么0x28(%esp)开始的第n个单元现在存放第xn个链表节点的地址。

        for(int i = 0; i < 6; i ++) {             if(a[ i ] > 1)                  for(int j = 1; j < a[ i ] ; j++)                     t = *(ptr + 8);             b[ i ] = a[ i ];        }

使用gdb工具,我们可以查阅这个链表中每个结点中存储的数据:

                                                                        第一个结点                                    第二个结点                                  第三个结点                                  第四个结点                              第五个结点                              第六个结点

接下去这一段程序可以说是非常有规律了。简单来说,它修改了链表的连接顺序,使它们按照0x28(%esp)开始的单元中存放地址的顺序连接,也就是说与我们输入的六个数的顺序有关。由于此时数据和地址的外表非常相似,注意不能搞混。

紧跟着又是一段循环,循环次数为5次,通过0x28(%esp)开始的单元存放的指针访问它们指向的结点,判断这个结点和它的后续结点中包含的值得大小关系,可以看到,链表必须是以降序连接的,否则炸弹爆炸。
已经知道链表中数值原来的顺序是948,755,847,815,96,378。要使链表获得正确的降序顺序,输入的值应该是1 3 4 2 6 5。

7、 彩蛋关卡
令人感到十分悲伤的是,艰难的搞懂了第六关之后还要面对一个不知道入口在哪里的secret phase。首先找到唯一调用了函数的函数。

可以看见以上语句中将0x804c74c中的值与6比较,猜测这个地址记录了解出炸弹的次数。为了验证这个想法,用watch的方法查询了里面的数据,程序运行前为0,调用一次

    fun7(int x, int *p) {            if(p == NULL)  return 0xffffffff;            if(*p == x)  return 0;            if(*p > x)  return 2 * fun7(x, *(p + 4));             if(*p < x)  return 2 * fun7(x, *(p + 8)) + 1;        }

正如前面所提到的,由于最后的返回应该是6,可以推测整个递归的计算公式是6 = ( ( (0*2) +1) *2+1) *2。因此是四层循环。设需要输入的数为x,比较的数的地址为p
第一层:x < 0x804c0c0(36) return 2 fun7(x, *(0x804c0c0 + 4))
*(0x804c0c0 + 4) = 0x804c0cc
第二层:x > 0x4c0cc(8) return 2 fun7(x, *(0x804c0cc + 8))
*(0x804c0cc + 8) = 0x804c0e4
第三层:x > 0x4c0e4(22) return 2 fun7(x, *(0x804c0e4 + 8))
*(0x804c0e4 + 8) = 0x804c138
第四层:x = *0x804c138(35) return 0
查询0x804c138处存储的数据 ,发现是35,这就是需要输入的数字。它小于1001和36,大于8和22,符合要求。

二、 一些收获
1、 gdb调试工具的简单应用
列出一些本次实验常用到的几个调试命令。
file filename
gdb
p *address
p/x *address
p (char *)address
b *address
d
d
info reg
r
c 等等
然而炸弹(且要扣分)的设定给熟悉gdb带来阻碍,很多时候因为怕炸掉而不敢尝试新的命令,如果之后还有机会在没有压力的情况下用gdb调试,可以做更多的练习。

2、 sscanf函数
这次的程序中包括了sscanf这个在各个编程语言中都比较常用的函数。查阅资料后了解了他的具体用法。sscanf与scanf类似,都是用于输入的,只是后者以键盘(stdin)为输入源,前者以固定字符串为输入源。在linux系统中成功执行的情况下返回的是成功转换的值的个数。定义是:int sscanf(const char *buffer, const char *format, [ argument ] … ); 具体用法见百度。
举例:
sscanf(“1 2 3”,”%d %d %d”,buf1, buf2, buf3);
成功调用返回值为3,即buf1,buf2,buf3均成功转换。
sscanf(“1 2”,”%d %d %d”,buf1, buf2, buf3);
成功调用返回值为2,即只有buf1,buf2成功转换。

3、 调用函数时栈的变化
当然我认为这才是最主要的做这个作业的目的。为了得到揭开炸弹的方法,不得不分析了大量(?)的函数。总结一下:调用者函数将要传递的参数存放到栈顶;调用函数,程序自动保存return address;被调用的函数建立自己的栈空间;从调用者的栈中拿到参数,运算后将要返回的值保存在%eax中;释放栈空间并返回原函数。大致的过程就是这样,根据需要可能也需要保护一些被复用的寄存器的值。

4、 指针
在做这个作业的过程中多次涉及到指针的操作,甚至还有指针的指针,让我十分苦恼。首先要明白寄存器的含义:它是一个存储空间的地址的代称(设定几个固定的存储空间并用寄存器的名字来指代它们),比如%esp,它对应的空间内存放的是栈顶的地址。其次一个难点是正确认识lea和mov两个命令。举例来说明:
lea 0x1c(%esp),%eax 的含义是 0x1c+%esp  %eax ;简单加法
mov 0x30(%esp),%eax 的含义是 *(0x30 + %esp) %eax ;需要取内容

三、 遗留的问题
主要是第五关出现的防溢出保护功能。
secret phase 如何进入的问题

0 0