栈溢出
栈(stack)是一种简单且经典的数据结构,最主要的特点是使用先进后出(FILO)的方式存取栈中的数据。一般情况下,最后放入栈的数据被称为栈顶数据,其存放位置被称为栈顶。向栈中存放数据的操作被称为入栈(push),取出栈顶数据的操作被称为出栈(pop)。
由于函数调用的循序也是最先调用的函数最后返回,因此栈非常适合保存函数运行过程中使用到的中间变量和其他临时数据。
目前,大部分主流指令架构(x86、ARM、MIPS等)都在指令集层面支持栈操作,并且设计有专门的寄存器保存栈顶地址。大部分情况下,将数据入栈从内存高地址向低地址增长。
-
栈溢出原理
栈溢出是缓冲区溢出的一种。函数的局部变量通常保存在栈上。如果这些缓冲区发生溢出,就是栈溢出。最经典的栈溢出利用方式是覆盖函数返回地址,以达到劫持程序控制流的目的。
x86架构中一般使用指令call调用一个函数,并使用指令ret返回。CPU在执行call指令时,会先将当前call指令的下一条指令的地址入栈,再跳转到被调用函数。当被调用函数需要返回时,只需要执行ret指令。CPU会出栈栈顶的地址并赋值给EIP寄存器。这个用来告诉被调用函数自己应该返回到调用函数什么位置的地址被称为返回地址。理想情况下,取出的地址就是之前调用call存入的地址。这样程序可以返回到父函数继续执行了。编译器会始终保证即使子函数使用了栈并修改了栈顶的位置,也会在函数返回前将栈顶恢复到刚进入函数时候的状态,从而保证取到的返回地址不会出错。
例子可以看N1Book-Pwn系列I中的第一题。
-
栈保护技术
栈溢出利用难度很低,危害巨大。为了缓解栈溢出带来的日益严重的安全问题,编译器开发者们引入Canary机制来检测栈溢出攻击。
Canary中卫译为金丝雀。Canary保护机制是,通过在栈保存rbp的位置前插入一段随机数,这样如果攻击者利用栈溢出漏洞覆盖返回地址,也会把Canary一起覆盖。编译器会在函数ret指令前添加一段会检查Canary的值是否被改写的代码。如果被改写,则直接抛出异常,中断程序,从而阻止攻击发生。
-
常利用栈溢出的危险函数
通过寻找危险函数可以助我们快速确定程序是否存在栈溢出漏洞,以及栈溢出的位置,常见的危险函数如下:
-
输入:
-
gets(),直接读取一行,到‘\n’为止,同时‘\n’被转换为’\x00’;
-
scanf(),格式化字符串的%s不会检查长度;
-
vscanf(),同上;
-
-
输出:
- sprintf(),将格式化后的内容写入缓冲区中,但是不会检查缓冲区长度;
-
字符串:
- strcpy(),遇到’\x00’停止,不会检查长度,经常容易出现单字节写0溢出(堆利用-off-by-one);
- strcat();
-
-
可利用的栈溢出覆盖位置
可利用的栈溢出覆盖位置通常有3种:
- 覆盖函数返回地址,上面的例子都是通过覆盖返回地址控制程序。
- 覆盖栈上所保存的BP寄存器的值。函数被调用时会先保护栈现场,返回时在恢复。
返回时:如果栈上的BP值被覆盖,那么函数返回后,主调函数的BP值会被改变,主调函数返回指行ret时,SP不会指向原来的返回地址位置,而是被修改后的BP位置。 - 根据现实执行情况,覆盖特定的变量或地址的内容,可能导致一些逻辑漏洞的出现。