一.首先认识由c/c++编译的程序占用的内存有哪些部分:
1.其中堆区与栈区是相对而生的。(之后研究栈帧会将栈区单独列出来)
2.在程序运行期间,地址空间区是一直存在的,程序退出,地址空间也随即释放。
二.其次需要认识一个数据结构以及两个汇编命令。
1.认识一个数据结构:栈。
栈有栈底和栈顶,是一种“先进后出”的结构。
2. 两个汇编命令:push和pop
从栈顶入栈称为push
从栈底出栈称为pop
其中入栈和出栈都是在同一位置进行的。
三.接下来了解CPU中需要用到的一些寄存器。
在CPU中:读取指令(内存-->CPU)-->分析指令(CPU)-->执行指令(CPU)
1. 通用寄存器:EAX 、EBX 、ECX 、EDX
2. EIP(pc):程序计数器(用来存放当前正在执行指令的下一条指令的地址)
3. ESP:栈顶
4. EBP:栈底
四.现在开始研究栈帧。
1. 研究栈帧,接下来会将其从地址空间里面单独列出。
2. 在VC6.0下通过此代码研究栈帧:
#include <stdio.h>
#include <windows.h>
int myadd(int a,int b)
{
int c = a+b;
return c;
}
int main()
{
int a = 0xAAAAAAAA;
int b = 0xBBBBBBBB;
int ret = myadd(a,b);
printf("%d\n",ret);
system("pause");
return 0;
}
3. 现在要做的是:Fn+F11进行调试,然后将其转换成汇编语言,打开寄存器和内存。
main()函数
(1)进入main(),首先会根据该函数内部临时变量的大小为它开辟空间(也就是 main的栈帧)
(2) 开始对main()进行一系列操作
汇编意思:
12行:将0xAAAAAAAA(a)放入ebp-4的位置(即为a开辟4字节空间,其&a = ebp-4)
13行:将0xBBBBBBBB(b)放入ebp-8的位置(即为b开辟4字节空间,且在a下方)
寄存器与内存的情况:
在栈帧中:
临时变量:
(1)形成a、b的临时变量
汇编解释:将b存在eax中,然后b入栈
将a存在ecx中,然后a入栈
(注意:先传入的是b,然后才是b)
寄存器和内存的变化:
b形成临时变量:EAX 、esp以及esp的内容均发生了变化
a形成临时变量:继续观察EAX 、esp以及esp的内容
在栈中:观察如何创建临时变量:
将变量(a或者b)放在EAX中,EAX入栈,esp下移4字节(int所占空间的大小)。
结论:在传递变量是从左往右传,形成临时变量是从右向左。
(2)现在是进入被调用函数的准备工作:
1) 执行时:注意观察esp(执行现在esp为0019FEE0)的变化,以及之后一步EIP的变化
寄存器和内存的情况:
2)esp的变化(将现在执行指令的下一条指令 00401093 进行了入栈操作)
3)现在观察EIP的变化:跳转到了目标函数的入口。
4)则call命令的执行分为两步:
1) 将当前执行指令的下一条指令入栈。
2) 跳转至目标函数,也就是修改EIP的值。
5) 在栈帧中的情况:
(3)myadd()入栈操作,形成它的栈帧
汇编含义:ebp入栈--->ebp指向esp的位置--->esp-44h形成myadd()栈帧。
寄存器和内存的变化:ebp入栈:esp下移将ebp的值放入。
现在是将ebp指向esp指向的位置,esp-44h创建了myadd()的栈帧。
在栈帧中的情况:
对myadd()的调用
(1)调用后对函数的的执行
汇编含义:将ebp+8(即a)放入eax中,将b+a的值放入eax中,将eax放在ebp-4处。
在将ebp-4放入eax中。
寄存器和内存的变化:
EAX放入a的值
EAX放入a+b的值
将EAX入栈
在栈帧中的情况:
(2)myadd()的出栈过程。
汇编含义:
esp指向ebp所指的位置(即释放了myadd()的栈帧,不过它只是认为可以其他覆盖,所以当刚刚释放完之后还是 可以找到其内部的变量)
ebp出栈:返回到刚才存入mai_ebp的位置(即回到main()栈顶)
开始执行ret指令:
可以看出:ret执行执行两点:
1) 将当前esp所指向的内容(即main()的返回值地址出栈)
2) 然后用其修改EIP。
栈帧的情况:
(3)返回到main()函数
汇编含义:
esp+8即指向main的esp,也就是释放了临时变量的空间。
将eax移到ebp-ch的位置,即ret的位置。
栈帧的情况:
以上便完成了一次完整的函数调用。
五.利用已知的栈帧结构改变函数的返回位置:
main()--> myadd() --> bug()--> main()的过程。
由栈帧结构可以知道:call中保存了返回函数的地址,所以只要将这一地址改为bug()的入口地址即可。
执行代码以及代码分析
#include <stdio.h>
#include <windows.h>void* g_ret = NULL;
void bug()
{
int x = 0;
int *p = &x; //取x的地址,将其放在指针变量P中
p += 2; //找到恢复函数的地址
*p = (int) g_ret;//将之前最开始返回的地址有放回原处,保证可以回到main函数
printf("i am a bug!haha...you run here!\n");
system("pause");
}
int myadd(int a,int b)
{
int *p = &a; //定义一个指针变量p,使他指向a
int c = 0;
p--; //p-1到达要返回地址
g_ret =(void*) *p;//将它存在g_ret中
*p = (int)bug; //将其接受bug的地址,待会跳转至bug函数
c = a+b;
printf("myadd begin run!...\n");
return c;
}
int main()
{
int a = 1;
int b = 2;
int ret = 0;
printf("main gegin run!...\n");
ret = myadd(a,b);
printf("you should run here! \n");
_asm{ //bug函数是通过修改地址进入的,没有执行call命令,则它少一个地址的push过 程,而调完之后依旧执行了pop指令,则是的esp大了4
sub esp,4 //则需要将其-4以保证栈的平衡性
}
system("pause");
return 0;
}