栈溢出攻击原理
栈溢出原理
马上软考了,补一补
寄存器分配 ESP、EBP、EIP
以32位x86架构为基础,Windows提供三个寄存器帮助定位栈和函数调用--ESP、EBP、EBP
ESP
ESP 用来存储函数调用栈的栈顶指针
,指向栈区中最上一个栈帧的栈顶
EBP
EBP 用来存储当前函数状态的基地址,即栈底指针
,指向栈区中最上一个栈帧的栈底
EIP
EIP 用来存放下一个执行语句的地址的指令寄存器
函数调用步骤
下面让我们来看看发生函数调用时,栈顶函数状态以及上述寄存器的变化。变化的核心任务是将调用函数(caller)的状态保存起来,同时创建被调用函数(callee)的状态。
所谓被调用函数,就是函数中的函数 Caller's Caller's ebp=Callee's ebp
void caller(arg1,arg2,....argn){
callee(arg1,arg1,arg2,....argn);
}
Step1.参数入栈
首先将被调用函数(callee)的参数按照逆序依次压入栈内 argn......arg2,arg1
这个栈是从下往上压的,也就是esp位置的先出栈
Step2.返回地址入栈
将调用函数(caller)进行调用之后的下一条指令地址作为返回地址压入栈内。这样调用函数(caller)的 EIP(指令地址)保存在 Return Address
中。这里的地址就是callee()运行完接着caller()下一条指令执行的地址
Step3.调用函数的EBP入栈
将当前的 EBP 寄存器的值(也就是调用函数caller()的基地址)压入栈内
并将EBP(栈底)的值更新为当前ESP(栈顶)的地址 → mov esp, ebp
Step4.被调用函数局部变量入栈
到这一步,被调用函数callee的函数帧结构就完整了
在压栈的过程中,esp 寄存器的值不断减小,栈内数据不断变大(对应于栈从内存高地址向低地址生长)。
压入栈内的数据包括:调用参数(argn...arg1)、返回地址(Return Address)、调用函数的基地址(新EBP),以及局部变量(Local Variables)
其中调用参数
以外的数据共同构成了"被调用函数(callee)的状态"。在发生调用时,程序还会将被调用函数(callee)的指令地址存到 EIP 寄存器内,这样程序就可以依次执行被调用函数的指令了。
看过了函数调用发生时的情况,就不难理解函数调用结束时的变化。变化的核心就是丢弃被调用函数(callee)的状态,并将栈顶弹出恢复为调用函数(caller)的状态。
Step5.将被调用函数的局部变量弹出栈外
首先被调用函数的局部变量会从栈内直接弹出,栈顶会指向调用函数(caller)的基地址。
Step6.弹出调用函数EBP
然后将基地址内存储的调用函数(caller)的基地址从栈内弹出,并存到 ebp 寄存器内。这样调用函数(caller)的 ebp(基地址)信息得以恢复。此时栈顶会指向返回地址。下图容易误解,其实这里的Caller's Caller's ebp已经变成了Caller's ebp
Step7.弹出返回地址,存入EIP
再将返回地址从栈内弹出,并存到 eip 寄存器内。这样调用函数(caller)的 eip(指令)信息得以恢复到caller()的执行状态。
至此调用函数的状态恢复,之后继续执行调用函数的指令
栈溢出攻击
什么是栈溢出呢?
栈溢出是指向向栈中写入了超出限定长度的数据,溢出的数据会覆盖栈中其它数据,从而影响程序的运行。如果我们计算好溢出的长度,编写好溢出数据,让我们想要的地址数据正好覆盖到函数返回地址
那么被调函数调用完返回主函数时,就会跳转到我们覆盖的地址上。所以控制程序执行指令最关键的寄存器就是 EIP
,我们的目标就是让EIP
载入攻击指令的地址。通过这样改变程序流程,接下来我们就可以干很多坏事了!
该怎么构造呢,这里有个简单的缓冲区溢出的例子
void f(char *str){
char buf[16];
strcpy(buf,str);
}
void main(){
char buf[128];
for(i=0;i<=127;i++)
buf[i]='A';
f(buf);
print("It's Buffer OverFlow!");
}
这是执行被调用函数f()的stack
EIP(Return Address)
其中的buf是局部变量,但是我们从程序中可以看出,128位的*str输入明显越界,会导致数据溢出,溢出就会覆盖EBP、EIP
我们通过精心构造一个位数精准的数据,使得EIP处正好被我们溢出的那几位数据覆盖为AAAA,其中A对应ASCII 0x41
AAAA对应EIP地址为0x41414141
当0x41414141
覆盖了原来EIP的位置时,f函数在返回时就会将0x41414141
弹出来给EIP,假如这时shell程序调用地址是0x41414141
,那么main()接下来就会去执行shell,我们就能运行shell实施攻击
(这里全填A是为了方便理解,实际情况构造shell地址肯定不是AAAA哈,自己根据shell地址构造就行了)
防范缓冲区溢出策略
常用的函数有strcpy()
、sprintf()
、strcat()
、vsprintf()
、gets()
、scanf()
,以及在循环内的函数getc()
、fgetc()
、getchat()
等都非常容易导致溢出
防范策略有:
系统管理防范策略:关闭不必要的特权程序、及时打系统补丁
开发时的防范策略:注意危险函数的调用、缓冲区不允许执行、静态分析检查指针、堆栈向高地址方向增长等