从进程地址空间分布到缓冲区溢出

进程的地址空间


一个进程的空间分布大致如上图所示,主要由数据段、代码段、堆、栈这几部分组成,其中代码段(.code)主要存放程序代码在内存中的映射;数据段具体会分为初始化数据段(.data)和为初始化数据段(.bss);而堆主要用于存放局部变量、临时变量,函数调用时,存放函数的返回指针,用于控制函数的调用和返回;堆用于存储动态内存分配,需要手动分配,手动释放,比如我们常见的malloc()函数产生的值就是存放在堆中。

 

运行时栈

如上面的过程所示,函数在执行的时候,对栈帧的影响。IA32和x86-64在栈帧的需求上有很大的不同,对于IA32来说,它的栈指针会随着值的压入和弹出不断前后移动,但是x86-64过程的栈帧通常有固定的大小,如上例,在进程开始时通过减小栈指针(寄存器%rsp)来设置。在调用的过程中,栈指针保持在固定的位置,使得可以用相对与栈指针的偏移量来访问数据。因此,也就不再需要IA32代码中可见的帧指针(寄存器%ebp)了。

那么什么时候函数需要栈帧呢?

<1>局部变量太多,不能都放在寄存器中;

<2>有些局部变量是数组或结构;

<3>函数用去地址操作符(&)来计算一个局部变量的地址;

<4>函数必须将栈上的某些参数传递到另一个函数;

<5>在修改一个被调用者保存寄存器之前,函数需要保存它的状态。

 

缓冲区溢出

所谓缓冲区溢出(buffer overflow),比如自爱栈中分配某个字节数组来保存一个字符串,但是字符串的长度超出了为数组分配的长度,这个时候就会发生缓冲区溢出。缓冲区溢出的一个致命的使用就是让程序执行它本来不愿意执行的函数。这是一种常见的通过计算机网络攻击系统安全的方法。通常,输入给程序一个字符串,这个字符串包含一些可执行代码的字节编码,成为攻击代码。另外,还有一些字节会用一个指向攻击代码的指针覆盖返回地址。那么,执行ret指令的效果就是跳转到攻击代码。

 

防护以及对抗缓冲区溢出

(1)栈随机化(主要受linux系统版本限制,老版本不支持栈随机化):使得栈的位置在程序每次运行时都有变化。为了在系统插入攻击代码,攻击者不但要插入代码,还需要插入指向这段代码的指针(指向攻击代码的首地址/栈地址),这个指针也是攻击字符串的一部分。产生这个指针需要知道这个字符串放置的栈地址。老的系统版本,如果在相同的系统运行相同的程序,栈的位置是相当固定的。所以黑客可以在一台机器上研究透系统上的栈是如何分配地址的,就可以入侵其它主机。

实现的方式:程序开始时,在栈上分配一段0~n字节之间的随机大小的空间。分配的范围n必须足够大,才能获得足够多样的栈地址变化,但是又要足够小,不至于浪费程序太多的空间。tradeoff。

(2)栈破坏检测(主要受GCC版本的限制,老的GCC版本不支持栈破坏检测):检测到何时栈被破坏。从strcpy等函数我们可以看到,破坏通常发生在当超越局部缓冲区的边界时。在C语言中,没有可靠的方法来防止对数组的越界写。但是,我们能够在发生了越界写的时候,并且,在其还没有造成任何有害结果之前,尝试检测到它,并且把程序终止。

实现的方式:金丝雀,加入一种栈保护机制。 在栈帧中,紧接着局部缓冲区的位置放置一个哨兵(金丝雀),哨兵值是随机产生的,攻击者没有简单的方法能够知道它是什么。在恢复寄存器状态和函数返回之前,程序检查这个金丝雀的值是否发生改变,如果发生改变立即终止程序。《深入理解操作系统》P182页,有一个特别好的例子。

 

(3)限制可执行代码区域(主要受硬件版本的限制,需要硬件支持):消除攻击者向系统插入可执行代码的能力,一种方法是:限制那些能够存放可执行代码的存储器区域。在典型的系统中,只有保存编译器产生的代码的那部分存储器才需要是可执行的,其它部分可以被限制为只允许读和写。

一般的系统允许三种访问的形式:读(从存储器读数据)、写(存储数据到存储器)和执行(将存储器的内容看作是机器级代码)。以前,x86体系结构将读和执行访问控制合并为1位的标志,这样任何被标记为可读的页都是可执行的。栈又要求必须是既可以读又可以写的,所以x86体系结构栈上的字节都是可执行的。也有一些体制,能够限制一些页是可读但是不可执行,但是这些体制一般都会带来严重的性能损失。

实现的方式:AMD为它的64位存储器的内容保护引入了“NX”(No-eXecute,不执行)位,将读和执行访问模式分开,intel也跟进了。从这开始,栈可以被标记为可读、可写,但是不可执行。检查页是否可执行由硬件来完成,效率上没有损失。

 http://www.cnblogs.com/fanzhidongyzby/p/3250405.html



展开阅读全文

没有更多推荐了,返回首页