逆向工程核心原理笔记(六)——栈帧

逆向工程核心原理笔记(六)——栈帧

1.栈帧

栈帧是利用 EBP (栈帧指针)寄存器访问栈内的局部变量,参数,函数返回地址等等的手段,通过前面的学习可以了解到,ESP 寄存器承担着栈顶部指针的作用,EBP 寄存器负责行使栈帧指针的职能。在程序运行的时候,ESP 寄存器的值随时变化,访问栈中函数的局部变量,参数的时候,如果按照 ESP 的值为基准编写程序将会十分困难,并且很难使 CPU 引用带准确的地址,所以在调用某个函数的时候,需要先把 ESP 的值作为基准点保存到 EBP 中,并且维持在函数内部,这样无论 ESP 的值如何变化,以 EBP 为基准能安全访问到相关函数的局部变量,参数,返回地址等,这就是 EBP 作为栈帧指针的作用。

先来看看栈帧对应的汇编代码:

PUSH EBP            函数开始(使用EBP前先把已有的值保存到栈中)
MOV EBP, ESP        保存当前ESP到EBP中
                    函数体
...                 无论ESP的值如何变化,EBP保持不变,可以安全访问函数的局部变量和参数等

MOV ESP, EBP        将函数的起始地址返回到ESP中
POP EBP             在函数返回前弹出保存在栈中的EBP值
RETN                函数终止

最新的编译器中都带有一个优化的选项,使用这个选项编译简单的函数将不会生成栈帧

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

3.调试样例程序

我们接下来将尝试调试书中给出的样例程序
在这里插入图片描述
我们直接将程序跳转到 00401000 地址处
在这里插入图片描述
首先我们先从主函数开始分析程序,主函数 main() 是程序开始的地方,在 main() 函数的起始地址(401020)处,在这里设置一个断点后将程序运行到断点处。

接下来我们需要密切关注栈的变化。
在这里插入图片描述
当前 ESP 和 EBP 的值分别为 0019FF2C 和 0019FF70
在这里插入图片描述
现在我们将地址 401250 保存在了 ESP(12FF44) 中,这里将是主函数执行完后要返回的地址,主函数一开始运行就生成与其对应的函数栈帧。

接下来我们逐行分析代码:

00401020 PUSH EBP

PUSH指令是一条压栈指令,上面的 PUSH 语句是将 EBP 的值压入栈中,在主函数中,EBP 为栈帧指针,用来把 EBP 之前的值备份到栈中。

00401021 MOV EBP, ESP

MOV 是一条数据传送命令,上面语句的意思是将 ESP 的值传送到 EBP 中,换言之,从这条命令开始,EBP 和 ESP 拥有相同的值,直到主函数执行完毕,EBP 的值始终保持不变,也就是说,我们通过 EBP 可以安全访问到存储在栈中的函数参数和局部变量,执行完如上的两条命令后,主函数的栈帧就被生成了即设置好了 EBP

接下来我们进入 OD 的栈窗口,在右键的菜单中选择 Address -> Relative to EBP
在这里插入图片描述
我们找到栈窗口中的 EBP 的位置,当前的 EBP 的值在执行了上面的两步之后值也为 0019FF28
在这里插入图片描述
此时的 ESP 和 EBP 的值一样,在 0019FF28 的地址处保存着 0019FF70,这是主函数开始执行时 EBP 的初始值。

接下来我们分析源文件中的变量声明和赋值语句:

00401023 SUB ESP, 8

SUB 指令是汇编中一条减法指令,上面个的语句用来将 ESP 的值减去8个字节,执行这条语句之前,ESP 的值为 0019FF28 ,执行后变为 0019FF20,从 ESP 中减去八个字节实际上是为了两个变量开辟新的空间,以便于将这两个变量保存在栈中,由于两个 int 类型的变量占据4个字节的大小,因此在栈中开辟8个字节的空间来保存这两个变量的值。

使用 SUB 指令从 ESP 中减去8个字节,为两个函数变量开辟好栈空间后,在主函数内部,无论 ESP 的值如何变化,变量 a 和 b 的栈空间都不会被损坏,由于 EBP 的值在主函数内部是固定不变的,我们就能使用此为基准访问函数的局部变量了。

接下来我们接着往后看:

00401026 MOV [local.1], 1 ; MOV DWORD PTR [EBP-4], 1
0040102D MOV [local.2], 2 ; MOV DWORD PTR [EBP-8], 2

上面的指令类似于 C 语言中的指针

上面的两条 MOV 指令含义是将数据1和2分别保存到[EBP-4]和[EBP-8]中,即[EBP-4]代表局部变量a,[EBP-8]代表局部变量b,执行完上述的语句后,函数的栈内的情况如下所示:
在这里插入图片描述
接下来主函数调用了 add() 函数进行求和操作,汇编代码如下:
在这里插入图片描述
上面的五行代码描述了调用 add() 函数的整个过程,在 0040103C 处调用了 401000 处的函数,而 401000处的函数就是 add 的函数,由于函数 add 需要 a,b 两个参数,所以在调用这个函数的时候需要先将这两个参数压入栈中,即 00401034 - 0040103B 之间的代码执行了这样的操作。由于参数入栈的顺序和调用参数的顺序恰好相反因此我们要先b入栈再a入栈。将上面四行代码执行完后,栈内的情况如何所示:
在这里插入图片描述
接下来我们将进入 add 函数的内部,分析整个函数的调用过程。
在这里插入图片描述
add函数开始执行的时候,栈中会单独生成与其对应的栈帧,上面的两行代码和执行主函数时的代码万千相同,先把 EBP 的值保存在栈中,再把当前的 ESP 保存到 EBP 中,这样函数 add 的栈帧就生成了,如此,函数 add 的内部的 EBP 的值始终保持不变,执行完上述的代码后,栈中情况如下所示:
在这里插入图片描述
可以看到 EBP 的值被备份到栈中,然后 EBP 的值被设置为一个新的值。

