本系列文章为中国科学技术大学计算机专业学科基础课《计算机系统》布置的实验,上课所用教材和内容为黑书CSAPP,当时花费很大精力和弯路,现来总结下各个实验,本文章为第三个实验——缓冲区溢出炸弹(Buflab)。
掌握函数调用时的栈帧结构,利用输入缓冲区的溢出漏洞,将攻击代码嵌入当前程序的栈帧中,使得程序执行我们所期望的过程
·溢出的字符将覆盖栈帧上的数据
-特别的,会覆盖程序调用的返回地址
-赋予我们控制程序流程的能力
·通过构造溢出字符串,程序将“返回”至我们想要的代码上
本实验需要你构造一些攻击字符串,对目标可执行程序BUFBOMB分别造成不同的缓冲区溢出攻击。实验分5个难度级分别命名为Smoke(level 0)、Fizz(level 1)、Bang(level 2)、Boom(level 3)和Nitro(level 4),其中,前2个为必做,后3个为选做,我选做了Bang,这一关有个坑,需要关闭进程地址空间随机化,很少有教程会提到,当时找了好久才解决。
本次lab利用getbuf()方程不检查读取string长度的漏洞破坏该方程的 return address 从而达到对主程序造成破坏的目的。从getbuf() 的assembly code我们可以看到:
位于 <0x8048fe6> 地址处代码为预读的 string 在 stack 创建了0xc(也就是12)个Byte 的空间。具体位置可以通过gdb在下一行设置 breakpoint 查找 %eax 的值得到,如下所示:
通过gdb调试得到,getbuf()申请的12字节缓冲区首地址为 <0xffffb87c>,这个地址后面会用到。
通常在P过程调用Q过程时,程序的stack frame结构如下图所示:
为了覆盖被存在Return Address上的值(4 Bytes for m32 machine),我们需要读入超过系统默认12 Bytes大小的string。由于Saved ebp 占据了4 Bytes 所以当我们的input string 为20 Bytes时,最后4位Bytes 刚好覆盖我们的目标Return address.
(Notes: 由于我们在输入文件下写入的都是character(字符)因此我们需要利用hex2raw这个小程序帮助我们将我们写入的character转换成所对应的二进制数列。)
Smoke任务的目标是构造一个攻击字符串作为bufbomb的输入,在getbuf()中造成缓冲区溢出,使得getbuf()返回时不是返回到test函数,而是转到smoke函数处执行。为此,你需要:
如以上实例中,smoke的开始地址是<0x08048e20>。
如以上实例,你可以看到getbuf()的栈帧是0x18+4个字节,而buf缓冲区的大小是0xc(12个字节)。
攻击字符串的功能是用来覆盖getbuf函数内的数组buf(缓冲区),进而溢出并覆盖ebp和ebp上面的返回地址,所以攻击字符串的大小应该是0xc+4+4=20个字节。并且其最后4个字节应是smoke函数的地址,正好覆盖ebp上方的正常返回地址。这样再从getbuf返回时,取出的根据攻击字符串设置的地址,就可实现控制转移。
所以,这样的攻击字符串为:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20 8e 04 08
总共20个字节,并且前面16个字节可以为任意值,对程序的执行没有任何影响,只要最后四个字节正确地设置为smoke的起始地址<0x08048e20>即可,对应内存写入20 8e 04 08(小端格式)。
通过Linux终端执行:
至此,leve0任务smoke通过!
level1 和 level0 大同小异,唯一的区别是本次要求跳入函数 fizz(int) 且该函数有一个参数(要求用所给cookie作参数)。
原理与smoke相同,观察栈帧结构可以发现只需要在smoke攻击字串后面再继续覆盖调用栈帧的参数。
我们知道在执行完ret指令后栈顶指针 %esp 会自动增加4以还原栈帧。
通过查找fizz()得知:
fizz()函数的起始地址为 <0x08048dc0> 。与smoke相同,ebp+4为栈帧返回地址。执行完ret指令后栈顶指针 %esp 会自动增加4以还原栈帧。在fizz汇编代码段,cmp指令是将存放cookie的变量与%ebp+0x8处的值相比,此时参数地址也就是旧的ebp+4+8。我们只需要将自己的cookie放置在该位置即可。
所以构造攻击文件fizz.txt如下:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c0 8d 04 08 00 00 00 00 12 2c 46 22
其中,<0x08048dc0>为fizz函数起始地址,<0x22462c12>为自己的cookie,通过参数传递给fizz。
最后执行测试结果如下:
至此,leve1任务fizz通过!
level2的难度开始增加,除了需要跳转至目标函数bang() 地址为<0x08048d60>:
我们还需要执行一些自行设计的指令,因为该任务我们需要将global_value 的值改成我们的cookie,通过objdump -D bufbomb | less (注意D要大写我们才能看到header的代码,-d不会显示):
通过objdump -D 反汇编可以看到:
global_value的地址是<0x0804a1dc>, 目前该位置的初始值为 0 ;
cookie的地址是<0x0804a1cc>, 目前该位置的值初始为 0,程序运行后会变为cookie的值。Cookie:0x22462c12
我们需要做的就是,在程序运行时将global_value的值设置为cookie的值。
构造自定义攻击指令bang.s:
由于是Assembly code 不需要考虑 little endian的问题。先将global_value 用mov指令变cookie (0x0804a1dc前不加$表示地址),然后将bang()函数地址<0x08048d60>写给esp,再执行ret指令时,程序自动跳入bang()函数。
指令 gcc -m32 -c bang.s 将assembly code写成machine code -->bang.o,再用objdump -d bang.o 读取machine code如下:
将指令代码抄入攻击文件,除此之外我们还需要找到input string存放的位置作为第一次ret指令的目标位置,具体操作方法见Overview, 经过gdb调试分析getbuf()申请的12字节缓冲区首地址为<0xffffb87c>
所以构造攻击字符串bang.txt如下:
c7 05 dc a1 04 08 12 2c 46 22 68 60 8d 04 08 c3 7c b8 ff ff
使用sendstring获得新的攻击字符,执行程序测试运行结果
提示运行失败,这里出现段错误是因为Linux系统默认开启了栈保护机制,用于阻止缓冲区溢出攻击
解决方法:
安装execstack sudo apt-get install execstack
修改程序堆栈的可执行属性 execstack -s bufbomb
这还不够,还要关闭进程地址空间随机化:
sudo -s
echo 0 > /proc/sys/kernel/randomize_va_space
再次测试运行结果:
至此,leve2任务bang通过!