1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "stdafx"
void surprise()
{
printf("surprise!\\n");
exit(0);
}

void test()
{
int tmp = 10;
int *p = (int *)(&tmp + 4); //这里所加的值与代码编译环境有关
//此处的4是在VS2017中测试所得
*p = (int)surprise;
}

int main()
{
test();
return 0;
}

初看到这个代码,完全不理解这是个什么意思,学习过函数的栈帧后,就觉得很容易理解了。

其结果为程序运行了surprise函数。

过程其实很简单。 首先main函数调用test函数。 在test函数中,先定义了一个int型变量,然后定义了一个指针p,这个指针指向tmp地址加4的地址,然后把这个地址里的数据改为surprise函数的指针。 其实这个地址存的就是main函数调用test函数之后要返回的指令的地址。

无图无真相,下面是我在vs2017中分析的过程:

首先我们在main处设置断点,以便于接下来的调试。

对于main之前的过程这里不做分析。

接下来按f5进入调试,在代码上右键点击选择转到反汇编。
我们就会看到如下汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main()
{
002A17C0 push ebp
002A17C1 mov ebp,esp
002A17C3 sub esp,0C0h
002A17C9 push ebx
002A17CA push esi
002A17CB push edi
002A17CC lea edi,[ebp-0C0h]
002A17D2 mov ecx,30h
002A17D7 mov eax,0CCCCCCCCh
002A17DC rep stos dword ptr es:[edi]
test();
002A17DE call _test (02A1384h)
return 0;
002A17E3 xor eax,eax
}

其中有两个指针寄存器 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的栈底紧挨着main的栈顶, 从而可以在test函数中根据tmp的地址增加4来找到main的返回地址,进而改变程序的运行。

在test的汇编中有这么几句,这几句暂时还不清楚有什么作用, 不过这就是为什么在VS2017中要加4而在VC++6.0中要加2的原因。

mov eax,dword ptr [__security_cookie(02AA000h)]
mov eax,ebp
mov dword ptr [ebp-4],eax

到此,关于这个代码的分析就结束了。

当然,日常编码中我觉得没人会这么干。 不过,这个有趣的代码加深了我对栈帧的理解。