接下来,我们源代码中存在如下一句代码:

int x = a, y = b;

上面得代码顶一个两个 int 类型的变量,并且用两个形式参数为他们赋初值,于是我们选在需要开辟八个字节的空间来开辟这两个局部变量,于是 00401003 处的代码如下:

00401003 SUB ESP, 8

接下来:

00401006 MOV EAX, [arg.1]   ; MOV EAX, [EBP + 8]
00401009 MOV [local.2], EAX ; MOV [EBP - 8], EAX
0040100C MOV ECX, [arg.2]   ; MOV ECX, [EBP + C]
0040100C MOV [local.1], ECX ; MOV [EBP - 4], ECX

add 函数的栈帧生成之后,EBP 的值发生了变化, [EBP + 8]和[EBP + C]分别指向了参数 a 和 b,执行完上述语句后栈内的情况如下:
在这里插入图片描述

00401012 MOV EAX, [lacal.2] ; MOV EAX, [WBP - 8]

上面的语句中,x的值被赋值给EAX

00401015 ADD EAX, [lacal.1] ; ADD EAX, [EBP - 4]

上面的语句中 ADD 为加法指令,将变量 y 的值和原 EAX 的值相加,并且将运算结果储存在 EAX 中。EAX 寄存器是一种通用寄存器,在算数的运算中存储输入输出的数据,为函数提供返回值,当函数即将返回的时候,如果想 EAX 中输入某个值,那么这个值将原封不动的返回,并且在执行运算的时候栈内的情况保持不变。

在执行完加法运算的时候,要返回函数 add ,在此之前需要先删除函数 add 的栈帧。

00401018 MOV ESP, EBP

上面的命令将 EBP 的值赋值给 ESP ,与地址 00401001 处的命令相对应。函数执行完毕后,将存储的 EBP 的值恢复到 ESP 中。

0040101A POP EBP

上面的命令用于回复函数 add 开始执行时备份到栈中的 EBP 的值,此处的语句和 00401000 处的 PUSH EBP 语句对应,EBP 的值恢复,成为主函数的 EBP 值,至此,add 函数中的栈帧被删除完毕。

进行完上述步骤,栈中的情况如下所示。
在这里插入图片描述

0040101B RETN

执行上述的 RETN 命令后,存储在栈中的地址会被返回。执行完之后,栈内的情况如下:
在这里插入图片描述
初始栈中的情况已经完全返回到调用 add 函数之前的情况。

程序通过上面的方式管理栈,不论有多少函数进行嵌套调用,栈都能得到比较好的维护,不会发生崩溃。但是由于函数的局部变量,参数,返回地址一次性保存在栈中,利用字符串函数的漏洞柔伊引起栈缓冲区的溢出,最终呆滞程序或者系统崩溃。

00401041 ADD ESP, 8

上面的语句将 ESP 加 8,由于上面在地址 0019FF18 和 0019FF1C 处存储的是给函数 add 传递的参数 a 和 b,函数执行完毕返回后不再需要这两个参数,于是将 ESP 加8,将这两个参数从栈中清除,执行完上面的语句后,栈中的情况如下所示:
在这里插入图片描述

被调用函数执行完毕后,函数的调用者(Caller)负责清理存储在栈中的函数,这种函数被称为 cdecl 方式,反之,被调用者(Callee)负责清理保存在栈中的参数,这种方式被称之为 stdcall 凡是,这些函数的调用规则统称为调用约定(Calling Convention)

接下来程序将调用 printf 函数将结果进行输出:

00401044 PUSH EAX
00401045 PUSH 40B384
0040104A CALL 00401067
0040104F ADD ESP, 8

在地址 401044 处的 EAX 寄存器种存储着 add 的返回值,即计算结果 3 ,在地址 0040104A 处的 CALL 命令调用了 00401067 处的函数,由于 printf 函数中需要两个参数,大小为 8 个字节(32位寄存器 + 32位常量 = 64 位 = 8 字节),因此在 0040104F 处使用 ADD 命令,将 ESP 加上 8 个字节,把函数的参数从栈中删除。

接下来程序进行:

return 0;

主函数通过这个语句设置返回值 0.

00401052 XOR EAX, EAX

XOR 命令用来进行异或运算,特点位两个相同的值进行 异或运算,结果为 0.异或命令比 MOV EAX, 0 命令的执行速度更快,通常用于寄存器的初始化操作。

利用相同的值连续执行两次以后运算即可变为原值,这个特征被大量运用于编码和解码。

接下来程序将执行删除栈帧操作和将主函数终止。

主函数终止运行,和 add 函数一样,返回前将先从栈中删除和它对应的栈帧。

00401054 MOV ESP, EBP
00401056 POP EBP

执行完上述的两条命令后,主函数的栈帧就被删除了,并且局部变量不再有效。

接下来执行:

00401057 RETN

续执行两次以后运算即可变为原值,这个特征被大量运用于编码和解码。

接下来程序将执行删除栈帧操作和将主函数终止。

主函数终止运行,和 add 函数一样,返回前将先从栈中删除和它对应的栈帧。

00401054 MOV ESP, EBP
00401056 POP EBP

执行完上述的两条命令后,主函数的栈帧就被删除了,并且局部变量不再有效。

接下来执行:

00401057 RETN

执行上述命令后,主函数执行完毕并返回。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值