在阅读《深入理解计算机系统》第三章,程序的机器级表示的时候,解开了以前学习C语言的时候的一些疑惑,在这里稍做记录。
程序在调用另一个程序的时候涉及下面的这个数据结构——栈帧,这个数据结构是实现整个过程的关键。
调用者程序调用(call指令)另一个被调用者程序的时候,首先调用者程序将将当前程序计数器的值(下一条指令的地址)保存(push)在栈帧中,再跳转到被调用者程序。然后被调用者程序保存原来的帧指针(%ebp),并且跟新%ebp使得它指向栈底,然后通过减少栈指针(%esp)的值给程序自身分配栈空间,用于存放被调用者程序需要保存的寄存器值,被调用者函数所需的局部变量,临时变量,以及需要传递给它所调用函数的参数,最后被调用者程序调用ret指令返回到之前保存的程序计数器的值所指向的程序的地址。
以上是针对IA32架构的,新的x86-64架构,引入了更多的寄存器(从8个到16个)从而对程序调用产生了影响,当然工程师们出于其它原因的考虑还做了优化。总结如下:
- 参数(最多是前6个)通过寄存器传递到过程,而不是在栈上。这消除了在栈上存储和检索值的开销。
- callq指令将一个64位地址存储在栈上。
- 函数最多可以访问超过当前栈指针128个字节的栈上存储空间。这允许一些函数将信息存储在栈上而无需修改栈指针。
- 没有帧指针,作为替代,对栈位置的引用相对于栈指针。大多数函数在调用开始时分配所需要的整个栈存储,并保持栈指针指向固定位置。
理解了函数调用的一般原理,下面记录一下缓冲区溢出带来的攻击问题。
其实道理很简单,buf是一个指向局部变量(是一个字符数组)的一个指针参数,传给某个接口,书中以gets为例,但是gets存在漏洞(它不检查buf缓冲区的大小),如果程序用户通过gets输入的字符串大于buf的大小,就会覆盖保存%ebp的地方以及程序的返回地址,如果在返回地址上覆盖上攻击者代码的指针,那么攻击者就可以调用自己的程序胡作非为了。
针对这种攻击方法,现在已经有一些方法了。分别是
1.栈随机化,从而无法在攻击字符串中嵌入攻击代码程序的指针。2.栈破坏检测。3.限制可执行代码的区域。