栈与栈帧的调试
2022-3-6 17:59:0 Author: mp.weixin.qq.com(查看原文) 阅读量:15 收藏


本文为看雪论坛优秀文章
看雪论坛作者ID:顾忧

栈,通常用于存储局部变量、传递函数参数、保存函数返回地址等。

栈内存在进程中的作用

1、暂时保存函数内的局部变量
2、调用函数时传递参数
3、保存函数返回后的地址

栈的特征

一个进程中,栈顶指针(ESP)初始状态指向栈底端、执行push命令将数据压入栈时,栈顶指针就会上移到栈顶端。执行pop命令从栈中弹出数据时,若栈为空,则栈顶指针就会重新移动到栈底端。

栈是由下往上扩展的,栈顶顶针在初始状态下指向栈底这就是栈的特征

栈操作示例

1、od打开stack.exe文件,从中看到寄存器窗口中栈顶指针ESP的值为0012FFC4,栈窗口中最上面的地址(即栈顶地址)也为0012FFC4,右边为0012FFC4对应的值。

2、在代码窗口按下F7(Step into),自己执行地址00401000处的PUSH 100指令。


可以看到ESP的值减少了四个字节,变成了0012FFC0,并且栈窗口中的栈顶指定也指向了0012FFC0地址,且值也变成100。

再次执行pop EAX,ESP的值增加4个字节,变为0012FFC4。OD状态变成最开始的状态。

栈帧在程序中用于声明局部变量、调用函数。理解栈帧主要用来掌握保存在其中的函数参数与局部变量。

简而言之,栈帧就是利用EBP(栈帧指针,注意不是ESP)寄存器访问栈内局部变量、参数、函数返回地址等的手段。ESP寄存器承担着栈顶指针的作用,而EBP寄存器则负责行驶栈帧指针的职能。

在程序运行中,ESP寄存器的值随时会变化,访问栈中函数的局部变量、参数时,若以ESP的值为基准编写程序会十分困难,并且也很难使CPU引用到准确的地址。

所以,调用某函数时,先要把用作基准点(函数起始地址)的ESP值保存到EBP,并维持在函数内部。这样,无论ESP的值如何变化,以EBP的值为基准能够安全访问到相关函数的局部变量、参数、返回地址。


栈帧对应的汇编代码:

示例:stackframe.cpp源码

在main()函数的起始地址(40120)处下一个断点,然后F9运行程序,程序运行到断点处停止。

在开始调试前,记录一下栈的状态。如下,此时ESP的值为0012FF7C,EBP的值为0012FFC0。


注意,地址00401250保存在ESP(0012FF7C)中,它是main()函数执行完毕后要返回的地址。

main()函数一开始运行就生成与其对应的函数。


PUSH是一条压栈指令,下面这一句的意思是将EBP的值压入栈。
main()函数中,EBP为栈帧指针,用来把EBP之前的值备份到栈中(main()函数执行完毕后,在返回之前,该值会再次恢复)。同时执行,这一条PUSH指令后,ESP的值变成12 FF78(0012FF7C-4)

同时执行这一条PUSH指令后,ESP的值变成12 FF78(0012FF7C-4)。

MOV是一条数据传输指令,这条语句的意思是将ESP的值传送到EBP。也就是说,从这条指令开始,EBP就有了与现在ESP相同的值,并且直到main()函数执行完毕,EBP的值始终保持不变。

换言之,可以通过EBP安全的访问到存储在栈中的函数参数与局部变量。执行完PUSH与MOV两条指令,栈帧就生成了,即EBP被设置好了。

在OD的栈窗口,右击依次选择Address-Relative to EBP。

现在的栈内情况被转换成相对于EBP的偏移后,能更直观地观察到栈内情况。如下,在栈窗口中可以看到EBP的位置。

2、设置局部变量
开始分析源文件中的变量声明与赋值语句。下面的语句用于在栈中为局部变量(a,b)分配空间

执行该指令之后,ESP由原来的12 FF78变成12 FF70。减去这8个字节,是为局部变量(a和b)开辟空间,以便将它们保存在栈中。由于局部变量a与b都是long型(长整型),它们分别占据4个字节,因此需要开辟8个字节的空间。

为函数变量开辟好栈空间后,在main()函数内部,无论ESP的值如何变化,变量a与b的栈空间都不会收到损坏。由于EBP的值在函数内部是固定不变的,所以可以以它为基准来访问函数的局部变量了。

接下来看如下代码,dword ptr ss:[ebp-0x4]可以理解为c语言中的指针。

汇编语言与C语言的指针语句格式:


提示:DWORD PTR SS:[EBP-4]语句中,SS是Stack Segment的缩写,表示栈段。由于Windows中使用的段内存模型,使用时需要指出相关内存属于哪一个区段。起始,32位的Windows OS中,SS、DS、ES的值皆为0,所以采用这种方式附上区段并没有什么意义。

因为EBP与ESP是指向栈的寄存器,所以添加上了SS寄存器。DWORD PTR SS:等字符串可以通过OD的相关选项来隐藏。

