通过gdb调试探寻函数调用时栈的变化

先来一个链接,讲gdb调试讲的很好~ https://deepzz.com/post/gdb-debug.html

这个是接上一篇讲shellcode的,我觉得得先理解了函数调用时栈的变化,才能对其进行进一步的漏洞利用。所以我们废话不多说,开始吧。

首先需要明确两个重要的寄存器:%rsp %rbp
%rsp:指的是当前栈桢的顶部(他可是个调皮的人,总是在变化位置)
%rbp:指的是栈桢的开始

这两个寄存器我们可以这么理解,%rbp相当于我们通过段加基址访问内存时候的的基地址,永远指向一个栈的开始,当我们想要往栈里压数据的时候,%rsp先给咱腾地方,然后咱们通过%rbp找到咱们存放的位置。

我们通过自己写一个简单的例子,放到gdb中调试,来正确的认识一下
代码如下:

1.c

#include<stdio.h>

void call(int a,int b)
{
    printf("%d\n",a+b);
}

int main()
{
   call(10,20);
   int a = 0;
   printf("%d\n",a);
}

我们要放到gdb中调试的话,一定要在gcc编译的时候,加上-g选项,所以,我们接着输入
gcc -g 1.c
生成一个a.out可执行文件。
我们通过gdb ./a.out来进行调试(这里我用了peda插件,下载方法:
git clone https://github.com/longld/peda.git ~/peda
echo “source ~/peda/peda.py” >> ~/.gdbinit
在这里插入图片描述
输入start,程序自动跳转到main函数里的第一条指令

咱们这就来好好分析code那一段到底是怎么回事

   0x555555554676 <main>:	push   rbp
   0x555555554677 <main+1>:	mov    rbp,rsp
   0x55555555467a <main+4>:	sub    rsp,0x10

先明确一下,这里的汇编使用x86书写方式来写的,不是Intel的AT&T写法。这里先将rbp寄存器的值压入栈中,然后将rbp寄存器里的值赋给rsp,然后rsp这个小浪漂就开始自己嗨皮的移动了。这里rsp向下开辟了0x10,这么大内存的栈桢。

我们应该都知道,main函数并不是程序执行时的第一个函数,它是通过_start函数来调用的(详情可见这个链接),所以main函数作为一个调用函数,也给我们展示了调用的过程,我们这里先得出一个小小的结论,然后再到下一个函数调用中,我们来进行验证。

⚠️注意哦,这个已经进入到被调函数里面分析了,实际上,在这之前我们还会保存一些信息,不过我们待会再说

也就是说在函数调用的时候,会开辟一个新的栈桢,在这个栈桢上,我们首先会保存上一栈桢的rbp内的值,然后将rbp栈桢指向这个新栈,并将rbp的地址赋值给rsp,此时rbp桢指针和rsp桢指针同时都指向新的栈桢的起始位置。然后通过rsp的变化来进行保存或弹出一些临时变量。

?️这里有一个我当时不明白的问题,不知道您有没有疑问

为什么这里还要保存旧的栈桢?明明函数在返回时,已经早就先压入了调用函数中被调函数下一条指令的地址啊?

我们不妨先整体来看一个图:

这个图是整体的函数调用的一个栈桢的图。我们不难发现,这其实就是在一个栈上,我的意思也就是,这里不管是父函数的桢还是子函数的桢,总共也就只有一个rsp和rbp栈桢。

于是,当我们函数在返回的时候,我们不仅要得到函数的返回值(保存在 %rax 中),还需要将栈的结构恢复到函数调用之前的状态,也就是rbp和rsp也要恢复到之前的状态。由于你的rbp在新的栈桢中发生了变化,你如果不保存旧的rbp栈桢的地址的话,你就无法恢复到图中所显示的父函数桢的那个状态。

????????????????????????????
一道诱人的分割线~我们好像扯远了,然我们接着回到gdb中来调试吧~

我们在上面程序停到了main函数入口的第一行代码,我们再把那个图贴出来

我们注意,这里我特意没有去定义一个变量,而是直接传值,我们发现code中有这样的几行代码

   0x55555555467e <main+8>:	mov    esi,0x14
   0x555555554683 <main+13>:	mov    edi,0xa
   0x555555554688 <main+18>:	call   0x55555555464a <call>

我们知道0x14是20的十六进制表示,0xa是10的十六进制表示。我们不难发现,在进入函数调用之前,我们保存了函数调用的参数,并且参数的保存是按照从右到左的顺序的。
我们来看这条汇编指令
0x555555554688 <main+18>: call 0x55555555464a <call>
call + 标号的形式,意味着先将call + 标号的下一条语句的IP放入栈中,然后使当前的IP+16位位移

所以这里我们就明白其在压入被调函数参数后,紧接着压入了返回地址

好,下面我们要进入我们的call函数里了,同样和main一样是被调用的函数,我们来验证一下之前我们的结论对不对。

在gdb中单步调试我们用n(next的缩写),但是这个并不进入到函数里面去,为了进入到函数里面去,我们用s 来进行单步调试。

细心的童鞋会发现,这时在code中,左边的已经悄悄地由main 变成了call哦

呀,我们想来验证,却发现它自动为我们跳过去了,不行,我一定要来验证一下

通过上一章图的推算,我们可以推算出栈底在0x55555555464a处,所以我们就用disassemble 指定起始地址来查看一下。

?哈哈,果不其然,开头是我们熟悉的

	push   rbp
    mov    rbp,rsp

证明我们所推断的一切都是对滴~猴嗨森~

到这里,我们就清楚了,在函数调用过程中栈的变化:

先将要传给调用函数的参数压入栈中 ➡️ 然后将被调函数的下一条地址,也就是我们称的返回地址压入栈中 ➡️ 然后就转到我们的调用函数里面了,先将旧的rbp压入栈中,方便后来的恢复,然后将rbp栈桢赋值给rsp,让rsp这个小顽皮的孩子在外面随着栈桢变化而蹦上蹦下,嗯,咱们rbp就静静看着不说话?

参考资料:
https://akaedu.github.io/book/ch10s01.html 这是一个很好的gdb调试c程序的例子,里面也揭示来一些细节问题,值得一看
https://blog.csdn.net/china_video_expert/article/details/7212481 linux下gdb单步调试的方法
https://zhuanlan.zhihu.com/p/27339191 函数调用时栈变化讲得很好的文章

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

五月的天气

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

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

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

打赏作者

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

抵扣说明:

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

余额充值