关于函数栈帧的有趣代码
1 [cc lang="c"] 2 #include "stdafx" 3 void surprise() 4 { 5 printf("surprise!\n"); 6 exit(0); 7 } 8 9 void test() 10 { 11 int tmp = 10; 12 int *p = (int *)(&tmp + 4); //这里所加的值与代码编译环境有关 13 //此处的4是在VS2017中测试所得 14 *p = (int)surprise; 15 } 16 17 int main() 18 { 19 test(); 20 return 0; 21 } 22 [/cc]
初看到这个代码,完全不理解这是个什么意思
学习过函数的栈帧后,就觉得很容易理解了。
其结果为程序运行了surprise函数。
过程其实很简单。
首先main函数调用test函数。
在test函数中,先定义了一个int型变量,
然后定义了一个指针p,这个指针指向tmp地址加4的地址,
最后把这个地址里的数据改为surprise函数的指针。
其实这个地址存的就是main函数调用test函数之后要返回的指令的地址。
无图无真相,下面是我在vs2017中分析的过程:
首先我们在main处设置断点,以便于接下来的调试。
对于main之前的过程这里不做分析。
接下来按f5进入调试,在代码上右键点击选择转到反汇编。
我们就会看到如下汇编代码:
1 [cc lang="c"]int main() 2 { 3 002A17C0 push ebp 4 002A17C1 mov ebp,esp 5 002A17C3 sub esp,0C0h 6 002A17C9 push ebx 7 002A17CA push esi 8 002A17CB push edi 9 002A17CC lea edi,[ebp-0C0h] 10 002A17D2 mov ecx,30h 11 002A17D7 mov eax,0CCCCCCCCh 12 002A17DC rep stos dword ptr es:[edi] 13 test(); 14 002A17DE call _test (02A1384h) 15 return 0; 16 002A17E3 xor eax,eax 17 }[/cc]
其中有两个指针寄存器
ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
我们可以看到前三句指令就是为main函数在内存中分配了一个大小为0C0h的栈。
接着将ebx,esi,edi入栈,这里暂且不管这三个寄存器作用。
然后下面到call之前的代码是将之前分配的栈初始化。
call指令调用test函数(02A1384h) ,这里我们先记下return指令的地址:002A17E3
在VS2017中选择 “调试-->窗口-->内存-->内存1 ” 进入内存查看窗口:
运用逐语句及逐过程调试菜单将汇编代码运行至call指令处,
过程中在内存窗口中我们可以通过esp所指向的地址来查看相应内存变化。
可以观察到,0x0135FC5C - 0x0135FD24为main函数的栈。

接着按f11(逐语句)执行call命令,
我们会发现在main函数的栈顶压入了一个地址002A17E3,
对,就是刚才main中test()的下一条指令return的地址。

划重点,后面在test函数中就是通过改变这个地址的值为surprise函数的地址,
使程序在test函数执行后跳转至surprise函数的。
逐步执行程序,可以得知如下图所示信息,
因为内存中栈是由高地址向低地址扩展的,因此test的栈底紧挨着main的栈顶,
从而可以在test函数中根据tmp的地址增加4来找到main的返回地址,进而改变程序的运行。

在test的汇编中有这么几句,暂时不太理解其含义。
mov eax,dword ptr [__security_cookie(02AA000h)]
mov eax,ebp
mov dword ptr [ebp-4],eax

到此,关于这个代码的分析就结束了。
当然,日常编码中我觉得没人会这么干。
不过,这个有趣的代码加深了我对栈帧的理解。

浙公网安备 33010602011771号