07.栈帧

1. 栈帧

作用: 栈帧在程序中用于声明局部变量、调用函数。
栈帧就是利用EBP寄存器访问栈内局部变量、参数、函数返回地址等的手段
与ESP寄存器不同:ESP承担栈顶指针的作用。而EBP寄存器则负责行驶栈帧指针的职能

程序运行中,ESP寄存器的值随时变化,访问栈中函数的局部变量、参数时,若以ESP值为基准编写程序会十分困难,并且也很难使CPU引用到准确的地址。
所以,调用某函数时,先要把用作基准点(函数起始地址)的ESP值保存到EBP,并维持在函数内部
这样,无论ESP的值如何变化,以EBP的值为基准(base)能够安全访问到相关函数的局部变量、参数、返回地址,这就是EBP寄存器作为栈帧指针的作用。

●最新的编译器中都带有一个“优化”(Optimization)选项,使用该选项编译简单的函数将不会生成栈帧。

●在栈中保存函数返回地址是系统安全隐患之一,攻击者使用缓冲区溢出技术能够把保存在栈内存的返回地址更改为其他地址。

2. 调试示例:stackframe.exe

源代码

#include "stdio.h"

long add(long a, long b)
{
    long x = a, y = b;
    return (x + y);
}

int main(int argc, char* argv[])
{
    long a = 1, b = 2;
    
    printf("%d\n", add(a, b));

    return 0;
}

调试器加载程序 并goto到 401000处
Pasted image 20250311213850

2.1. 开始执行main()函数&生成栈帧

int main(int argc, char* argv[]){}

函数 main 是程序开始执行的地方,在 main 函数的起始地址(401020)处,按F2键设置个断点,然后按F9运行程序,程序运行到 main 函数的断点处暂停。

然后观察栈的变化
Pasted image 20250311214234
Pasted image 20250311214321
当前ESP的值为 19ff2cEBP的值为 19ff70。切记地址 401250 保存在ESP(19ff2c)中,它是 main 函数执行完毕后要返回的地址。

观察代码可以发现 main 函数 一开始运行就会生成与其对应函数的栈帧
Pasted image 20250311214836
这里 PUSH EBP 就是把EBP的值压入栈。 main 函数中 EBP为栈帧指针,用来把EBP之前的值备份到栈中。(main 函数执行完毕,返回之前,该值会再次恢复)

下一条命令 MOV EBP,ESP 就是把ESP的值 传递给EBP。换言之,从这条命令开始,EBP就有和ESP相同的值,直到 main 函数执行完毕。EBP的值始终不变。 也就是说,我们通过EBP就可以安全访问到存储在栈中的函数参数与局部变量了

执行完 PUSH EBPMOV EBP,ESP 后,函数 main 的栈帧就生成了(设置好了EBP)

然后在栈窗口 选择 地址->相对EBP
Pasted image 20250311215704
然后调试到 mov ebp,esp
Pasted image 20250311215929
可以发现当前EBP值与ESP值相同。 EBP处的数值是 19FF70 他是 main 函数开始执行时EBP持有的初始值。

2.2. 设置局部变量

分析源代码中的变量声明与赋值语句

long a = 1, b = 2;

main 函数中,上述代码用于在栈中为局部变量 a b分配空间,并赋初始值

如何分配空间?
Pasted image 20250311220417
SUB ESP,0x8 这里就是把ESP减去8个字节(因为a b是两个长整形,每个占用4个字节)开辟空间,以便将它们保存在栈中。

当为这两个变量开辟好空间后,在 main 函数内部,无论ESP的值怎么变化。变量a b的栈空间都不会受影响。由于EBP的值在 main 函数内部是固定不变的。所以,我们可以通过以它为基准进行访问函数的局部变量了。

通过指令

mov     dword ptr ss:[ebp-0x4],0x1
mov     dword ptr ss:[ebp-0x8],0x2

来设置值。
在界面中可能会显示
Pasted image 20250311221519
那是因为反汇编工具会将 ebp 作为基址,并对局部变量进行标注,以提高可读性
其中 local.1 表示一个局部变量。这里就是 a 变量

DWORDPTRSS:[EBP-4]语句中,SS是StackSegment的缩写,表示栈段。由于Windows中使用的是段内存模型(Segment MemoryModel),使用时需要指出相关内存属于哪一个区段。其实,32位的WindowsOS中,SS、DS、ES的值皆为0,所以采用这种方式附上区段并没有什么意义。因EBP与ESP是指向栈的寄存器,所以添加上了SS寄存器。请注意,“DWORDPTR”与“SS:”等字符串可以通过设置OllyDbg的相应选项来隐藏。

执行完这两天命令后,查看栈内的情况可以发现已经将变量的值存储到栈内了
Pasted image 20250311221955

2.3. add函数参数传递与调用

