深入理解计算机系统之缓冲区溢出炸弹实验
深入理解计算机系统之缓冲区溢出炸弹实验
实验目标
在实验过程中,进一步掌握函数调用时栈帧结构的变化。充分了解缓冲区溢出原理,并学会利用输入缓冲区的溢出漏洞,将攻击代码嵌入当前程序的栈帧中,使得程序执行我们所期望的过程。从实验中进一步感悟缓冲区溢出攻击方式,吸取经验教训,从而写出更安全的代码。
实验材料
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 |
|