[zz]堆栈溢出的运行时探测

转载自http://blog.csdn.net/dajuan1989/article/details/7307671

文章:Run-time Detection of Heap-based Overflows
作者:William Robertson, Christopher Kruegel, Darren Mutz, and Fredrik Valeur(University of California, Santa Barbara)

声明:自己的理解不一定很透彻,对于自己就当笔记,不是逐字句翻译的。仅供大家参考,若有错误或不当之处欢迎批评指正!

摘要:缓冲区溢出是如今网络上最常见的攻击类型之一。虽然目前基于栈的变量更普遍,基于堆的溢出正在获得愈来愈多的关注。现实世界中有一些漏洞,能够破坏堆管理信息并允许任意代码优于被攻击者的程序得以执行。

        本文提出了一项保护堆管理信息的技术,允许实时探测堆溢出。我们讨论了这些攻击的结构,我们提出的探测方案已经作为GNU Lib C的补丁。实验结果展示了此方法的探测效率。另外,我们讨论了内存保护的不同机制。

1. 简介

        缓冲区溢出漏洞属于如今网络中最骇人听闻的攻击。最近的研究表明报告给CERT的漏洞中大约50%是关于缓冲区溢出的。

        最常见的缓冲区攻击是基于堆栈溢出的。漏洞利用了程序调用的返回地址与局部变量存储在一起,溢出一个局部变量可以重写一个返回地址。当函数返回时更改程序流。这就无形中允许了恶意用户执行任意代码。

        基于堆栈的溢出可以分成两类:一类是分配给堆的缓冲区溢出,能够直接更改相邻内存块的内容;另一类则更改内存管理员使用的管理信息(如分配和释放内存)。多数内存分配都会将管理信息存储在本身的堆空间。攻击的主要思想就是修改管理信息,使得可以实现任意内存重写。这样,返回地址、连接表或者应用级别的数据都可以被更改。这类攻击是被Solar Designer首先提出的。

          本文提出的技术是为了保护基于边界标签的堆管理信息,防止被恶意或者意外更改。这种方法已经被用到Doug Lea的GNU Lib C 2.3版本的内存分配中了。

2. 关于堆栈溢出的预防和探测的相关工作

         目前关于堆栈溢出的预防和探测已经有很多研究。比较有名的是StackGuard,属于编译器的扩展,在每个函数返回地址前插入一个标识字符。当执行基于堆栈的攻击时,入侵者企图溢出分配给本地堆栈的缓冲区来改变当前函数的返回地址。这可能会使入侵者改变程序执行流并且控制执行过程。通过在返回地址和局部变量之间插入标识字符,返回地址的溢出会改变这个标识符,因此可以被探测到。

        也有不同的防攻击机制,通过仅仅在溢出流中插入标识符,这样会使保护机制不那么有效。一种解决方案是在启动程序的时候选择随机标识符,这样的标识符不能被猜到。另一种方案就是使用由4个不同字节组成的终结标识符,这4个字符由字符串操作函数库中的函数生成。这种方法原理是,入侵者需要插入这些字符溢出缓冲区来重写标识符并且不被探测到。然而,字符串操作函数遇到终结字符的时候会停止,因此返回地址保持不变。

        StackShield是一种类似的方法,这种方法不是在栈中插入标识符,而是保留第二个栈,只用来存储返回地址的副本。在一个程序返回之前,副本会与原始的地址进行比较,任何偏差都会使程序中止。基于堆栈的溢出是利用管理信息(函数返回地址)和数据(自动变量和缓冲区)存储在一起这个事实。StackGuard和StackShield都是强制保持栈内管理信息的完整性。我们的技术也是基于这种思想,将保护扩展到保护堆的管理信息。

        其他的预防堆栈溢出的方案不是被编译器强制加上的而是以库的形式执行。 Libsafe 和Libverify执行并覆盖了C函数库中的不安全函数。安全版本为运行中的缓冲区预测了一个安全边界,在任何写操作进行之前要先检查这个边界。这防止了在重写函数地址时的用户输入。

        另一种方法是使栈不能执行。虽然这并不能防止真正的溢出和返回地址的修改,这种方法是基于多数漏洞直接在栈上运行恶意负载这个事实。这种方法可能会阻碍合法使用,比如函数编程语言在运行时产生代码并且在栈上执行的时候。gcc使用可执行栈作为嵌套函数的中转,Linux用可执行用户栈来进行信号处理。这个问题的解决方案就是探测合法使用,动态地运行程序。然而,这样的方案通常情况下很难执行。

         关于保护堆内存的研究还不多。提供内存保护的系统是内存调试器,比如Nalgrind或者Electric Fence.这些工具监督内存使用(读和写)并且拦截内存管理调用来检测错误。这些工具使用的方法与我们的类似,企图保持使用内存的完整性。然而,每个内存使用都要进行检查,而我们的方法只有在分配和回收内存块的时候才需要进行检查。内存调试器有效地阻止非授权的内存使用,防止基于堆的缓冲区溢出。然而,这些工具会产生很大的运行代价,使程序变得很慢。

 3. 技术