源代码中调用了add函数 并且打印了返回值

printf("%d\n", add(a, b));

观察这5行代码,它表示了调用add()函数的全过程。
Pasted image 20250311222335
401000 处的函数就是 add 函数,函数 add 接受 a b 这两个长整型参数。所以调用 add 函数之前 需要把这两个参数压入栈中。

值得注意的是:这里入栈的顺序与 add 函数的参数顺序正好相反。 这就是参数的逆向存储
即先入栈b 再入栈a

然后进入add函数内部进行分析
Pasted image 20250311222807
在执行函数之前,CPU会先把函数的返回地址入栈,用作函数执行完毕后的返回地址。

观察代码可以发现函数 add40103c 处被调用。他的下一条命令地址是 401041 这就是 add 函数的返回地址。
Pasted image 20250311223208
可以发现此时CPU也把函数返回地址压入栈中了
Pasted image 20250311223307

2.4. 开始执行add函数&生成栈帧

add 函数前两行

long add(long a, long b)
{

函数开始执行时,栈中会单独生成与其对应的栈帧
Pasted image 20250311223507

执行后就会把原来EBP的值备份到栈中,然后把ESP的值传递给EBP

2.5. 设置add函数的局部变量(x,y)

源代码

long x=a, y=b

这里声明了两个长整型变量 x y并用a b 赋值给他们

首先开辟空间
SUB EBP,0x8
然后赋值
Pasted image 20250311224338
执行后的栈内情况
Pasted image 20250311224445

2.6. add运算

源代码

return (x+y)

用于返回两个局部变量之和
Pasted image 20250311224657
执行后eax的值就是3
Pasted image 20250311224724

2.7. 删除函数add的栈帧&函数执行完毕(返回)

对应原代码

return (x+y);
}

执行完加法运算后,要返回函数 add 在此之前,需要先删除 add 函数的栈帧
mov esp,ebp
这里与开始生成栈帧的命令 mov ebp,esp 向对应

生成栈帧时: 把函数开始执行时的ESP值放入EBP
删除栈帧时:把放入EBP中的ESP值还给ESP

执行完上面的命令后,地址 401003 处的 SUB ESP,0x8 就会失效,即函数 add 的两个局部变量x,y不在有效

然后 pop ebp 恢复函数 ADD 开始执行时备份到栈中的EBP值。它与 401000 处的 PUSH EBP 命令对应。 EBP值恢复为 19FF28 它是 main 函数的EBP值,到此 add 函数的栈帧就被删除了
Pasted image 20250311225917
可以看到此时ESP的值为 19ff14 此地址的值是 401041 它是执行 call 401000 命令时CPU存储到栈中的返回地址

然后执行retn命令后。存储在栈中的返回地址即被返回, 此时调用栈也已经完全返回到被调用到 add 函数之前的状态
Pasted image 20250311230223

2.8. 从栈中删除函数add的参数(整理栈)

现在,函数执行流已经返回到 main 函数中
然后执行 ADD esp,0x8 即回收空间。不需要a b参数了
这里与前面开辟空间的 SUB esp,0x8 相对应

被调函数执行完毕后,函数的调用者(Caller) 负责清理存储在栈中的参数,这种方式称为cdecl方式;
反之,被调用者(Callee) 负责清理保存在栈中的参数,这种方式称为stdcall方式。
这些函数调用规则统称为调用约定(CallingConvention),这在程序开发与分析中是一个非常重要的概念

2.9. 调用printf函数

对应源代码

printf("%d\n",add(a,b));

Pasted image 20250311231113

由于上面的printf函数有2个参数,大小为8个字节(32位寄存器+32位常量=64位=8字节),
所以在 40104F 地址处使用 ADD 命令,将ESP加上8个字节,把函数的参数从栈中删除。函数 printf 执行完毕并通过 ADD 命令删除参数后 栈内情况
Pasted image 20250311231350

2.10. 设置返回值

对应源代码

return 0;

main 函数使用该语句设置返回值0
汇编代码 xor eax,eax 异或清0 且比 MOV eax,0 更快

2.11. 删除栈中&main函数终止

对应源代码

return 0;
}

主函数终止,同 add 函数一样,需要先从栈中删除其对应的栈帧
对应汇编代码
MOV ESP,EBP
POP EBP
执行后,main 函数栈帧被删除。其局部变量,a b 也不再生效。
Pasted image 20250311231845
此时与 main 函数开始的栈内情形是一样的

然后执行 retn 程序流跳转到返回地址 401250 处,该地址指向Visual C++的启动函数区域。随后执行进程终止代码。

3. 设置OllyDbg选项

3.1. 反汇编选项

Alt+O
Pasted image 20250311232239

3.2. 分析1选项

关闭后就不会显示 Local.1 这种形式了
Pasted image 20250311232502