再次分析一下刚刚的两条MOV指令,它们的含义是把数据1与数据2分别保存在[EBP-4]与[EBP-8]中,即[EBP-4]代表局部变量a,[EBP-8]代表局部变量b。执行完两条指令后,栈内情况如下图,可以看到其对应的值分别为1,2。

3、add()函数参数传递与调用


代码如下:

请看下面五行汇编代码,它描述了add()函数的整个过程。
地址0040103C处为"CALL 40100" 命令,用于调用40100处的函数,而40100处的函数为add()函数。

函数add()接收a、b两个长整型参数,所以在调用add()之前需要把两个参数压入栈,地址401034—40103B之间的指令就是这个作用。这一过程中需要注意的是,参数入栈的顺利与C语言源码中的参数顺序相反(这种现象被称为函数参数的逆向存储)。

换言之,变量B首先入栈,接着变量a再入栈。执行完401034—40103B的指令后,栈内情况如下:

执行CALL命令进入被调用函数之前,CPU会把函数的返回值压入栈,用做函数执行完毕后的返回地址。

从上图可以看出调用add()函数之后,下一条命令的地址为401041。函数执行完毕后就会跳转到这个地址。这个地址被称为函数的返回地址。执行CALL命令后的栈窗口如下:

接下来执行add()函数的汇编指令。指令如下:

指令先把EBP值(main()函数的基址指针)保存到栈中,再把当前的ESP的值存储到EBP当中。执行完,栈窗口中的情况如下:

4、设置add()函数的局部变量(x,y)

源代码如下:
声明了两个长整型的局部变量(x,y),并用两个形式参数(a,b)分别为它们赋初始值。

执行完下面的指令后,[EBP+8]与[EBP+C]分别执行参数a与b,而[EBP-8]与[EBP-4]分别指向add()函数的两个局部变量x,y。栈窗口如下:

5、ADD运算

源码:

汇编指令:

小知识:
EAX是一种通用寄存器,在算术运算中存储输入输出数据,为函数提供返回值。若向EAX输入某个值,该值就会原封不动地返回。执行过程中,栈内状态不变。


6、删除函数add()的栈帧以及函数执行完成返回


把EBP值赋给ESP,与前文的MOV EBP,ESP指令对应。
把存储到EBP的值恢复到ESP中。


提示:

执行完MOV ESP,EBP之后,地址401003处的指令不在有效。POP EBP用于恢复函数add()开始执行时备份到栈中的EBP值,可以看到ESP的值为0012FF64,对应的值为401041是执行call 401000命令时cpu存储到栈中的返回地址。


EBP恢复为0012FF78,是当初main()函数的EBP值。

执行下面的语句后,存储在栈中的返回地址被调用。

栈中地址如下:此时栈中的地址与返回到调用add()函数之前的状态。

7、从栈中删除函数add()的参数

执行如下命令,将ESP+8。

ESP加8的原因可以从上一张图看出,地址12FF68与12FF6C处存储的是传递给函数add()的参数a与b。函数add()执行后,不需要参数a与b了,所以ESP+8将它们从栈中清理掉。

提示:
1、调用add()函数之前先使用push指令把参数a,b压入栈;
2、被调函数执行完毕后,函数的调用者(Caller)负责清理掉存储在栈中的参数,这种方式称为cdecl方式;
反之,被调用者(Caller)负责清理保存在栈中的参数,这种方式被称为stdcall方式。



这些函数调用规则统称为调用约定。

8、调用printf()函数

汇编指令如下:

地址401044处的 EAX 寄存器中存储着函数 add()的返回值,它是执行加法运算后的结果值3。地址40104A处的 CALL 401067命令中调用的是401067地址处的函数,它是一个 C 标准库函数 printf ,所有 C 标准库函数都由 Visual C ++编写而成(其中包含着数量庞大的函数,在此不详细介绍)。

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


执行如下指令来设置返回值:

常用于寄存器的初始化操作,执行下面两条命令来对栈帧进行删除以及函数终止。


执行后栈内地址如下图:

执行完下面的命令,主函数执行完毕并返回,程序执行跳转到返回地址(401250)。该地址执行visual C++的启动函数区域。
00401056 retn

参考:《逆向工程核心原理》

看雪ID:顾忧

https://bbs.pediy.com/user-home-890008.htm

*本文由看雪论坛 顾忧 原创,转载请注明来自看雪社区

# 往期推荐

1.CVE-2021-31956分析与利用

2.malloc源码分析

3.Windows本地代码执行漏洞(CVE-2012-1876)x86/x64平台分析

4.栈溢出原理与实践之读书笔记

5.符号执行挖掘开源库命令注入

6.CVE-2021-4034 pkexec本地提权漏洞复现与原理分析

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458431878&idx=1&sn=c79b29b646a4139144d877a1ccb3450a&chksm=b18f810c86f8081a216223ae36fbc920f8575b14dcfa4b5e082d27dc07e91ff04b009301dcbe#rd
如有侵权请联系:admin#unsafe.sh