CSAPP:Bufbomb

from wordpress, 放 tietuku 的图都挂光了

关于 CSAPP 的各个经典的 Lab ,网上的解答已是一搜一大堆。本文的目的是记录自己亲身做完后的一些感悟,不过多探讨具体的做法,而是归纳一些平时看书不太注意的,而需要在 Lab 里理解透彻的地方,和做 Lab 时需要默认知道的一些信息和经验,以便少走弯路。在此基础上略谈各题做法。

我的解题主要参考了这篇文章,尽管网上有许多解答,但这篇图文并茂的博文给我带来的帮助显然是最大的,此外还有这篇文章,通过对比两篇文章,我才能更好理解最后一题。在此表示感谢。

在我看来,这个 Lab 的主要目的在于,使你对栈帧的结构、调用过程中的寄存器变化(%esp,%ebp,%eax)有更加深刻的了解。在这个 Lab 里你必须要对 leaveret 指令的具体操作有了解,而不能仅仅像上一个 Lab:Binarybomb 那样,仅仅将它们抽象为回到调用过程前的状态。因为通过缓冲区溢出,你已经破坏了原来的栈帧结构。

关于解题,你需要知道的一些知识:

  1. 栈帧结构

    当调用一个过程后,栈帧结构如下所示

    ......
    传入参数2
    传入参数1
    返回地址
    旧的 %ebp
    ......
    栈顶

    其中 %ebp 指向旧的 %ebp%esp 指向栈顶

  2. 本 Lab 中输入字符串的保存位置和合法的字符串最大长度

    getbuf 函数的代码如下(图挂)

    其中调用了 Gets 读取输入,但这个大概和系统函数差不多,就不深究了。观察之前系统为这个函数分配了 0x38 的空间,但这并不代表字符串允许长度为 0x38 。下一行的 lea 指令计算了 %ebp-0x28 的值。这里先验地说明,这个值,即旧的 %ebp 往下 0x28 的空间里,才是存储字符串的地方。如果字符串超出了这个限制,则会继续向上覆盖旧的 %ebp ,返回地址,etc。 同时这也说明真正合法字符串的长度并不是 PDF 给的代码中规定的值。 0x28 即十进制 40 说明最多 40 个字节,那么 41-44 字节就覆盖了旧的 %ebp , 45-48 字节覆盖了返回地址。最后一题的 getbufn 也是同理(开始做的时候直接以为是 512 但其实是 520 所以被坑死)。 那么如何查看输入的字符串?以使用 GDB 为例,在 0x804940d 设下断点,在此处可以查看 %ebp 的值,其减去 0x28 即字符串的起始地址。也可直接查看 %eax ,先验认为从 Get 返回后, %eax 即保存了返回值的地址。查看时要注意代码为p (char *) %eax,因为 %eax 的值已经是字符串的地址,不能再在 %eax 前加 *

  3. leaveret 指令

    ret 指令前必须要 leave ,否则无法完全恢复调用前的状态,但也可以自己手动模拟这两个指令的效果。 leave 指令等价于:

    movl %ebp, %esp  # %esp 指向 %ebp 指向的位置,即旧的 %ebp
                    # 由于 %esp 就是栈顶指针, pop 和 push 都相对与这个操作,
                    # 所以相当于 %esp 下面的东西都没了
    popl %ebp        # 注意 %esp 指向旧的 %ebp ,所以将 %ebp 恢复为旧的 %ebp
                    # 同时 %esp+4,指向返回地址

    ret 指令的效果为从栈中弹出一个地址,并跳转到这个地址 因为此时 %esp 指向返回地址,因此程序跳转到返回地址,此时 %esp 继续 +4 ,指向返回地址的上一个

    利用 ret 的性质,本 Lab 中后几题都要用到一个技巧,在这些题中我们要自己输入汇编代码,再修改 getbuf 的返回地址,让程序执行我们输入的代码,然后再 ret 到别的函数中。但先让程序跳转到我们输入的地方已经使栈帧结构荡然无存,如何再让其 ret 到一个特定位置?使用如下代码:

    push $0xabcdefg
    ret

    这段代码使程序 ret 到地址 0xabcdefg 处。知道了 ret 的具体效果,应该不难理解它的原理。

  4. 小端法

    这只是一个小提醒了,直接输入地址作为 char 时要反一下,但如果是通过反编译生成的 char 就不用了,因为编译时系统已自动生成。看一下反编译出来的 asm 文件就能明白。