3.1 GNU Lib C(glibc)中的堆管理

        C语言没有提供如动态内存管理、字符串操作、输入输出等内置功能,而是把这些功能定义在一个标准库中,当用户使用的时候会被编译和链接。GNU C库就是这一一个库,定义了ISO C标准中的所有库函数,以及POSIX(可移植操作系统接口)和GNU系统的特定函数。

        C语言支持两种内存分配机制:静态和自动。当一个变量被声明为静态或者全局变量的时候采用静态内存分配,每个静态或者全局变量定义了一个固定大小的块空间。这个空间从程序开始就被分配,在运行期间也不释放。当变量是自动变量(如函数参数或者局部变量)时进行自动内存分配,当遇到包含声明的复合语句时会自动分配内存空间,当退出复合语句的时候内存就被释放。

        第三种内存分配方式是动态分配,这种方式不受C语言支持,但是通过glibc函数使用。动态内存分配是一种由程序决定哪些信息需要被存储的技术。当需要的内存数量或者内存使用周期未知的时候需要使用这种技术。使用到的两个基本函数:malloc,动态分配内存;free,释放内存。此外还有其他函数如calloc,realloc。

        GNU Lib C使用了Doug Lea的内存分配器dlmalloc来执行动态内存分配函数。dlmalloc利用了两个核心特征,边界标签(boundary tags)和分箱技术(binning),来代替用户程序管理内存分配请求和释放请求。

        内存管理是基于“块”的,即包含可用区域和内存管理信息的内存块。内存管理信息也叫边界标签(boundary tags),存储在每个块的开始位置,保持当前块和前面块的大小。这样便允许两个相邻的未使用块合并为一个大块,使得未使用的小块数量保持较小,减少碎片。

        当前不使用的块存储在bin中,根据大小分组。小于512字节的文件只保存一个尺寸,大于等于512字节的,其大小按几何级数增长。搜索可用块的时候是按照最小优先的策略进行的,根据需要的内存大小在合适的bin中开始寻找。对于未分配的块,管理信息包含了两个指针,用双向链表(自由链表)存储块和对应的bin。这些链表指针被称为forward (fd) 和back (bk)。

        对于32位的结构,管理信息通常包含4字节大小的信息域(块大小和前面块的大小)。当块未必分配时,也会包含两个4字节大小的指针用来操作bin中自由块的双链表。

3.2 堆溢出漏洞剖析

       使用前后指针连接bin中的可用块存在安全隐患:如果恶意用户可以使分配的内存块溢出,用户可以重写相邻的下一个内存块头指针。当溢出块未被使用时存储在bin的双向链表中,攻击者就可用控制块中前后指针的值。基于此,考虑到下面glibc使用的unlink宏:

