接触过缓冲区溢出的朋友对这个绝对不陌生,EIP,EBP,ESP寄存器。这里先不解释,先看一段代码吧。
char a[8] = "zpf06188"; for (int i=0;i<8;i++) { printf("%# x \n",&a[i]); }
在VC6.0编译器里面,这样的代码是会报一个array bounds overflow的错,下标越界了。这是编译器直接对数组下标的检查,换一种方法,用拷贝函数呢?
#include<stdio.h> #include<string.h> char name[] = "abcdefghijklmnopqrstuvwxyz"; int main() { char output[8]; strcpy(output, name); for(int i=0;i<8;i++) printf("%# x ",output[i]); return 0; }
输出结果就成了:
注意这里的值70 6F 6E 6D 分别是什么?对照码表就可以查出来是ponm,当然存储方式应该是mnop。
堆栈的存储是低位向高位存储,整个操作完成,main函数执行完毕之后,堆栈中的EBP,EIP要回复回去,但是由于在strcpy的时候,拷贝的字符长度已经超过了数组限定的值,这就导致写入的值覆盖了EIP,EBP。最后弹出EIP的时候却发现,它原来的东西没了,被写成了6d6e6f70了。所以出错了。
EBP是"基址指针"(BASE POINTER), 它最经常被用作高级语言函数调用的"框架指针"(frame pointer). 在破解的时候,经常可以看见一个标准的函数起始代码:
ESP 专门用作堆栈指针,被形象地称为栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,ESP也就越来越小。在32位平台上,ESP每次减少4字节。
EIP:寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。
栈的基本模型:
参数N | ↓高地址 |
参数… | 函数参数入栈的顺序与具体的调用方式有关 |
参数 3 | |
参数 2 | |
参数 1 | |
EIP | 返回本次调用后,下一条指令的地址 |
EBP | 保存调用者的EBP,然后EBP指向此时的栈顶。 |
临时变量1 | |
临时变量2 | |
临时变量3 | |
临时变量… | |
临时变量5 | ↓低地址 |
先写个小程序:
void fun(void) { printf("hello world"); } void main(void) { fun() printf("函数调用结束"); }
这是一个再简单不过的函数调用的例子了。
当程序进行函数调用的时候,我们经常说的是先将函数压栈,当函数调用结束后,再出栈。这一切的工作都是系统帮我们自动完成的。
但在完成的过程中,系统会用到下面三种寄存器:
1.EIP
2.ESP
3.EBP
当调用fun函数开始时,三者的作用。
1.EIP寄存器里存储的是CPU下次要执行的指令的地址。
也就是调用完fun函数后,让CPU知道应该执行main函数中的printf("函数调用结束")语句了。
2.EBP寄存器里存储的是是栈的栈底指针,通常叫栈基址,这个是一开始进行fun()函数调用之前,由ESP传递给EBP的。(在函数调用前你可以这么理解:ESP存储的是栈顶地址,也是栈底地址。)
3.ESP寄存器里存储的是在调用函数fun()之后,栈的栈顶。并且始终指向栈顶。
当调用fun函数结束后,三者的作用:
1.系统根据EIP寄存器里存储的地址,CPU就能够知道函数调用完,下一步应该做什么,也就是应该执行main函数中的printf(“函数调用结束”)。
2.EBP寄存器存储的是栈底地址,而这个地址是由ESP在函数调用前传递给EBP的。等到调用结束,EBP会把其地址再次传回给ESP。所以ESP又一次指向了函数调用结束后,栈顶的地址。