1.前言
缓冲区溢出(buffer-overflow)是一种非常普遍、同时非常危险的漏洞,在各种操作系统、应用软件中广泛存在。缓冲区溢出攻击是利用缓冲区溢出漏洞所进行的攻击,轻则可以导致程序失败、系统关机等,重则可以利用它执行非授权指令,甚至获取系统特权,从而进行其它的非法操作。所以,了解缓冲区溢出攻击的原理以及如何防范缓冲区溢出攻击就变得十分重要。
2.缓冲区溢出
想了解缓冲区溢出攻击,首先就要了解什么是缓冲区和缓冲区溢出。缓冲区是一块连续的计算机内存区域,可保存相同数据类型的多个实例。缓冲区可以是堆栈(自动变量)、堆(动态内存)和静态数据区(全局或静态)。在C/C++语言中,通常使用字符数组和堆中动态分配的内存(也称为“malloc”或“new”区域)之类内存分配函数实现缓冲区。
操作系统所使用的缓冲区又被称为堆栈,在各个操作进程之间,指令被临时存储在堆栈当中,堆栈也会出现缓冲区溢出。理想情况是,程序检查数据长度并且不允许输入超过缓冲区长度的字符串。但是绝大多数程序都会假设数据长度总是与所分配的存储空间相匹配,这就为缓冲区溢出埋下隐患。当一个超长的数据进入到缓冲区时,超出部分就会被写入其他缓冲区,其他缓冲区存放的可能是数据、下一条指令的指针,或者是其他程序的输出内容,这些内容都被覆盖或者破坏掉。可见一小部分数据或者一套指令的溢出就可能导致一个程序或者操作系统崩溃。
3.缓冲区溢出攻击
在本节,会着重说明攻击者如何利用缓冲区溢出来实施攻击,并通过几个示例来加深对于缓存区溢出攻击的理解。
3.1缓冲区溢出攻击原理
缓冲区溢出的一个致命的使用就是让程序执行它本来不愿意执行的函数。这是一种常见的通过计算机网络攻击系统安全的方法。通常,输入给程序一个字符串,这个字符串包含一些可执行代码的字节编码,成为攻击代码。另外,还有一些字节会用一个指向攻击代码的指针覆盖返回地址。那么,执行ret指令的效果就是跳转到攻击代码。
攻击者可以故意将精心制作的输入馈入程序,程序将尝试将该输入存储在不够大的缓冲区中,因此输入会覆盖连接到缓冲区空间的部分内存。如果程序的内存布局定义明确,则攻击者可以故意覆盖已知包含可执行代码的区域。然后,攻击者可以用自己的可执行代码替换这些代码,这可以大大改变程序的工作方式。
缓冲区溢出漏洞攻击都是在寻求改变程序的执行流程,使它跳转到攻击代码,最为基本的就是溢出一个没有检查或者其他漏洞的缓冲区,这样做就会扰乱程序的正常执行次序。通过溢出某缓冲区,可以改写相近程序的空间而直接跳转过系统对身份的验证。原则上来讲攻击时所针对的缓冲区溢出的程序空间可为任意空间。
3.2缓冲区溢出攻击示例
3.2.1 strcpy溢出的攻击
在学习c/c++的时候,就讲到了一些C类型的字符串函数不是安全的,比如strcpy没有检查长度会溢出,推荐使用strncpy。而使用strcpy就容易造成缓冲溢出,从而收到缓冲溢出攻击,下面将演示由使用strcpy导致溢出得攻击。
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
if (argc != 2)
{
printf("Invalid params\n");
exit(1);
}
bool check_result = false;
char pass[10];
memset(pass,0,10);
strcpy(pass, argv[1]);
if (0 == strcmp("password", pass))
{
check_result = true;
}
if (check_result)
{
printf("Check password succ!\n");
}
else
{
printf("Check password failed!\n");
}
return 0;
}
上面的代码在linux系统中打开终端使用g++编译 g++ -o guess 1.c 生成可执行文件,然后输入 ./guess 0123456789012345运行,运行结果如下图所示。
图3-1 strcpy溢出攻击运行结果
很显然我们输入的密码是错误的,我们预期中它会输出“Check password failed!”,但是通过运行结果我们可以看到,程序运行输出的是“Check password succ!”。说明尽管我们不知道密码是多少,通过缓冲区溢出,我们绕过了密码的验证逻辑,直接登陆了进去。
上述check_result和pass两个变量由于是局部变量,故申请的时候是在栈上分配。由于栈是从高地址往低地址,所以从地址由低到高,check_result分配在pass后面,当进行strcpy操作的时候,因为没有检测输入长度,所以pass溢出了,会覆盖后面的内存区域。由于check_result在pass后面,所以当溢出足够的时候,check_result被改写了。缓存溢出就意味着要面临数据被异常改写的风险。
4.缓冲区溢出攻击防范
4.1强制写正确的代码的方法
编写正确的代码是一件非常有意义但耗时的工作,特别像编写C语言那种具有容易出错倾向的程序(如:字符串的零结尾),这种风格是由于追求性能而忽视正确性的传统引起的。尽管花了很长的时间使得人们知道了如何编写安全的程序,具有安全漏洞的程序依旧出现。因此人们开发了一些工具和技术来帮助经验不足的程序员编写安全正确的程序。虽然这些工具帮助程序员开发更安全的程序,但是由于C语言的特点,这些工具不可能找出所有的缓冲区溢出漏洞。所以,侦错技术只能用来减少缓冲区溢出的可能,并不能完全地消除它的存在。除非程序员能保证他的程序万无一失,否则还是要用到以下部分的内容来保证程序的可靠性能。
4.2栈随机化
使得栈的位置在程序每次运行时都有变化(主要受linux系统版本限制,老版本不支持栈随机化)。为了在系统插入攻击代码,攻击者不但要插入代码,还需要插入指向这段代码的指针(指向攻击代码的首地址/栈地址),这个指针也是攻击字符串的一部分。产生这个指针需要知道这个字符串放置的栈地址。老的系统版本,如果在相同的系统运行相同的程序,栈的位置是相当固定的。所以黑客可以在一台机器上研究透系统上的栈是如何分配地址的,就可以入侵其它主机。
实现的方式:程序开始时,在栈上分配一段0~n字节之间的随机大小的空间。分配的范围n必须足够大,才能获得足够多样的栈地址变化,但是又要足够小,不至于浪费程序太多的空间。
4.3栈破坏检测
检测到何时栈被破坏(主要受GCC版本的限制,老的GCC版本不支持栈破坏检测)。从strcpy等函数我们可以看到,破坏通常发生在当超越局部缓冲区的边界时。在C语言中,没有可靠的方法来防止对数组的越界写。但是,我们能够在发生了越界写的时候,并且,在其还没有造成任何有害结果之前,尝试检测到它,并且把程序终止。
实现的方式:加入一种栈保护机制。 在栈帧中,紧接着局部缓冲区的位置放置一个哨兵(金丝雀),哨兵值是随机产生的,攻击者没有简单的方法能够知道它是什么。在恢复寄存器状态和函数返回之 前,程序检查这个金丝雀的值是否发生改变,如果发生改变立即终止程序。
4.4限制可执行代码区域
消除攻击者向系统插入可执行代码的能力,一种方法是:限制那些能够存放可执行代码的存储器区域。在典型的系统中,只有保存编译器产生的代码的那部分存储器才需要是可执行的,其它部分可以被限制为只允许读和写。
一般的系统允许三种访问的形式:读(从存储器读数据)、写(存储数据到存储器)和执行(将存储器的内容看作是机器级代码)。以前,x86体系结构将读和执行访问控制合并为1位的标志,这样任何被标记为可读的页都是可执行的。栈又要求必须是既可以读又可以写的,所以x86体系结构栈上的字节都是可执行的。也有一些体制,能够限制一些页是可读但是不可执行,但是这些体制一般都会带来严重的性能损失。
实现的方式:AMD为它的64位存储器的内容保护引入了“NX”(No-eXecute,不执行)位,将读和执行访问模式分开,intel也跟进了。从这开始,栈可以被标记为可读、可写,但是不可执行。检查页是否可执行由硬件来完成,效率上没有损失。