#define unlink(P, BK, FD) { \
[1] FD = P->fd;  \
[2] BK = P->bk;  \
[3] FD->bk = BK;  \
[4] BK->fd = FD;  \
}

       如果试图溢出bin中自由表的块,unlink过程便会遭到恶意用户的破坏,在任意内存地址中写任意值。

在上面的unlink宏中,第一个参数P指向将要从双向链表中移除的块。攻击者必须在P→fd中存储指针地址(-12字节,下面会解释),在P→bk中存储需要的值。在第[1] 行和第[2] 行中,前指针(P →fd)和后指针 (P→bk) 的值分别被存储在临时变量FD和BK中。在第[3] 行中,FD被间接引用,FD中的地址增加了12字节(边界标签中bk的偏移量)。这种技术可以用来改变程序的GOT (Global Offset Table) 并且再直接访问攻击者代码的函数指针。

       上图中的堆溢出漏洞变种,操作块的大小域,而不是列表指针。攻击者可以给相邻块的大小域提供任意值,类似于操作列表指针。当大小域可以操作的时候,例如合并两个未使用块,堆管理过程可能被诱使其将攻击者控制下的内存区域作为下一个块。攻击者可以在那个位置设置一个伪块来进行攻击。如果攻击者由于某种原因未能写相邻块的列表指针但是能够操作其大小域,那么这次攻击也不一定成功。

3.3 堆完整性探测

       为了保护堆,我们的系统对glibc的对管理做了很多改进,包括每个块的结构和各自的管理方式。如下图:

        保护块管理信息的第一个元素是预先在块结构中插入一个标识符,还加了一个填充区域_pad0。标识符包含了块头种子和随机值的检校和,下面会具体描述。

        第二个元素是引入种子值的全局检校和,保留在一个静态变量中(_heap_magic),这个变量在程序启动时被赋予一个随机值,然后通过一个函数设置内存映像保护来防止被被改写。这与依赖重复函数设置内存映像保护的堆保护方案相反。我们只需要程序启动时一次调用,不用遭受其他方案在运行时产生的一些损失。

        最后一个元素是增大堆管理方案,用代码管理和检查每个块的标识符。新分配的块的标识符被初始化为保护内存位置和块大小域的检校和,有全局变量_heap_magic的种子值。注意检校和不包括分配块的列表指针,因为这些区域属于块中用户数据部分。新块才能使用这个方法。

        当一个块通过释放内存函数返回到堆管理,其标识符要受检查看是否和分配时的检校和一致。如果存储的值不匹配,就认为堆管理信息被破坏了。 这时会引发警报,程序会中断。否则,程序照常进行,块被插入到bin中,必要时与其他块进行合并。任何链表操作都在检查块的标识符之前进行。当块被插入到bin中后,其标识符会被更新。

        上面提到的元素通过修改块的头信息有效预防任意位置的内存写操作,无论通过溢出还是直接操作块的头信息。每个分配的块都通过随机种子的检校和进行保护,每个链表指针都要通过检查完整性进行保护。

        这种方法还有其他用处,比如可以探测出无意的堆溢出或者重复调用释放函数。此方法缺点就是不能定位指针破坏攻击,比如破坏应用函数的指针。也不能保证 用户数据的完整性,只能保证块的头信息的有效性。

        需要注意的是glibc的堆管理已经包含了保证堆管理完整性的功能。但是使用调试方案会增加资源耗费。这种方案对堆的自由链表和全局状态进行全面的检查,包括与对制作漏洞无关的检查。此外,不能保证所有的攻击都被探测到。不是所有的链表操作都会被检查,恶意值可能通过不是针对堆溢出的完整性检查。这样,我们只能说这种方法不适合保护我们提到的那些情况。

        上面的系统已经被应用到glibc2.3和glibc2.2.9x中,以及RedHat 8.0使用的glibc预发布版本。这项技术可以被灵活运用到其他堆设计中,下一步需要将此技术应用到除了glibc之外的其他开放式系统。

posted @ 2012-06-07 21:12  yarpee  阅读(411)  评论(0编辑  收藏  举报