栈基础以及溢出的最通俗理解~

前言

PWN入门这么久,对栈的概念理解不是特别深刻,容易忘记,有时做题思考时还需要查阅资料回顾。网上栈基础的教程比较多,也比较杂,为了方便个人理解,所以考虑做一篇对栈、栈溢出的相关理解笔记,帮助自己强化下记忆,方便以后回顾~部分内容来自ctf-wiki


栈基础知识

栈是一种LIFO(last in first out)后进先出的数据结构,大学最基本的C语言就有讲过这一点。主要的操作有push(压栈)和pop(出栈)两种操作,这两个指令的直接使用是在底层汇编语言中,直接阅读C、C++这类高级语言是看不到这两个指令的。

在这里插入图片描述

高级语言在运行时都会被转换为汇编程序,在汇编程序运行过程中,充分利用了这一数据结构。每个程序在运行时都有虚拟地址空间,其中某一部分就是该程序对应的栈,用于保存函数调用信息和局部变量。此外,常见的操作也是压栈与出栈。需要注意的是,程序的栈是从进程地址空间的高地址向低地址增长的。——CTF wiki

这段话对栈的基本作用有了一个概括总结,最后一句话“程序的栈是从进程地址空间的高地址向低地址增长的”值得深思。这一点我在最初学习完栈知识后,在脑中总是会混淆高低地址,刻板认为在上面的就是高地址,在下面的就是低地址。实际上如上图栈所示,高地址在下,低地址在上,在完成压栈操作后ESP指针往上移,相应地址-4字节(32位)。

这样理解起来,我们在脑中构建的高低概念似乎就有点反转了,脑海中模拟出的栈操作也比较反人类= =。因此个人倾向于在脑海中构造一个如下图所示的栈印象,高地址在上,低地址在下,压栈操作等于在栈的下方往上压,先进栈的参数为高地址,在栈中也处于高位置,栈的下面为栈顶,上面为栈底(这样才是一个比较公认的理解印象,但比较难受的就是文字描述的上面就变成了下面了…自己悟会一会儿= =)

栈

在后续做题时,也可以发现,IDA反汇编后的可执行程序中的栈,是一个低地址在上的直观体现。

在这里插入图片描述


栈的寄存器

(1)esp:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。

(2)ebp:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。(ebp在当前栈帧内位置固定,故函数中对大部分数据的访问都基于ebp进行)

(3)eip:指令寄存器(extended instruction pointer),其内存放着一个指针,该指针永远指向下一条等待执行的指令地址。 可以说如果控制了EIP寄存器的内容,就控制了进程——我们让eip指向哪里,CPU就会去执行哪里的指令。eip可被jmp、call和ret等指令隐含地改变(事实上它一直都在改变)

函数栈帧的大小并不固定,一般与其对应函数的局部变量多少有关。函数运行过程中,其栈帧大小也是在不停变化的。eax一般用来保存函数的返回值,记住esp是栈顶指针寄存器ebp是栈底指针寄存器

ESP 中的指针将一直指向这个新位置, 所以 ESP 中的地址数据是动态的.

上面这段话是对栈的寄存器一个不错的总结,其中有一个概念之前没有怎么弄懂。

  1. “系统栈最上面一个栈帧的栈顶” 栈帧是什么意思?以我的理解来看,几个栈帧似乎就代表主函数调用了几个函数(包括主函数本身),函数每调用一次其他函数,就要跳转到另外一个函数中,由于跳转要涉及到返回地址等信息的保存,每跳转一次EBP和ESP指针都会变动一次。EBP和ESP之间夹着的应该就是一个栈帧,表示当前调用函数,具体的调用过程后面再分析记录。
  2. “EIP是个特殊寄存器,不能像访问通用寄存器那样访问它,即找不到可用来寻址EIP并对其进行读写的操作码(OpCode)”,这个意思似乎指的是汇编中没有能直接选择EIP为操作对象的指令,例如mov eax,XX,这个指令可以选择eax为操作对象,但EIP是不行的。
  3. “它只能由jmp、call、ret等指令隐含改变”,似乎指的是当使用jmp等指令的时候,EIP的内容就被改变了,ret指令就是把当前栈顶保存的返回值地址 弹到eip中,但我们是看不到他的变化的。

不知理解是否有问题,后续可以更改一下~


函数调用中的栈

函数状态主要涉及三个寄存器 —— esp,ebp,eip。esp 用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。ebp 用来存储当前函数状态的基地址,在函数运行时不变,可以用来索引确定函数参数或局部变量的位置。eip 用来存储即将执行的程序指令的地址,cpu 依照 eip 的存储内容读取指令并执行,eip 随之指向相邻的下一条指令,如此反复,程序就得以连续执行指令。

