一、寄存器ESP、EBP、EIP
- CPU的ESP寄存器存放当前线程的栈顶指针,
- EBP寄存器中保存当前线程的栈底指针。
- CPU的EIP寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行
二、函数调用模式
堆栈由逻辑堆栈帧组成。当调用函数时逻辑堆栈帧被压入栈中,当函数返回时逻辑堆栈帧被从栈中弹出。堆栈帧包括函数的参数,函数地局部变量,以及恢复前一个堆栈帧所需要的数据,其中包括在函数调用时指令指针(IP)的值。
当一个例程被调用时所必须做的第一件事是保存前一个 FP(这样当例程退出时就可以恢复)。然后它把SP复制到FP,创建新的FP,把SP向前移动为局部变量保留空间。这称为例程的序幕(prolog)工作。当例程退出时,堆栈必须被清除干净,这称为例程的收尾(epilog)工作。Intel的ENTER和LEAVE指令,Motorola的LINK和 UNLINK指令,都可以用于有效地序幕和收尾工作
三、函数示例原理
void function(int a, int b, int c)
{
char buffer1[5];
char buffer2[10];
}
void main()
{
function(1,2,3);
}
使用gcc的-S选项编译, 可以产生汇编代码输出
即$ gcc -S -o example1.s example1.c
对function()的调用被汇编成如下代码:
pushl \$3
pushl \$2
pushl \$1
call function
以从后往前的顺序将function的三个参数压入栈中, 然后调用function()
pushl %ebp
movl %esp,%ebp
subl $20,%esp
将帧指针EBP压入栈中. 然后把当前的SP复制到EBP, 使其成为新的帧指针. 我们把这个被保存的FP叫做SFP. 接下来将SP的值减小, 为局部变量保留空间.
我们必须牢记:内存只能以字为单位寻址. 在这里一个字是4个字节, 32位. 因此5字节的缓冲区会占用8个字节(2个字)的内存空间, 而10个字节的缓冲区会占用12个字节(3个字)的内存空间. 这就是为什么SP要减掉20的原因。
从上图来看,假如我们输入的buffer1超长了,直接覆盖掉后面的sfp和ret,就可以修改该函数的返回地址了。
四、具体实例
函数foo是正常的函数,在main函数中被调用,执行了一段非常不安全的strcpy工作。利用不安全的strcpy,我们可 以传入一个超过缓冲区buf长度的字符串,执行拷贝后,缓冲区溢出,把ret返回地址修改成函数bar的地址,达到调用函数bar的目的。
#include <stdio.h>
#include <string.h>
void foo(const ch