|
作者: nightcolor [nightcolor] 版主 | 登录 |
黑客中级技术--缓冲区溢出攻击 左晓栋?yesky 缓冲区溢出是一种非常普遍、非常危险的漏洞,在各种操作系统、应用软件中广泛存在 。利用缓冲区溢出攻击,可以导致程序运行失败、系统当机、重新启动等后果。更为严重的是 ,可以利用它执行非授权指令,甚至可以取得系统特权,进而进行各种非法操作。缓冲区溢出 攻击有多种英文名称:buffer overflow,buffer overrun,smash the stack,trash the s tack,scribble the stack, mangle the stack, memory leak,overrun screw;它们指的 都是同一种攻击手段。第一个缓冲区溢出攻击--Morris蠕虫,发生在十年前,它曾造成了全世 界6000多台网络服务器瘫痪。 本文将分析缓冲区溢出的原理;研究各种类型的缓冲区溢出漏洞和攻击手段;最后,还 将着重研究各种防御手段,用来消除这些漏洞所造成的影响。 一、 缓冲区溢出的原理 通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈, 使程序转而执行其它指令,以达到攻击的目的。造成缓冲区溢出的原因是程序中没有仔细检查 用户输入的参数。例如下面程序: void function(char *str) { char buffer[16]; strcpy(buffer,str); } 上面的strcpy()将直接吧str中的内容copy到buffer中。这样只要str的长度大于16,就 会造成buffer的溢出,使程序运行出错。存在象strcpy这样的问题的标准函数还有strcat(), sprintf(),vsprintf(),gets(),scanf()等。 当然,随便往缓冲区中填东西造成它溢出一般只会出现“分段错误”(Segmentation f ault),而不能达到攻击的目的。最常见的手段是通过制造缓冲区溢出使程序运行一个用户s hell,再通过shell执行其它命令。如果该程序属于root且有suid权限的话,攻击者就获得了 一个有root权限的shell,可以对系统进行任意操作了。 缓冲区溢出攻击之所以成为一种常见安全攻击手段其原因在于缓冲区溢出漏洞太普遍了 ,并且易于实现。而且,缓冲区溢出成为远程攻击的主要手段其原因在于缓冲区溢出漏洞给予 了攻击者他所想要的一切:植入并且执行攻击代码。被植入的攻击代码以一定的权限运行有缓 冲区溢出漏洞的程序,从而得到被攻击主机的控制权。 在1998年Lincoln实验室用来评估入侵检测的的5种远程攻击中,有2种是缓冲区溢出。而 在1998年CERT的13份建议中,有9份是是与缓冲区溢出有关的,在1999年,至少有半数的建议 是和缓冲区溢出有关的。在Bugtraq的调查中,有2/3的被调查者认为缓冲区溢出漏洞是一个很 严重的安全问题。 缓冲区溢出漏洞和攻击有很多种形式,会在第二节对他们进行描述和分类。相应地防卫 手段也随者攻击方法的不同而不同,将在第四节描述,它的内容包括针对每种攻击类型的有效 的防卫手段。 二、缓冲区溢出的漏洞和攻击 缓冲区溢出攻击的目的在于扰乱具有某些特权运行的程序的功能,这样可以使得攻击者 取得程序的控制权,如果该程序具有足够的权限,那么整个主机就被控制了。一般而言,攻击 者攻击root程序,然后执行类似“exec(sh)”的执行代码来获得root权限的shell。为了达到 这个目的,攻击者必须达到如下的两个目标: 1. 在程序的地址空间里安排适当的代码。 2. 通过适当的初始化寄存器和内存,让程序跳转到入侵者安排的地址空间执行。 根据这两个目标来对缓冲区溢出攻击进行分类。在二.1节,将描述攻击代码是如何放入 被攻击程序的地址空间的。在二.2节,将介绍攻击者如何使一个程序的缓冲区溢出,并且执行 转移到攻击代码(这个就是“溢出”的由来)。在二.3节,将综合前两节所讨论的代码安排和 控制程序执行流程的技术。 二.1 在程序的地址空间里安排适当的代码的方法 有两种在被攻击程序地址空间里安排攻击代码的方法: 1、植入法: 攻击者向被攻击的程序输入一个字符串,程序会把这个字符串放到缓冲区里。这个字符 串包含的资料是可以在这个被攻击的硬件平台上运行的指令序列。在这里,攻击者用被攻击程 序的缓冲区来存放攻击代码。缓冲区可以设在任何地方:堆栈(stack,自动变量)、堆(he ap,动态分配的内存区)和静态资料区。 2、利用已经存在的代码: 有时,攻击者想要的代码已经在被攻击的程序中了,攻击者所要做的只是对代码传递一 些参数。比如,攻击代码要求执行“exec (“/bin/sh”)”,而在libc库中的代码执行“exe c (arg)”,其中arg使一个指向一个字符串的指针参数,那么攻击者只要把传入的参数指针改 向指向”/bin/sh”。 二.2 控制程序转移到攻击代码的方法 所有的这些方法都是在寻求改变程序的执行流程,使之跳转到攻击代码。最基本的就是 溢出一个没有边界检查或者其它弱点的缓冲区,这样就扰乱了程序的正常的执行顺序。通过溢 出一个缓冲区,攻击者可以用暴力的方法改写相邻的程序空间而直接跳过了系统的检查。 分类的基准是攻击者所寻求的缓冲区溢出的程序空间类型。原则上是可以任意的空间。实 际上,许多的缓冲区溢出是用暴力的方法来寻求改变程序指针的。这类程序的不同之处就是程 序空间的突破和内存空间的定位不同。主要有以下三种: 1、活动纪录(Activation Record s): 每当一个函数调用发生时,调用者会在堆栈中留下一个活动纪录,它包含了函数结束时返 回的地址。攻击者通过溢出堆栈中的自动变量,使返回地址指向攻击代码。通过改变程序的返 回地址,当函数调用结束时,程序就跳转到攻击者设定的地址,而不是原先的地址。这类的缓 冲区溢出被称为堆栈溢出攻击(Stack Smashing Attack),是目前最常用的缓冲区溢出攻击 方式。 2、函数指针(Function Pointers): 函数指针可以用来定位任何地址空间。例如:“void (* foo)()”声明了一个返回值为 void的函数指针变量foo。所以攻击者只需在任何空间内的函数指针附近找到一个能够溢出的 缓冲区,然后溢出这个缓冲区来改变函数指针。在某一时刻,当程序通过函数指针调用函数时 ,程序的流程就按攻击者的意图实现了。它的一个攻击范例就是在Linux系统下的superprobe 程序。 3、长跳转缓冲区(Longjmp buffers): 在C语言中包含了一个简单的检验/恢复系统,称为setjmp/longjmp。意思是在检验点设 定“setjmp(buffer)”,用“longjmp(buffer)”来恢复检验点。然而,如果攻击者能够进入 缓冲区的空间,那么“longjmp(buffer)”实际上是跳转到攻击者的代码。象函数指针一样, longjmp缓冲区能够指向任何地方,所以攻击者所要做的就是找到一个可供溢出的缓冲区。一 个典型的例子就是Perl 5.003的缓冲区溢出漏洞;攻击者首先进入用来恢复缓冲区溢出的的l ongjmp缓冲区,然后诱导进入恢复模式,这样就使Perl的解释器跳转到攻击代码上了。 二.3代码植入和流程控制技术的综合分析 最简单和常见的缓冲区溢出攻击类型就是在一个字符串里综合了代码植入和活动纪录技 术。攻击者定位一个可供溢出的自动变量,然后向程序传递一个很大的字符串,在引发缓冲区 溢出,改变活动纪录的同时植入了代码。这个是由Levy指出的攻击的模板。因为C在习惯上只 为用户和参数开辟很小的缓冲区,因此这种漏洞攻击的实例十分常见。 代码植入和缓冲区溢出不一定要在在一次动作内完成。攻击者可以在一个缓冲区内放置 代码,这是不能溢出的缓冲区。然后,攻击者通过溢出另外一个缓冲区来转移程序的指针。这 种方法一般用来解决可供溢出的缓冲区不够大(不能放下全部的代码)的情况。 如果攻击者试图使用已经常驻的代码而不是从外部植入代码,他们通常必须把代码作为 参数调用。举例来说,在libc(几乎所有的C程序都要它来连接)中的部分代码段会执行“ex ec(something)”,其中somthing就是参数。攻击者然后使用缓冲区溢出改变程序的参数,然 后利用另一个缓冲区溢出使程序指针指向libc中的特定的代码段。 三、 缓冲区溢出攻击的实验分析 2000年1月,Cerberus 安全小组发布了微软的IIS 4/5存在的一个缓冲区溢出漏洞。攻击 该漏洞,可以使Web服务器崩溃,甚至获取超级权限执行任意的代码。目前,微软的IIS 4/5 是一种主流的Web服务器程序;因而,该缓冲区溢出漏洞对于网站的安全构成了极大的威胁; 它的描述如下: 浏览器向IIS提出一个HTTP请求,在域名(或IP地址)后,加上一个文件名,该文件名以 “.htr”做后缀。于是IIS认为客户端正在请求一个“.htr”文件,“.htr”扩展文件被映像 成ISAPI(Internet Service API)应用程序,IIS会复位向所有针对“.htr”资源的请求到 ISM.DLL程序 ,ISM.DLL 打开这个文件并执行之。 浏览器提交的请求中包含的文件名存储在局部变量缓冲区中,若它很长,超过600个字符 时,会导致局部变量缓冲区溢出,覆盖返回地址空间,使IIS崩溃。更进一步,在如图1所示的 2K缓冲区中植入一段精心设计的代码,可以使之以系统超级权限运行。 四、缓冲区溢出攻击的防范方法 缓冲区溢出攻击占了远程网络攻击的绝大多数,这种攻击可以使得一个匿名的Internet 用户有机会获得一台主机的部分或全部的控制权。如果能有效地消除缓冲区溢出的漏洞,则很 大一部分的安全威胁可以得到缓解。 目前有四种基本的方法保护缓冲区免受缓冲区溢出的攻击和影响。在四.1中介绍了通过 操作系统使得缓冲区不可执行,从而阻止攻击者植入攻击代码。在四.2中介绍了强制写正确的 代码的方法。在四.3中介绍了利用编译器的边界检查来实现缓冲区的保护。这个方法使得缓冲 区溢出不可能出现,从而完全消除了缓冲区溢出的威胁,但是相对而言代价比较大。在四.4中 介绍一种间接的方法,这个方法在程序指针失效前进行完整性检查。虽然这种方法不能使得所 有的缓冲区溢出失效,但它能阻止绝大多数的缓冲区溢出攻击。然后在四.5,分析这种保护方 法的兼容性和性能优势。 四.1 非执行的缓冲区 通过使被攻击程序的数据段地址空间不可执行,从而使得攻击者不可能执行被植入被攻 击程序输入缓冲区的代码,这种技术被称为非执行的缓冲区技术。在早期的Unix系统设计中, 只允许程序代码在代码段中执行。但是近来的Unix和MS Windows系统由于要实现更好的性能和 功能,往往在数据段中动态地放入可执行的代码,这也是缓冲区溢出的根源。为了保持程序的 兼容性,不可能使得所有程序的数据段不可执行。 但是可以设定堆栈数据段不可执行,这样就可以保证程序的兼容性。Linux和Solaris都 发布了有关这方面的内核补丁。因为几乎没有任何合法的程序会在堆栈中存放代码,这种做法 几乎不产生任何兼容性问题,除了在Linux中的两个特例,这时可执行的代码必须被放入堆栈 中: (1)信号传递: Linux通过向进程堆栈释放代码然后引发中断来执行在堆栈中的代码来实现向进程发送Un ix信号。非执行缓冲区的补丁在发送信号的时候是允许缓冲区可执行的。 (2)GCC的在线重用: 研究发现gcc在堆栈区里放置了可执行的代码作为在线重用之用。然而,关闭这个功能并 不产生任何问题,只有部分功能似乎不能使用。 非执行堆栈的保护可以有效地对付把代码植入自动变量的缓冲区溢出攻击,而对于其它 形式的攻击则没有效果。通过引用一个驻留的程序的指针,就可以跳过这种保护措施。其它的 攻击可以采用把代码植入堆或者静态数据段中来跳过保护。 四.2 编写正确的代码 编写正确的代码是一件非常有意义的工作,特别象编写C语言那种风格自由而容易出错的 程序,这种风格是由于追求性能而忽视正确性的传统引起的。尽管花了很长的时间使得人们知 道了如何编写安全的程序,具有安全漏洞的程序依旧出现。因此人们开发了一些工具和技术来 帮助经验不足的程序员编写安全正确的程序。 最简单的方法就是用grep来搜索源代码中容易产生漏洞的库的调用,比如对strcpy和spr intf的调用,这两个函数都没有检查输入参数的长度。事实上,各个版本C的标准库均有这样 的问题存在。 此外,人们还开发了一些高级的查错工具,如fault injection等。这些工具的目的在于 通过人为随机地产生一些缓冲区溢出来寻找代码的安全漏洞。还有一些静态分析工具用于侦测 缓冲区溢出的存在。 虽然这些工具帮助程序员开发更安全的程序,但是由于C语言的特点,这些工具不可能找 出所有的缓冲区溢出漏洞。所以,侦错技术只能用来减少缓冲区溢出的可能,并不能完全地消 除它的存在。 四.3 数组边界检查 数组边界检查能防止所有的缓冲区溢出的产生和攻击。这是因为只要数组不能被溢出, 溢出攻击也就无从谈起。为了实现数组边界检查,则所有的对数组的读写操作都应当被检查以 确保对数组的操作在正确的范围内。最直接的方法是检查所有的数组操作,但是通常可以采用 一些优化的技术来减少检查的次数。目前有以下的几种检查方法: 1、 Jones & Kelly: C的数组边界检查 Richard Jones和Paul Kelly开发了一个gcc的补丁,用来实现对C程序完全的数组边界检 查。由于没有改变指针的含义,所以被编译的程序和其它的gcc模块具有很好的兼容性。更进 一步的是,他们由此从没有指针的表达式中导出了一个“基”指针,然后通过检查这个基指针 来侦测表达式的结果是否在容许的范围之内。 当然,这样付出的性能上的代价是巨大的:对于一个频繁使用指针的程序,比如向量乘 法,将由于指针的频繁使用而使速度比本来慢30倍。这个编译器目前还很不成熟;一些复杂的 程序还不能在这个上面编译,执行通过。 2、 Compaq C 编译器 Compaq公司为Alpha CPU开发的C编译器支持有限度的边界检查(使用check_bounds参数)。 这些限制是: 只有显式的数组引用才被检查,比如“a[3]”会被检查,而“*(a+3)”则不会。 由于所有的C数组在传送的时候是指针传递的,所以传递给函数的的数组不会被检查。 带有危险性的库函数如strcpy不会在编译的时候进行边界检查,即便是指定了边界检查。 由于在C语言中利用指针进行数组操作和传递是如此的频繁,因此这种局限性是非常严重 的。通常这种边界检查用来程序的查错,而且不能保证不发生缓冲区溢出的漏洞。 3、 Purify:内存存取检查 Purify是C程序调试时查看内存使用的工具。Purify使用“目标代码插入”技术来检查所有的 内存存取。通过用Purify连接工具连接,可执行代码在执行的时候数组的所有引用来保证其合 法性。这样带来的性能上的损失要下降3-5倍。 4、 类型-安全语言 所有的缓冲区溢出漏洞都源于C语言缺乏类型安全。如果只有类型-安全的操作才可以被 允许执行,这样就不可能出现对变量的强制操作。如果作为新手,可以推荐使用具有类型-安 全的语言如Java。但是作为Java执行平台的Java虚拟机是C程序,因此通过攻击JVM的一条途径 是使JVM的缓冲区溢出。 四.4 程序指针完整性检查 程序指针完整性检查和边界检查有略微的不同,程序指针完整性检查在程序指针被引用 之前检测到它的改变。因此,即使一个攻击者成功地改变了程序的指针,由于系统事先检测到 了指针的改变,因此这个指针将不会被使用。 与数组边界检查相比,这种方法不能解决所有的缓冲区溢出问题;采用其它的缓冲区溢 出攻击方法就可以避免这种检测。但是这种方法在性能上有很大的优势,而且在兼容性也很好 。 程序完整性检查大体上有三个研究方向: Snarskii为FreeBSD开发了一套定制的能通过监测cpu堆栈来确定缓冲区溢出的libc。 堆栈保护方法所开发的一个编译器,它能够在函数调用的时候自动生成完整性检测代码。 正在开发中的指针保护方法,这种方法类似于堆栈保护,它提供对所有程序指针的完整性的 保护。 1、堆栈监测 Snarskii为FreeBSD开发了一套定制的能通过监测cpu堆栈来确定缓冲区溢出的libc。这 个应用完全用手工汇编写的,而且只保护libc中的当前有效纪录函数。这个应用达到了设计要 求,对于基于libc库函数的攻击具有很好的防卫,但是不能防卫其它方式的攻击。 2、堆栈保护:编译器生成的有效纪录完整性检测 堆栈保护是一种提供程序指针完整性检查的编译器技术,通过检查函数活动纪录中的返 回地址来实现。堆栈保护作为gcc的一个小的补丁,在每个函数中,加入了函数建立和销毁的 代码。加入的函数建立代码实际上在堆栈中函数返回地址后面加了一些附加的字节。而在函数 返回时,首先检查这个附加的字节是否被改动过。如果发生过缓冲区溢出的攻击,那么这种攻 击很容易在函数返回前被检测到。 但是,如果攻击者预见到这些附加字节的存在,并且能在溢出过程中同样地制造他们, 那么他就能成功地跳过堆栈保护的检测。通常,有如下的两种方案对付这种欺骗: (1)终止符号: 利用在C语言中的终止符号如0(null),CR,LF,-1(EOF)等不能在常用的字符串函数中使用 ,因为这些函数一旦遇到这些终止符号,就结束函数过程了。 (2)随机符号: 利用一个在函数调用时产生的一个32位的随机数来实现保密,使得攻击者不可能猜测到 附加字节的内容。而且,每次调用,附加字节的内容都在改变,也无法预测。 堆栈保护对于各种系统的缓冲区溢出攻击都有很好的保护作用,并能保持较好的兼容性 和系统性能。堆栈保护版本的Red Hat Linux 5.1已经在各种系统上运行了多年,包括个人的 笔记本计算机和工作组文件服务器。这个系统和本来的系统工作完全一样,这表明堆栈保护并 不对系统的兼容性构成很大的影响。堆栈保护中增加了系统的开销,而在网络的测试中,表明 这种开销不是很大,参见表1。 表1 堆栈保护对系统性能影响的测试结果 是否堆栈保护 客户端数目 连接次数 (次/秒) 平均延时 (秒) 平均吞吐量 (Mbits/秒) NO 2 34.44 0.0578 5.63 NO 16 43.53 0.3583 6.46 NO 30 47.20 0.6030 6.46 YES 2 34.92 0.0570 5.53 YES 16 53.57 0.2949 6.44 YES 30 50.89 0.5612 6.48 通过在所有的代码指针之后放置附加字节来检验指针在被调用之前的合法性。如果检验失 败,会发出报警信号和退出程序的执行,就如同在堆栈保护中的行为一样。这种方案有两点需 要注意: (1)附加字节的定位: 附加字节的空间是在被保护的变量被分配的时候分配的,同时在被保护字节初始化过程 中被初始化。这样就带来了问题:为了保持兼容性,不想改变被保护变量的大小,因此不能简 单地在变量的结构定义中加入附加字节。还有,对各种类型也有不同附加字节数目。 (2)检查附加字节: 每次程序指针被引用的时候都要检查附加字节的完整性。这也存在问题:因为编译器关心指 针的使用,而各种的优化算法倾向于从内存中读入变量。随着不同类型的变量,读入的方法也 各自不同。 目前为止,只有很少一部分使用非指针变量的攻击能逃脱指针保护的检测。但是,可以 通过在编译器上强制对某一变量加入附加字节来实现检测,这时需要程序员自己手工加入相应 的保护了。 四.5 程序指针完整性检查与数组边界检查的比较 程序指针完整性检查与数组边界检查相比,并不能防止所有的缓冲区溢出问题。然而在执行 的性能和兼容性上具有相当的优势: (1)性能: 边界检查必须在每个数组元素操作时完成一次检查。相比之下,程序指针检查只在被引 用的时候实现检查。无论在C还是在C++中,这种花在程序指针引用上的开销始终比数组的指针 引用小。 (2)应用效率: 边界检查最难实现之处在于在C语言中,很能确定数组的边界。这是由于在C中,数组的 概念和通用指针的混用造成的。由于一个指针是一个独立的对象,没有与特定的边界条件关联 ,只有一个系统的机器字来存储它,而标识边界信息的资料却没有存放。因此需要特殊的方法 来恢复这些信息;数组的引用将不再是一个简单的指针,而是一个对缓冲区描述的指针组。 (3)与现有代码的兼容性: 一些边界检查方法为了与现有的代码保持兼容而在系统的性能上得到了损失。而另一些 则用别的方法达到目的。这样就打破的传统的C的转换规则,转而产生了一类新的C编译器,只 能编译C的一个子集,有的还不能使用指针或者需要别的改变。 四.6防卫方法的综合分析 在这里综合研究在本文描述的各种漏洞攻击和防卫方法,以此来确定何种组合能完全消 除缓冲区溢出问题。表2列出了关于缓冲区溢出的攻击和防卫的方法。其中,没有把边界检查 计算在内,因为它能有效地防止所有的缓冲区溢出,但是所需的开销也是惊人的。在表2中, 最上面一行列出了攻击代码植入的内存空间,最左面一列是溢出方法,中间的单元为对应的防 卫措施。 最常见的缓冲区溢出形式是攻击活动纪录然后在堆栈中植入代码。这种类型的攻击在19 97年中有很多纪录。而非执行堆栈和堆栈保护的方法都可以有效防卫这种攻击。非执行堆栈可 以防卫所有把代码植入堆栈的攻击方法,堆栈保护可以防卫所有改变活动纪录的方法。这两种 方法相互兼容,可以同时防卫多种可能的攻击。 表2 缓冲区溢出的攻击和防卫的方法 堆栈 (StackBuffer) 堆 (HeapBuffer) 静态空间 (StaticBuffer) 活动记录 堆栈保护 非执行的缓冲区 堆栈保护 堆栈保护 函数指针 指针保护 非执行的缓冲区 指针保护 指针保护 长跳转缓冲区 指针保护 非执行的缓冲区 指针保护 指针保护 其它变量 手工的指针保护 非执行的缓冲区 手工的指针保护 手工的指针保护 其余的攻击基本上可以用指针保护的方法来防卫,但是在某些特殊的场合需要用手工来 实现指针保护。全自动的指针保护需要对每个变量加入附加字节,这样使得指针边界检查在某 些情况下具有优势。 |
地主 发表时间: 04/13 22:01 |
回复: xiaojun [xiaojun] 剑客 | 登录 |
通过缓冲区溢出技术,可以实现远程取得Administrator权限,从而取得最高级控制权。此篇为这种技术的概要论述,与之研究相关者,阅读此篇很重要。 |
B1层 发表时间: 04/14 23:15 |
|
20CN网络安全小组版权所有
Copyright © 2000-2010 20CN Security Group. All Rights Reserved.
论坛程序编写:NetDemon
粤ICP备05087286号