下面让我们来看看发生函数调用时,栈顶函数状态以及上述寄存器的变化。变化的核心任务是将调用函数(caller)的状态保存起来,同时创建被调用函数(callee)的状态。

(1)首先将被调用函数(callee)的参数按照逆序依次压入栈内。如果被调用函数(callee)不需要参数,则没有这一步骤。这些参数仍会保存在调用函数(caller)的函数状态内,之后压入栈内的数据都会作为被调用函数(callee)的函数状态来保存。
在这里插入图片描述
(2)然后将调用函数(caller)进行调用之后的下一条指令地址作为返回地址压入栈内。这样调用函数(caller)的 eip(指令)信息得以保存。

在这里插入图片描述

(3)再将当前的ebp 寄存器的值(也就是调用函数的基地址)压入栈内,并将 ebp 寄存器的值更新为当前栈顶的地址。这样调用函数(caller)的 ebp(基地址)信息得以保存。同时,ebp 被更新为被调用函数(callee)的基地址。这里我个人的理解是:被调用函数(callee)的基地址中存放着调用函数(caller)的基地址?

此时的esp还是处于栈顶,也就是和ebp一样的位置。

在这里插入图片描述

(4)再之后是将被调用函数(callee)的局部变量等数据压入栈内。

在这里插入图片描述

上面即为函数调用的一个过程。在压栈的过程中,esp 寄存器的值不断减小(对应于栈从内存高地址向低地址生长)。压入栈内的数据包括调用参数、返回地址、调用函数的基地址,以及局部变量,其中调用参数以外的数据共同构成了被调用函数(callee)的状态**(注意被调用函数的状态包括了调用函数的基地址和返回地址)**。在发生调用时,程序还会将被调用函数(callee)的指令地址存到 eip 寄存器内,这样程序就可以依次执行被调用函数的指令了。

看过了函数调用发生时的情况,就不难理解函数调用结束时的变化,实际上在利用栈溢出漏洞的时候,核心就在于函数结束调用时。变化的核心任务是丢弃被调用函数(callee)的状态,并将栈顶恢复为调用函数(caller)的状态。

(1)首先被调用函数的局部变量会从栈内直接弹出,栈顶会指向被调用函数(callee)的基地址。

在这里插入图片描述

(2)然后将callee基地址内存储的调用函数(caller)的基地址从栈内弹出,并存到 ebp 寄存器内。这样调用函数(caller)的 ebp(基地址)信息得以恢复。此时栈顶会指向返回地址。注意!这里就是我们一般栈溢出所攻击的地方,利用溢出漏洞,覆盖掉这里的Return Address,就可以使得原来返回到调用函数的地址被修改为,system/bin/sh地址,获得控制权限。

在这里插入图片描述
(3)再将返回地址从栈内弹出,并存到 eip 寄存器内。这样调用函数(caller)的 eip(指令)信息得以恢复。至此调用函数(caller)的函数状态就全部恢复了,之后就是继续执行调用函数的指令了。

在这里插入图片描述

有一点需要注意的是,X86(32位)和X64(64位)的系统在函数调用时除了一字节所代表的长度不同外,还有着寄存器调用的不同:

  • X86

调用时,函数参数在函数返回地址的上方,使用栈来传递参数,使用 eax 存放返回值。

  • X64

而在X64里,System V AMD64 ABI (Linux、FreeBSD、macOS 等采用)中前六个整型或指针参数依次保存在RDI, RSI, RDX, RCX, R8 和 R9 寄存器中(这个顺序需要记住,后续解决64位程序ROP编写payload时很重要),如果还有更多的参数的话才会保存在栈上

栈溢出攻击原理

在理解完函数调用时栈的变更过程后,我们便不难理解栈溢出的攻击原理了:只有在发生函数调用或者结束函数调用时,程序的控制权会在函数状态之间发生跳转,这时才可以通过修改函数状态来实现攻击。而控制程序执行指令最关键的寄存器就是 eip,所以我们的目标就是让 eip 载入攻击指令的地址,但eip又是不可直接写的,我们该如何改变其内容呢?利用的就是ret指令
在这里插入图片描述

回头看下IDA里面反汇编的栈,可以看到s对应的局部变量,是我们可以利用的溢出点,当其读取长度大于其事先声明的长度且足以覆盖到ret address中时,编写payload,即可覆盖掉程序之前的返回地址,达到栈溢出攻击效果。

在这里插入图片描述


以覆盖到ret address中时,编写payload,即可覆盖掉程序之前的返回地址,达到栈溢出攻击效果。


个人博客地址:Cloudyun

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

C1ovd

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值