a.信号传递:
Linux通过向进程堆栈释放代码然后引发中断来执行在堆栈中的代码来实现向进程发送Unix信号。非执行缓冲区的补丁在发送信号的时候是允许缓冲区可执行的。
b.GCC的在线重用:
研究发现gcc在堆栈区里放置了可执行的代码作为在线重用之用。然而,关闭这个功能并不产生任何
问题,只有部分功能似乎不能使用。
非执行堆栈的保护可以有效地对付把代码植入自动变量的缓冲区溢出攻击,而对于其他形式的攻击则没有效果。通过引用一个驻留的程序的指针,就可以跳过这种保护措施。其他的攻击可以采用把代码殖入堆或者静态数据段中来跳过保护。
(3)改进C语言函数库
C语言中存在缓冲区溢出攻击隐患的系统函数有很多。例如gets(),sprintf(),strcpy(),strcat(),fscanf(),scanf(),vsprintf()等。可以开发出更安全的封装了若干已知易受堆栈溢出攻击的库函数。修改后的库函数实现了原有功能,但在某种程度上可以确保任一缓冲区溢出都被控制在现有堆栈帧之内。
(4)数组边界检查
可以说缓冲区溢出的根本原因是没有数组边界检查,当数组被溢出的时候,一些关键的数据就有可能被修改,比如函数返回地址、过程帧指针、函数指针等。同时,攻击代码也可以被植入。
因此,对数组进行边界检查,使超长代码不可能植入,这样就完全没有了缓冲区溢出攻击产生的条件。只要数组不能被溢出,溢出攻击就无从谈起。
为了实现数组边界检查,则所有的对数组的读写操作都应当被检查,以确保对数组的操作在正确的范围内。最直接的
方法是检查所有的数组操作,但是会使性能下降很多,通常可以采用一些优化的技术来减少检查的次数。
(5)使堆栈向高地址方向增长
缓冲区溢出的一个重要要素是植入的代码成功地被执行。最常见的是被植入的代码放在堆栈区中。通过修改操作系统核心,在核心层引入保护机制,限制代码在堆栈区的执行,这样,缓冲区溢出攻击就不可能成功。
到
目前为止,我们讨论利用函数返回地址控制程序转移到攻击代码的攻击
方法时,有一个基本的前提,那就是当堆栈被压入数据时,栈顶向低地址方向增长,只有这样,缓冲区溢出时才可能覆盖低地址处的函数返回地址指针,从而控制程序转移到攻击代码。如果我们使用的机器堆栈压入数据时向高地址方向前进,那么无论缓冲区如何溢出,都不可能覆盖低地址处的函数返回地址指针,也就避免了缓冲区溢出攻击。但是这种方法仍然无法防范利用堆和静态数据段的缓冲区进行溢出的攻击。
(6)程序指针完整性检查
程序指针完整性检查是针对上述缓冲区溢出的另一个要素——阻止由于函数返回地址或函数指针的改变而导致的程序执行流程的改变。它的原理是在每次
在程序指针被引用之前先检测该指针是否已被恶意改动过,如果发现被改动,程序就拒绝执行。
因此,即使一个攻击者成功地改变程序的指针,由于系统事先检测到了指针的改变,因此这个指针不会被使用。与数组边界检查相比,这种方法不能解决所有的缓冲区溢出
问题。但这种方法在性能上有很大的优势,而且兼容性也很好。
程序指针完整性检查大体上有三个
研究方向:第一,手写的堆栈检测;第二,堆栈保护;第三,保护指针。在手写的堆栈检测中会介绍Snarskii为FreeBSD开发了一套定制的能通过监测cpu堆栈来确定缓冲区溢出的libc。在堆栈保护中会介绍我们自己的堆栈保护方法所开发的一个编译器,它能够在函数调用的时候自动生成完整性检测代码。最后在保护指针中介绍正在开发中的指针保护方法,这种方法类似于堆栈保护,它提供对所有程序指针的完整性的保护。
1)手写的堆栈监测
Snarskii为FreeBSD开发了一套定制的能通过监测cpu堆栈来确定缓冲区溢出的libc。这个
应用完全用手工汇编写的,而且只保护libc中的当前有效纪录函数。这个
应用达到了设计要求,对于基于libc库函数的攻击具有很好的防卫,但是不能防卫其它方式的攻击。
2)堆栈保护:编译器生成的有效纪录完整性检测
堆栈保护是一种提供程序指针完整性检查的编译器技术,通过检查函数活动纪录中的返回地址来实现。堆栈保护作为gcc的一个小的补丁,在每个函数中,加入了函数建立和销毁的代码。加入的函数建立代码实际上在堆栈中函数返回地址后面加了一些附加的字节,如图2示。而在函数返回时,首先检查这个附加的字节是否被改动过。如果发生过缓冲区溢出的攻击,那么这种攻击很容易在函数返回前被检测到。
但是,如果攻击者预见到这些附加字节的存在,并且能在溢出过程中同样地制造他们,那么他就能成功地跳过堆栈保护的检测。通常,我们有如下的两种方案对付这种欺骗:
a.终止符号:
利用在C语言中的终止符号如0(null),CR,LF,-1(EOF)等不能在常用的字符串函数中使用,因为这些函数一旦遇到这些终止符号,就结束函数过程了。
b.随机符号:
利用一个在函数调用时产生的一个32位的随机数来实现保密,使得攻击者不可能猜测到附加字节的
内容。而且,每次调用,附加字节的
内容都在改变,也无法预测。
通过检查堆栈的完整性的堆栈保护法是从Synthetix
方法演变来的。Synthetix
方法通过使用准不变量来确保特定变量的正确性。这些特定的变量的改变是程序实现能预知的,而且只能在满足一定的条件才能可以改变。这种变量我们称为准不变量。Synthetix开发了一些工具用来保护这些变量。攻击者通过缓冲区溢出而产生的改变可以被系统当做非法的动作。在某些极端的情况下,这些准不变量有可能被非法改变,这是就需要堆栈保护来提供更完善的保护了。
实验的数据表明,堆栈保护对于各种系统的缓冲区溢出攻击都有很好的保护作用,并能保持较好的兼容性和系统性能。早先我们报告的堆栈保护所能抑制的漏洞都在表一中列出。随后,我们用堆栈保护的方法重新构造了一个完整的Linux系统(Red Hat5.1)。然后我们用XFree86-3.3.2-5和lsof的漏洞对此进行了攻击,结果表明,这个系统有效地抵御了这些攻击。这些
分析表明,堆栈保护能有效抵御现在的和将来的基于堆栈的攻击。
堆栈保护版本的Red Hat Linux 5.1已经在各种系统上运行了多年,包括个人的笔记本电脑和工作组
文件服务器。从我们的Web服务器上可以得到这个版本,而且在我们的邮件列表里已经有了55个成员。出了仅有的一次例外,这个系统和本来的系统工作完全一样,这表明堆栈保护并不对系统的兼容性构成很大的
影响。
我们已经用各种性能测试来评测堆栈保护的性能。Mircobenchmarks的结果表明在函数的调用,堆栈保护中增加了系统的开销。而在
网络的测试中(需要用到堆栈保护的地方),则表明这种开销不是很大。
我们的第一个测试对象是SSH,它提供了极强的加密和认证,用来替代Berkeley的r系列指令。SSH使用了软件加密,因此系统的占用的带宽不大,我们用
网络间复制一个大的
文件来测试带宽:
scp bigsource localhost:bigdest
测试结果表明:堆栈保护几乎不
影响SSH的网络吞吐性能。
第二个测试使用了Apache Web服务器。如果这种服务器存在基于堆栈的攻击,那么攻击者就可以轻易地取得Web服务器的控制权,允许攻击者阅读隐秘的内容和肆意篡改主页的内容。同时,Web服务器也是对性能和带宽要求较高的一个服务器部件。
我们用WebStone对带有和不带堆栈保护的Apache Web服务器进行了测试,测试的结果在表二中列出。
和SSH一样,他们的性能几乎没有区别。在客户数目较少的情况下,带有保护的服务器性能比不带保护的略微好些,在客户端数目多的时候,不带保护的性能好些。在最坏的情况下,带保护的服务器比不带保护的要差8%的连接性能,而在平均延时上保持优势。象以前一样,我们把这些归结为噪声的影响。因此,我们的结论是:堆栈保护对Web服务器系统性能没有重大的影响。
3)指针保护:编译器生成程序指针完整性检查
在堆栈保护设计的时候,冲击堆栈构成了缓冲区溢出攻击的常见的一种形式。有人推测存在一种模板来构成这些攻击(在1996年的时候)。从此,很多简单的漏洞被发现,实施和补丁了,很多攻击者开始用在第二部分中描述的更一般的
方法实施缓冲区溢出攻击。
指针保护是堆栈保护针对这种情况的一个推广。通过在所有的代码指针之后放置附加字节来检验指针在被调用之前的合法性。如果检验失败,会发出报警信号和退出程序的执行,就如同在堆栈保护中的行为一样。这种方案有三点需要注意:
a.附加字节的定位:
附加字节的空间是在被保护的变量被分配的时候分配的,同时在被保护字节初始化过程中被初始化。这样就带来了
问题;为了保持兼容性,我们不想改变被保护变量的大小,因此不能简单地在变量的结构定义中加入附加字。还有,对各种类型也有不同附加字节数目。
b.检查附加字节:
每次程序指针被引用的时候都要检查附加字节的完整性。这个也存在
问题;因为“从存取器读”在编译器中没有语义;编译器更关心指针的使用,而各种的优化算法倾向于从存储器中读入变量。
c.还有随着不同类型的变量,读入的
方法也各自不同。
已经开发的指针保护的一个原型(还是基于gcc的),通过附加字节来保护静态分配的函数指针,但不适用于结构和数组类型。这个计划还远没有完成。一旦这个项目完成了,那么用它和堆栈保护构成的可执行代码将不会受到缓冲区溢出的攻击了。
目前为止,只有很少一部分使用非指针变量的攻击能逃脱指针保护的检测。但是,可以通过在编译器上强制对某一变量加入附加字节来实现检测,这时需要程序员自己手工加入相应的保护了。
(7)利用编译器将静态数据段中的函数地址指针存放地址和其他数据的存放地址分离
我们知道缓冲区溢出的一个基本条件是在缓冲区的高地址附近存放着可供溢出覆盖的函数地址指针,如果我们破坏了这一条件,就会使缓冲区溢出不能覆盖函数地址指针。比如,我们可以假定编译器在编译程序时会将静态数据段中的函数地址指针存放地址和其他数据的存放地址隔开相当大的一段距离,例如数10M,数100M,这样才会迫使攻击者只有送入长的出乎想象数据才能抵达函数地址指针存放地址并覆盖它,以这样的不可操作性来制止攻击者实现攻击意图。另外,我们还可以使这两种数据段间的线形地址空间不分配物理地址并设置为不可读写,这些都可以通过硬件实现,这样每当缓冲区溢出进入这段区域,就会出现地址保护错误,从而阻止了缓冲区溢出攻击。
另外一种较为简洁的方法是始终保持使静态数据段中的函数地址指针存放地址低于其他数据的存放地址。但是这个方法仅仅是防止了静态数据段中的缓冲区溢出攻击,而不能避免堆的缓冲区溢出攻击,因为编译器和操作系统并不知道用户申请的内存是用来存放函数地址指针还是其他数据,从而无法为其隔离分配内存。表1是针对静态数据段、堆栈和堆的缓冲区溢出的防范策略,其中没有把边界检查包括在内,因为它能有效地防止所有的缓冲区溢出,但其运行开销也是惊人的。表的最上面一行是攻击代码所植入的内存空间,表的最左面一列是溢出方法,而中间单元就是相应的防范策略。
表1 缓冲区溢出的防范策略
堆栈
(Stack Buffer)
堆
(Heap Buffer)
静态数据段
(Static Buffer)
活动记录
堆栈保护
非执行的缓冲区
堆栈保护
堆栈保护
函数指针
指针保护
非执行的缓冲区
指针保护
指针保护
长跳转缓冲区
指针保护
非执行的缓冲区
指针保护
指针保护
其它变量
手工的指针保护
非执行的缓冲区
手工的指针保护
手工的指针保护
为了防止静态数据段、堆栈和堆这三种数据段中的一个缓冲区溢出覆盖另一个段中的函数地址指针,我们应该为这三种数据段隔离分配线形地址空间。
四
总结
缓冲区溢出是当今很流行的一种
网络攻击
方法,它易于攻击而且危害严重,给系统的安全带来了极大的隐患。因此,如何及时有效地检测出
计算机
网络系统入侵行为,已越来越成为网络安全管理的一项重要
内容。缓冲区溢出的漏洞一般有以下几种情况:
(1) 系统漏洞
如操作系统、服务器程序、数据库程序等,这种漏洞可以通过升级软件、打安全“补丁”等
方法来解决。
(2)
应用软件
在软件开发过程中,如果程序员没有很强的安全意识和良好的编程习惯,也会产生很多安全漏洞。
本文从上面的系统环境及程序设计语言角度,对
目前主要的网络攻击方式:缓冲区溢出攻击的出现和原理进行了详细的
分析,并根据缓冲区溢出攻击的类型提出了相应的防范策略。
然而,飞速
发展的网络技术还需要我们继续对各种黑客攻击系统的方法做更多的关注和应付措施的探索,为网络安全做出更大的实践意义的
研究。
参考文献
[1] 张小斌,严望佳,黑客
分析与防范技术[M] 北京清华大学出版社,1999。
[2] 陈意云 编译原理和技术[M],合肥,
中国科技大学出版社,1997。
[3] 汪立东,方滨兴,Unix缓冲区溢出攻击:技术原理、防范与检测,
计算机工程与
应用,2000(2)。
[4]谭毓安,网络攻击防护编码设计,北京希望
电子出版社,2002。
[5]www.nsfocus.com转贴于 中国论文下载中心 http://paper.studa.com