下面开始题目相关:

Level 0:Candle

要求执行完 getbuf 后跳转到 smoke 中,只要覆盖掉返回地址就可以,把原来的返回地址改为 smoke 函数的起始地址,本机上是 0x08048c8c ,这样答案可写成

00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00
8c 8c 04 08

前面 40 个是正常存储的字节,后 4 个是旧的 %ebp 区域,最后 4 个就是返回地址。前面 00 只要是除了 ’n’ 任意均可,最后写成 smoke 起始地址的反序。

Level 1: Sparkler

跳转到 fizz 中,但是这时要把你的 cookie 作为参数传进去。第一感觉参数放在返回地址上面,但实际上却不是。 ret 完事后 %esp 指向返回地址上面一个地址,然后执行 fizz 时,先 push 了 %ebp ,此时 %esp-4 ,即原来保存返回地址的地方,然后 mov 指令将 %ebp 也指向 %esp 指向的地方,再取了 %ebp+8 指向的值作为参数,所以参数应该保存在这个位置,即返回地址向上 2 格的位置处!

(图挂)

至于为什么和常识不一样,是因为我们篡改了 getbuf 的返回地址运行 fizz ,而没有 call fizz ,因此没有另保存一个返回地址,导致了偏差。

答案与上题类似,不专门给出,可以自行推算或查看我开头给的两个别人的博客。

Level 2: Firecracker

跳转到 bang 中,但是需要更改一个全局变量。先验地知道全局变量存在程序中一个固定的地址处。

查看 bang 中比较两数的代码:

(图挂)

不难发现 0x804d2a00x804d298 中存着全局变量和 cookie ,通过 GDB 调试进一步确定各自保存的东西(我在图中已经标出,另,据说也可通过 objdump -D 查看,此处不提)。知道了全局变量的保存地址,接下来就是自己构造代码了。思路如下:让代码以字符串的形式输入,然后修改 getbuf 的返回地址为保存的字符串的起始位置,使程序执行这些代码,然后 retbang 中。我们需要手写汇编代码,然后使用 gcc 编译后再利用 objdump 反编译得到对应的 char 。代码如下:

movl $0xcookie, 0x804d2a0   # 把你的 cookie 放入全局变量,不用反序
push $0x8048d08             # 让这段代码执行完后返回 bang 函数
ret                         # 如果看不懂,复习本文之前的内容

反编译得到 char 后任意填充到 40 字节,再任意 4 字节覆盖旧的 %ebp ,最后 4 字节写为保存字符串的起始地址即可。具体的就不再给出了。

Level 3:Dynamite

需要使 getbuf 返回 cookie 而不是 1 。返回值保存在 %eax 中,只需要使 getbuf 先返回到我们输入的代码处,替换掉 %eax 后再返回原来的 test 中即可。但是我们想让电脑以为什么错误都没发生,而之前的代码都把旧的 %ebp 覆盖掉了,这次不能覆盖它。(可以手工推一下,因为之前都是跳转到别的函数然后退出程序,这次返回到 test 中,而 testgetbuf 真正的调用者,返回后 %ebp 要读自己指向的内容即旧的 %ebp 才能恢复 test 的栈帧,若被覆盖则不能还原) 因此这题的答案要注意两点。一是手工编写的汇编代码:

movl $0xcookie, %eax  # 把 %eax 改为你的cookie
push $0x8048d75       # 返回地址设为 test 中 call<getbuf> 的下一条,即正常状态下的返回地址
ret

二是要先用 GDB 调试读出旧的 %ebp 的值,其地址即为此刻 %ebp 的值。然后要用这个值反序写在答案中覆盖旧的 %ebp 的部分。

Level 4:Nitroglycerin

运行一次程序,会提示你输入 5 次字符串,要求每次的字符串都要相等,但 5 次字符串保存的位置不一样。其余要求同上一题。同样面临两个问题。一是无法得知 getbufn 的返回地址应该设在哪里,因不知道每次字符串的保存位置。解决方法是用 nop 指令,至于为什么,可以参考文首给出的两篇博客,我虽可理解但也未必讲得出所以然。只想说注意跳转的位置指向 5 次随机数的最高处。这个静下心来想一想可以想清楚。还有就是不知道 %ebp 要恢复成哪一个值,解决方法是依靠其与 %esp 的数量关系,那两篇博客也讲得很好了,此处不提。

TAGS:  CSAPP
正在加载,请稍候……