深入理解计算机系统之缓冲区溢出炸弹实验
深入理解计算机系统之缓冲区溢出炸弹实验
实验目标
在实验过程中,进一步掌握函数调用时栈帧结构的变化。充分了解缓冲区溢出原理,并学会利用输入缓冲区的溢出漏洞,将攻击代码嵌入当前程序的栈帧中,使得程序执行我们所期望的过程。从实验中进一步感悟缓冲区溢出攻击方式,吸取经验教训,从而写出更安全的代码。
实验材料
makecookie:生成cookiebufbomb:可执行程序-攻击对象sendstring: 字符格式转换
bufbomb 程序是我们要攻击的对象,其缓冲区漏洞代码见下:
1 | |
1 | |
代码中没有对 buf 数组进行越界检查(常见 C 编程错误),超过11个字符将溢出。而溢出的字符将覆盖栈帧上的数据和程序的返回地址。如果我们精心构造溢出的字符串,将程序“返回”至我们想要的代码上,就能控制程序流程。
为了构造所需要的地址或其他数据,我们需要实现逆反“字符->ASCII码”的过程。出题人已经提供了 sendstring 工具,其使用方法为 $ ./sendstring < exploit.txt > exploit-raw.txt。其中 exploit.txt 保存目标数据(即空格分隔的ASCII码),exploit-raw.txt 为逆向出的字符串。
攻击 bufbomb 程序时,先使用 sendstring 工具将输入的 ASCII 码转为输入给程序的字符串:
1 | |
然后,再将学号(生成 cookie 需要)和该文件输入给攻击对象
1 | |
实验原理
函数过程调用时的栈帧结构,见下图。

注意,这张图的地址增长方向是向上。在x86架构上,Caller函数要在调用新函数前,准备好函数参数:传递的参数少于7个,就可以通过寄存器传递,当参数过多寄存器不够用时,就需要通过栈来传递。当Caller调用函数时,会将返回地址压入栈中,形成Caller的栈帧末尾。而被调用函数的栈帧就从保存帧指针的值开始。
实验开始
Level 0
这一阶段要求,控制程序进入一个在正常情况下不会被调用的函数 smoke() 。首先看一下 smoke() 函数的内存位置,是 0x08048e20 :

下图显示了在调用 Gets() 函数前,getbuf() 的栈帧情况(代码见前文)。

Gets() 函数接收 buf 的指针,因此这里是直接写入点,只要输入的字符超过 12 字节,就可以依次覆盖保存的 %ebp 的值和返回地址。我们将返回地址改为 0x08048e20 即可。注意,x86 是小端法。在 exploit.txt 中输入:
1 | |
修改完成后,生成 exploit-raw.txt 文本,输入,得到结果见下:

Level 1
现在实验要求程序跳转到 fizz() ,并且打印出 fizz() 函数的参数,必须为自己的 Cookie 值,我的 Cookie 值为 0x46dd0bfe 。
1 | |
还是一样,看一下 fizz() 函数的开始地址: 08048dc0 。那么只需要把上面的答案的最后几位更改一下,程序就可以跳转到 fizz() 函数中了!
但问题是,参数该怎么传入呢?先来看看 fizz() 怎么使用参数的:
1 | |
如果你熟悉 x86 汇编语言,很容易看出,fizz() 函数是从 0x8(%ebp) 取出。下图指出了程序运行的过程,Getbuf() 函数执行 ret 指令,将返回地址存到 eip 寄存器中,于是我们来到了 fizz() 函数,然后执行两个 push 命令,从图中可以清楚地看出参数的位置。

于是,在后面加上几位数字,就可以到达实验目的。见下图:
1 | |

Level 2
现在实验难度继续上升,要求程序跳转到 bang() 函数中,并且要求 global_value==cookie 这条分支。
1 | |
这意味着我们需要在程序执行过程中,更改 global_value 的值。跳转到 bang() 函数难度不大,将返回地址修改一下就行。那么,如何修改 global_value 的值呢?有点难度,我们先从汇编代码入手,找到 global_value 的存放地址吧!
1 | |
从 GDB 调试器中,可以轻松地找到 global_value 的地址:

现在,要想办法把这个变量的值变为我们的 cookie 。当然,可以直接在 GDB 调试器里写入,不过这样实验就失去意义了!修改全局变量就需要注入我们自己的代码,然后将返回地址篡改到攻击代码处执行,最后再执行 ret 返回到 bang()。
什么样的攻击代码才能满足我们的要求?不难,只需要修改 global_value 的值就行:
1 | |
然后,返回到 bang() 函数,装作无事发生:
1 | |
还有一个小问题,我们的攻击代码要放在哪里呢?我们只能掌控栈帧内存空间,虽然理论上说,这时我们已经可以指挥受害者干任何事情,但是这毕竟是一个实验,我不想把事情闹大,看起来也只有 buf 数组那边的空间比较合适。通过前两个实验的分析,buf 空间余留下16字节的空间,这应该够我们注入代码了!
使用 gcc 和 objdump 把这三句话转化为二进制,然后数一数空间,诶嘿,正好16字节!

然后,把得到的二进制指令,放入到前16个字节中,而返回地址应该要填 buf[0] 的地址,使用 GDB 调试器,可以知道 ebp 寄存器的值是 0xffffb878 ,那么 buf[0] 的地址就是 0xffffb878-0xc=0xffffb86c,于是写入:
1 | |
但是,如果你亲自实践的话,会发现这样仍然有错,程序不会跳转到你想要它去的地方,而是会发出 Segmentation Fault 这个令人烦恼的错误 :angry:。这是因为,Linux 为了防止缓冲区溢出攻击,已经将栈帧空间的代码可执行权限关闭了,如果将攻击代码放在栈帧区,也会因没有执行权限而无法运行。
解决方法:安装execstack:sudo apt-get install execstack。然后,修改程序堆栈的可执行属性:execstack -s bufbomb 。如果拥有 bufbomb 的源代码,也可以在编译时关闭保护机制重新编译:
1 | |
另外,修改堆栈可执行属性只能在gdb调试下有效,实际运行仍然会出现段错误。所以,彻底解决的方法还是找到源代码以后重新编译。还要注意一点,多次实验时可能会出现缓冲区首地址改变的情况。经过短暂地调整后,实验成功!

Level 3
最后一关,正常程序中 test() 返回后执行第 15 行代码,而我们要让函数执行第 12 行。
1 | |
这意味着我需要在 getbuf() 结束后回到 test() 原本的位置,并将 cookie 作为 getbuf() 的返回值传给 test() 。为了做到这一点,在攻击过程中,需要将 %ebp 的值恢复,使程序不会因为外部攻击而出错崩溃。可以使用GDB 调试器找到调用getbuf 函数之后,%ebp 的值:0xffffb898。
我打算故技重施,直接插入攻击代码,先修改返回值为我的 cookie,再将正常返回地址压入栈,最后保持存放 ebp 值不变。攻击代码我已写好:
1 | |
0x0804901e 值就是正常的返回地址,call 语句的下一条指令地址。

仍用之前的办法将攻击指令转为二进制代码,这次代码长度变短,正好11个字节,再放入0xffffb898 和攻击代码的起始地址。大功告成:
1 | |
