[PwnCourse:从0开始的PWN教程] 0-1-3 Stack & Calling convention

2026-04-11 14:311阅读0评论SEO教程
  • 内容介绍
  • 文章标签
  • 相关推荐
问题描述:

佬友们别来无恙,今天是栈相关知识的讲解。本小节结束之后我们就要正式进入栈利用的学习啦。真是,绕了好大一圈呢

Stack

在0-1-2中,我们简单了解了用户虚拟内存空间的各个区段,其中也提到了。我们摘取Wiki上的描述的话,是这样的

堆栈(stack)又称为栈或堆叠,是计算机科学中的一种抽象资料类型,只允许在有序的线性资料集合的一端(称为堆栈顶端,top)进行加入数据(push)和移除数据(pop)的运算。因而按照后进先出(LIFO, Last In First Out)的原理运作,堆栈常用一维数组或链接串列来实现。常与另一种有序的线性资料集合队列相提并论。

实际上,广义上的栈就是一种数据结构。这里我们所研究的栈是x86的计算机系统内存空间中用来储存函数的参数,返回值,返回地址,局部变量等数据的一个区段,这个区段整体是由广义上的栈所结构。

所以,狭义上的栈,同样是一种后进先出LIFO(Last in First out)的数据结构。这种形式也正好满足我们一般函数调用的方式,父函数调用子函数,子函数执行完毕后返回父函数,返回时,子函数在前,父函数在后。

image3840×2761 247 KB

栈只能从一端进行数据的增删,这一端称为栈顶;而栈尾部则称为栈底。需要注意的是,栈底一般是作为一个固定的地址,是不变的。而栈顶是随时变化的。栈支持两种数据操作,压栈push弹栈pop。上图很直观地展现了push和pop两种基本操作

Pasted image 20260324171855775×874 56.7 KB

之前在0-1-2中的这张图很好的展现了栈在内存中的特性,从高地址向低地址生长。但是在实际的调试过程中,我们习惯性会将栈底放在下面,栈顶放在上面,向上是低地址,向下是高地址。

在之前我们介绍过两个比较关键的寄存器rbprsp。它们的名字其实很好理解,一个是base pointer基底指针,所以rbp指向栈底;一个是stack pointer栈指针,所以rsp指向栈顶。

image1042×488 17 KB

此时我们push一个0xcafebabe进去,就是这样

image1167×645 20.7 KB

实际上,就是bp作为基指针不变,rsp随着pop和push不断变化。在64位,一个内存单元是8字节,32位是4字节。rsp和rbp会以8字节为单位变化,也就是说,rsp和rbp的末尾永远是8或者0. 这其实就是8字节对齐。如果末字节以0xX0结尾,就是16字节对齐

这里我们把原先的0xcafebabepop出去的话,是这样

image1024×595 19.8 KB

实际上,pop只是改变了rsp的指向,并没有对栈内的数据进行修改,只是我们不再能通过rsp直接访问到0xcafebabe了。但是数据仍然留存在栈上,我们可以通过其他方式访问。现在我们知道,rsp实际上是变化的指针,如果只是取用栈顶的数据还好,我们可以直接pop出来,但是如果我们需要取用栈中保存的地址呢?如果还是用rsp来取用的话,rsp会与栈中的数据发生相对位置的变化,不可能一个一个pop出来吧?所以我们很容易想到用rbp来进行寻址,因为在一个函数的执行过程,rbp的值是不变的,所以我们可以通过rbp加上偏移,即[rbp + offset]的方式来访问栈中的数据

Function Calling Convention

在先前我们说,函数的调用和返回是通过栈实现的,具体的实现方法就被称作函数调用约定。简单来说,函数调用约定规定了一个函数如何储存它的局部变量,如何从它本身返回到父函数。在详细讲解C语言的函数调用约定时,我们先来了解一下栈帧Stack frame作为关键的概念如何发挥作用。

Stack Frame

程序的执行可以看作连续的函数调用,当一个函数调用后被执行完毕,需要返回原来的函数继续执行。所以函数的调用通常是嵌套的,栈中时常会存在不同函数的不同数据,如何保证不发生数据泄露就是一个很关键的问题。于是,我们给每个函数分配一个独立的连续栈空间,称作栈帧

image820×547 75.4 KB

栈帧的边界通常由bpsp寄存器界定。在64位中,寄存器rsp指向当前函数的栈顶,寄存器rbp指向当前函数的栈底。局部变量在栈中存储,通过rbp进行寻址。

值得注意的是

  • 64位中,前六个参数由寄存器传递,从左到右分别是rdi, rsi, rdx, rcx, r8, r9。剩余的参数在栈上传递
  • 32位中,所有参数都在栈上传递。

Experiment - Pwndbg调试经典C程序

光讲解可能有点无趣,我们由一个实验来具体地学习C语言的函数调用约定。

编译以下源码

#include <stdio.h> int xor(int x, int y) { return x ^ y; } int main() { int a = 0xdead; int b = 0xbeef; int c; c = xor(a, b); printf("%p", c); return 0; }

64位和32位都值得学习,我们两种情况都进行讲解。先来简单解释一下这个经典程序。

  • xor函数具有两个参数,int型的x和y,将x和y异或之后返回一个int
  • main函数定义了三个int型的局部变量,初始化a,b,声明c;随后main调用xor函数,传入a,b。xor将a,b异或之后返回一个intmain将返回值存储到c中,随后以地址的形式打印c,结束返回0。

32 Bits

接下来我们先学习32位的情况,用以下命令编译

gcc -m32 exper0131.c -o b32

-m32表示使用32位编译。

使用gdb启动程序

gdb b32

为了完整调试完毕整个程序,我们从程序起点开始。

pwndbg> starti

image1920×901 299 KB

可以看到大部分寄存器都是0,这就是程序的起点。你可能发现了这里好像并不是main函数。没错,程序确实不是从main函数开始运行的,而是从start开始的,在进行一系列初始化工作后,才会调用main函数。所以实际上你会看到main会返回一个值,这就是原因。

我们跳过这部分复杂的初始化,直接定位到main。首先可以通过p(print)指令打印main的地址。

pwndbg> p main $1 = {<text variable, no debug info>} 0x56556192 <main>

拿到main的地址了,我们下个断点,使用b(breakpoint)指令

pwndbg> b *0x56556192

这里也可以直接b main

可以通过i b查看自己下过的断点。

然后我们需要让程序跑过去,其他的部分可以略过,所以直接使用c(continue)命令。

这里可不能用r命令,因为r命令会新开一个进程,而c命令会在当前的进程上继续执行。

image1920×919 363 KB

首先是函数序言。

  • lea ecx, [esp + 4]这条指令之前说过,是取有效地址,简单理解就是取指针。esp + 4就是0xffffd340[]在汇编语言中表示进行一次解引用,所以[esp + 4]就是栈上的1,而 lea是取有效地址,那么就是把0xffffd340赋给了ecx

我们通过ni执行一下,可以发现确实如我们所说。

  • and esp, 0xfffffff0这条指令将esp进行与运算,实际上是将esp低四位清零,所以0xffffd33c就会变成0xffffd330。由于esp是栈指针,所以对esp的操作会导致栈出现变化。

image1571×742 289 KB

执行之后发现栈抬升了一段距离。注意我们之前说的栈从高地址向低地址生长。这里pwndbg的栈是向下升高排序,所以esp的值减少是抬栈,增加就是降栈。那么这里为什么要进行抬栈呢?不知道你注意到我们抬栈之前和抬栈之后esp的变化没有,没错,esp的低四位清零意味着我们的栈16字节对齐了。这样做通常是为了满足优化和编译器需求,对齐后的运算会相对简单。

  • push dword ptr [ecx - 4]这条指令将ecx - 4的地址解引用后作为一个指针push到了栈上。这里就是将main返回地址保存到栈上了。这是因为栈对齐之后,栈顶发生了变化,原先的返回地址就失效了,所以需要重新保存一下返回地址。至于返回地址是什么,这里先按下不表,我们继续调试。

image1565×1081 404 KB

来到push ebp,为了便于大家理解,我们这里通过画图+调试的方法进行讲解。首先是一开始的时候,mainlibc_start_main调用,刚进入main函数,结束libc_start_main的栈帧

image410×993 3.82 KB

push ebp,这里的ebp还是之前_libc_start_main的栈帧的ebp。也就是说,这步保存了上一个函数的栈底

image430×277 2.93 KB

对应到gdb中

image1036×736 235 KB

mov ebp, esp,将esp的值赋给ebp,由于二者都是指针,所以这步实际上是将ebp移动到和esp相同的位置。

image763×148 4.19 KB

对应到gdb中

image1054×260 110 KB

现在二者的值都是0xffffd328。什么意思呢?实际上,现在栈帧已经开辟完成了,main有了新的栈底0xffffd328ebp也指向新的栈底。

push ecx保存ecx的值,esp上移。

image735×175 4.47 KB

随后,main需要一片栈空间用于保存局部变量,所以sub esp, 0x10抬栈。

image712×382 5.74 KB

此时的栈空间

image818×240 75.5 KB

这时call了一个神秘函数__x86.get_pc_thunk.bx。这个函数是用来ip寻址的,可以暂时不用管,我们专注调试程序内容。

来到mov dword ptr [ebp - 0x14], 0xdead

image1920×936 361 KB

此时函数开始保存局部变量0xdead, 0xbeef。可以看到现在都是通过[ebp + offset]进行寻址,可以通过Stack栏直观了解到程序在哪里保存了什么变量。

image800×250 79.6 KB

现在初始化完成局部变量后,马上就要调用xor了,此时是重点。

image1758×390 156 KB

程序连续push了两个值到栈上。现在的栈就变成了

image704×496 18.3 KB

你可能会疑惑,一模一样的数据为什么要两份?我们先暂且按下不表,继续调试。现在两个整数已经被作为参数保存在栈上,我们马上要调用xor函数,一个新的问题出现了:在执行完xor函数后,怎么返回main函数呢?

我们知道程序的执行流由eip寄存器控制,它总是保存当前指令的下一条指令的地址。所以我们要从xor返回到main,首要的任务就是恢复eip寄存器,要恢复的前提就是需要拥有备份,那我们在调用xor之前,先将当前eip的值push到栈上不就好了吗!这样,调用xor的指令的下一条指令的位置就被我们保存在了栈上,后续我们只需要把这个地址放到eip中,不就能自然地执行下去了吗。

所以,call xor,实际上做了两件事

  • push eip保存eip的值,作为xor返回地址xor总会无条件地将这里的值作为main返回
  • jmp xor跳转到xor函数中,这一步修改了eip寄存器

然后我们si步入xor进行调试

image1628×1261 417 KB

来到xor开始。我们还有一个问题亟需解决:执行流的问题是解决了,但是上下文呢?我们还需要恢复函数的栈帧啊。恢复栈帧的关键是什么?就是bp。只要我们恢复了bp,就恢复了原先函数的栈帧。由于bp在函数调用的过程中不会变化,只有ip会变,所以我们可以在函数序言执行这个工作:push ebp

image754×620 22.5 KB

然后开辟xor的栈帧:mov ebp, esp

image755×565 10.6 KB

然后开始寻址,找到刚刚push到栈上的两个参数。通过eax进行xor。得到的0x6042作为返回值刚好存储在eax中。eax作为返回值寄存器进行返回。

image1439×493 186 KB

关键点来了,此时我们pop ebp还原main函数的栈帧。

image749×564 10.2 KB

然后,返回到mainret。这个指令实际上就是pop eip

image789×559 10.5 KB

此时我们返回到main,栈帧也改回了main的栈帧。现在的add esp, 8处理残余的参数。

image783×587 10.7 KB

这时我们就知道为啥要备份一份了,因为一开始我们的栈帧就只开辟了0x10

随后就是printf打印了,具体的佬友们可以自行调试一下,这里就略过了。

image1328×658 194 KB

直接到这里,销毁栈帧

image803×591 10.6 KB

然后返回。

image1328×658 194 KB

至此,32位调试结束。我们马不停蹄进行64位的调试。

64 Bits

64位的调试我们就只看gdb了。

image1920×1062 332 KB

可以看到相比于32位,多了很多寄存器。基础寄存器的名字也发生了改变。实际上大多数寄存器就当作变量来理解就行。不要畏惧。我们定位到main

image1788×1325 564 KB

地址变长了,但是看起来反而更工整。首先是函数序言,保存上一个函数的bp,然后开辟栈帧。这里就少了栈对齐的步骤,因为64位本身地址低四位不是0就是8. 实际上不需要对齐。

image886×230 104 KB

然后是保存局部变量,没什么变化,也是用[rbp + offset]进行寻址。

image1219×230 115 KB

从这里开始就有变化了,由于64位多了很多寄存器,而且单位也变大了,所以用速度更快的寄存器进行传参是有必要的。首先,程序先将栈上的变量保存到edx, eax寄存器中,这里就纯粹的当变量用。需要注意的是这里使用的是edx而非rdx为什么?实际上,rdx往往会比使用edx的指令使用更多的字节数。在64位中存在零拓展机制,即当高位为0时,会自动当成rdx类型来处理。

随后mov esi, edx, mov edi, eax就是进行传参了。

image1538×920 333 KB

这里的call和32位并无本质区别,就略过了,我们直接步入xor

image1538×920 333 KB

依旧函数序言,然后这里将寄存器中的参数保存到了栈上,进行运算后使用rax存储返回值进行返回。

回到main

image1553×1070 444 KB

打印之后返回,这里就有区别了

image1046×115 31.6 KB

说一下这个leave。我们看一下leave之前的栈帧

image1228×232 119 KB

leave之后

image1285×239 129 KB

没错,leave实际上干了两件事。

  • mov rsp, rbp:将rsp移动到rbp上,这一步是销毁栈帧
  • pop rbp:还原上一个函数的bp

然后返回。调试结束。


Summries

本节内容比较多,但是一定要认真看完,这是后续我们进行漏洞利用中最基础最重要的内容。总结一下,其实就是程序在系统中是如何被调用的,如何返回,如何储存变量,如何寻址等。希望本节的实验,各位感兴趣的佬友能亲自调试。

Have Fun!我们下个帖子再见!

网友解答:
--【壹】--:

前排支持!


--【贰】--:

很强的佬啊,更新速度比我学的速度快

问题描述:

佬友们别来无恙,今天是栈相关知识的讲解。本小节结束之后我们就要正式进入栈利用的学习啦。真是,绕了好大一圈呢

Stack

在0-1-2中,我们简单了解了用户虚拟内存空间的各个区段,其中也提到了。我们摘取Wiki上的描述的话,是这样的

堆栈(stack)又称为栈或堆叠,是计算机科学中的一种抽象资料类型,只允许在有序的线性资料集合的一端(称为堆栈顶端,top)进行加入数据(push)和移除数据(pop)的运算。因而按照后进先出(LIFO, Last In First Out)的原理运作,堆栈常用一维数组或链接串列来实现。常与另一种有序的线性资料集合队列相提并论。

实际上,广义上的栈就是一种数据结构。这里我们所研究的栈是x86的计算机系统内存空间中用来储存函数的参数,返回值,返回地址,局部变量等数据的一个区段,这个区段整体是由广义上的栈所结构。

所以,狭义上的栈,同样是一种后进先出LIFO(Last in First out)的数据结构。这种形式也正好满足我们一般函数调用的方式,父函数调用子函数,子函数执行完毕后返回父函数,返回时,子函数在前,父函数在后。

image3840×2761 247 KB

栈只能从一端进行数据的增删,这一端称为栈顶;而栈尾部则称为栈底。需要注意的是,栈底一般是作为一个固定的地址,是不变的。而栈顶是随时变化的。栈支持两种数据操作,压栈push弹栈pop。上图很直观地展现了push和pop两种基本操作

Pasted image 20260324171855775×874 56.7 KB

之前在0-1-2中的这张图很好的展现了栈在内存中的特性,从高地址向低地址生长。但是在实际的调试过程中,我们习惯性会将栈底放在下面,栈顶放在上面,向上是低地址,向下是高地址。

在之前我们介绍过两个比较关键的寄存器rbprsp。它们的名字其实很好理解,一个是base pointer基底指针,所以rbp指向栈底;一个是stack pointer栈指针,所以rsp指向栈顶。

image1042×488 17 KB

此时我们push一个0xcafebabe进去,就是这样

image1167×645 20.7 KB

实际上,就是bp作为基指针不变,rsp随着pop和push不断变化。在64位,一个内存单元是8字节,32位是4字节。rsp和rbp会以8字节为单位变化,也就是说,rsp和rbp的末尾永远是8或者0. 这其实就是8字节对齐。如果末字节以0xX0结尾,就是16字节对齐

这里我们把原先的0xcafebabepop出去的话,是这样

image1024×595 19.8 KB

实际上,pop只是改变了rsp的指向,并没有对栈内的数据进行修改,只是我们不再能通过rsp直接访问到0xcafebabe了。但是数据仍然留存在栈上,我们可以通过其他方式访问。现在我们知道,rsp实际上是变化的指针,如果只是取用栈顶的数据还好,我们可以直接pop出来,但是如果我们需要取用栈中保存的地址呢?如果还是用rsp来取用的话,rsp会与栈中的数据发生相对位置的变化,不可能一个一个pop出来吧?所以我们很容易想到用rbp来进行寻址,因为在一个函数的执行过程,rbp的值是不变的,所以我们可以通过rbp加上偏移,即[rbp + offset]的方式来访问栈中的数据

Function Calling Convention

在先前我们说,函数的调用和返回是通过栈实现的,具体的实现方法就被称作函数调用约定。简单来说,函数调用约定规定了一个函数如何储存它的局部变量,如何从它本身返回到父函数。在详细讲解C语言的函数调用约定时,我们先来了解一下栈帧Stack frame作为关键的概念如何发挥作用。

Stack Frame

程序的执行可以看作连续的函数调用,当一个函数调用后被执行完毕,需要返回原来的函数继续执行。所以函数的调用通常是嵌套的,栈中时常会存在不同函数的不同数据,如何保证不发生数据泄露就是一个很关键的问题。于是,我们给每个函数分配一个独立的连续栈空间,称作栈帧

image820×547 75.4 KB

栈帧的边界通常由bpsp寄存器界定。在64位中,寄存器rsp指向当前函数的栈顶,寄存器rbp指向当前函数的栈底。局部变量在栈中存储,通过rbp进行寻址。

值得注意的是

  • 64位中,前六个参数由寄存器传递,从左到右分别是rdi, rsi, rdx, rcx, r8, r9。剩余的参数在栈上传递
  • 32位中,所有参数都在栈上传递。

Experiment - Pwndbg调试经典C程序

光讲解可能有点无趣,我们由一个实验来具体地学习C语言的函数调用约定。

编译以下源码

#include <stdio.h> int xor(int x, int y) { return x ^ y; } int main() { int a = 0xdead; int b = 0xbeef; int c; c = xor(a, b); printf("%p", c); return 0; }

64位和32位都值得学习,我们两种情况都进行讲解。先来简单解释一下这个经典程序。

  • xor函数具有两个参数,int型的x和y,将x和y异或之后返回一个int
  • main函数定义了三个int型的局部变量,初始化a,b,声明c;随后main调用xor函数,传入a,b。xor将a,b异或之后返回一个intmain将返回值存储到c中,随后以地址的形式打印c,结束返回0。

32 Bits

接下来我们先学习32位的情况,用以下命令编译

gcc -m32 exper0131.c -o b32

-m32表示使用32位编译。

使用gdb启动程序

gdb b32

为了完整调试完毕整个程序,我们从程序起点开始。

pwndbg> starti

image1920×901 299 KB

可以看到大部分寄存器都是0,这就是程序的起点。你可能发现了这里好像并不是main函数。没错,程序确实不是从main函数开始运行的,而是从start开始的,在进行一系列初始化工作后,才会调用main函数。所以实际上你会看到main会返回一个值,这就是原因。

我们跳过这部分复杂的初始化,直接定位到main。首先可以通过p(print)指令打印main的地址。

pwndbg> p main $1 = {<text variable, no debug info>} 0x56556192 <main>

拿到main的地址了,我们下个断点,使用b(breakpoint)指令

pwndbg> b *0x56556192

这里也可以直接b main

可以通过i b查看自己下过的断点。

然后我们需要让程序跑过去,其他的部分可以略过,所以直接使用c(continue)命令。

这里可不能用r命令,因为r命令会新开一个进程,而c命令会在当前的进程上继续执行。

image1920×919 363 KB

首先是函数序言。

  • lea ecx, [esp + 4]这条指令之前说过,是取有效地址,简单理解就是取指针。esp + 4就是0xffffd340[]在汇编语言中表示进行一次解引用,所以[esp + 4]就是栈上的1,而 lea是取有效地址,那么就是把0xffffd340赋给了ecx

我们通过ni执行一下,可以发现确实如我们所说。

  • and esp, 0xfffffff0这条指令将esp进行与运算,实际上是将esp低四位清零,所以0xffffd33c就会变成0xffffd330。由于esp是栈指针,所以对esp的操作会导致栈出现变化。

image1571×742 289 KB

执行之后发现栈抬升了一段距离。注意我们之前说的栈从高地址向低地址生长。这里pwndbg的栈是向下升高排序,所以esp的值减少是抬栈,增加就是降栈。那么这里为什么要进行抬栈呢?不知道你注意到我们抬栈之前和抬栈之后esp的变化没有,没错,esp的低四位清零意味着我们的栈16字节对齐了。这样做通常是为了满足优化和编译器需求,对齐后的运算会相对简单。

  • push dword ptr [ecx - 4]这条指令将ecx - 4的地址解引用后作为一个指针push到了栈上。这里就是将main返回地址保存到栈上了。这是因为栈对齐之后,栈顶发生了变化,原先的返回地址就失效了,所以需要重新保存一下返回地址。至于返回地址是什么,这里先按下不表,我们继续调试。

image1565×1081 404 KB

来到push ebp,为了便于大家理解,我们这里通过画图+调试的方法进行讲解。首先是一开始的时候,mainlibc_start_main调用,刚进入main函数,结束libc_start_main的栈帧

image410×993 3.82 KB

push ebp,这里的ebp还是之前_libc_start_main的栈帧的ebp。也就是说,这步保存了上一个函数的栈底

image430×277 2.93 KB

对应到gdb中

image1036×736 235 KB

mov ebp, esp,将esp的值赋给ebp,由于二者都是指针,所以这步实际上是将ebp移动到和esp相同的位置。

image763×148 4.19 KB

对应到gdb中

image1054×260 110 KB

现在二者的值都是0xffffd328。什么意思呢?实际上,现在栈帧已经开辟完成了,main有了新的栈底0xffffd328ebp也指向新的栈底。

push ecx保存ecx的值,esp上移。

image735×175 4.47 KB

随后,main需要一片栈空间用于保存局部变量,所以sub esp, 0x10抬栈。

image712×382 5.74 KB

此时的栈空间

image818×240 75.5 KB

这时call了一个神秘函数__x86.get_pc_thunk.bx。这个函数是用来ip寻址的,可以暂时不用管,我们专注调试程序内容。

来到mov dword ptr [ebp - 0x14], 0xdead

image1920×936 361 KB

此时函数开始保存局部变量0xdead, 0xbeef。可以看到现在都是通过[ebp + offset]进行寻址,可以通过Stack栏直观了解到程序在哪里保存了什么变量。

image800×250 79.6 KB

现在初始化完成局部变量后,马上就要调用xor了,此时是重点。

image1758×390 156 KB

程序连续push了两个值到栈上。现在的栈就变成了

image704×496 18.3 KB

你可能会疑惑,一模一样的数据为什么要两份?我们先暂且按下不表,继续调试。现在两个整数已经被作为参数保存在栈上,我们马上要调用xor函数,一个新的问题出现了:在执行完xor函数后,怎么返回main函数呢?

我们知道程序的执行流由eip寄存器控制,它总是保存当前指令的下一条指令的地址。所以我们要从xor返回到main,首要的任务就是恢复eip寄存器,要恢复的前提就是需要拥有备份,那我们在调用xor之前,先将当前eip的值push到栈上不就好了吗!这样,调用xor的指令的下一条指令的位置就被我们保存在了栈上,后续我们只需要把这个地址放到eip中,不就能自然地执行下去了吗。

所以,call xor,实际上做了两件事

  • push eip保存eip的值,作为xor返回地址xor总会无条件地将这里的值作为main返回
  • jmp xor跳转到xor函数中,这一步修改了eip寄存器

然后我们si步入xor进行调试

image1628×1261 417 KB

来到xor开始。我们还有一个问题亟需解决:执行流的问题是解决了,但是上下文呢?我们还需要恢复函数的栈帧啊。恢复栈帧的关键是什么?就是bp。只要我们恢复了bp,就恢复了原先函数的栈帧。由于bp在函数调用的过程中不会变化,只有ip会变,所以我们可以在函数序言执行这个工作:push ebp

image754×620 22.5 KB

然后开辟xor的栈帧:mov ebp, esp

image755×565 10.6 KB

然后开始寻址,找到刚刚push到栈上的两个参数。通过eax进行xor。得到的0x6042作为返回值刚好存储在eax中。eax作为返回值寄存器进行返回。

image1439×493 186 KB

关键点来了,此时我们pop ebp还原main函数的栈帧。

image749×564 10.2 KB

然后,返回到mainret。这个指令实际上就是pop eip

image789×559 10.5 KB

此时我们返回到main,栈帧也改回了main的栈帧。现在的add esp, 8处理残余的参数。

image783×587 10.7 KB

这时我们就知道为啥要备份一份了,因为一开始我们的栈帧就只开辟了0x10

随后就是printf打印了,具体的佬友们可以自行调试一下,这里就略过了。

image1328×658 194 KB

直接到这里,销毁栈帧

image803×591 10.6 KB

然后返回。

image1328×658 194 KB

至此,32位调试结束。我们马不停蹄进行64位的调试。

64 Bits

64位的调试我们就只看gdb了。

image1920×1062 332 KB

可以看到相比于32位,多了很多寄存器。基础寄存器的名字也发生了改变。实际上大多数寄存器就当作变量来理解就行。不要畏惧。我们定位到main

image1788×1325 564 KB

地址变长了,但是看起来反而更工整。首先是函数序言,保存上一个函数的bp,然后开辟栈帧。这里就少了栈对齐的步骤,因为64位本身地址低四位不是0就是8. 实际上不需要对齐。

image886×230 104 KB

然后是保存局部变量,没什么变化,也是用[rbp + offset]进行寻址。

image1219×230 115 KB

从这里开始就有变化了,由于64位多了很多寄存器,而且单位也变大了,所以用速度更快的寄存器进行传参是有必要的。首先,程序先将栈上的变量保存到edx, eax寄存器中,这里就纯粹的当变量用。需要注意的是这里使用的是edx而非rdx为什么?实际上,rdx往往会比使用edx的指令使用更多的字节数。在64位中存在零拓展机制,即当高位为0时,会自动当成rdx类型来处理。

随后mov esi, edx, mov edi, eax就是进行传参了。

image1538×920 333 KB

这里的call和32位并无本质区别,就略过了,我们直接步入xor

image1538×920 333 KB

依旧函数序言,然后这里将寄存器中的参数保存到了栈上,进行运算后使用rax存储返回值进行返回。

回到main

image1553×1070 444 KB

打印之后返回,这里就有区别了

image1046×115 31.6 KB

说一下这个leave。我们看一下leave之前的栈帧

image1228×232 119 KB

leave之后

image1285×239 129 KB

没错,leave实际上干了两件事。

  • mov rsp, rbp:将rsp移动到rbp上,这一步是销毁栈帧
  • pop rbp:还原上一个函数的bp

然后返回。调试结束。


Summries

本节内容比较多,但是一定要认真看完,这是后续我们进行漏洞利用中最基础最重要的内容。总结一下,其实就是程序在系统中是如何被调用的,如何返回,如何储存变量,如何寻址等。希望本节的实验,各位感兴趣的佬友能亲自调试。

Have Fun!我们下个帖子再见!

网友解答:
--【壹】--:

前排支持!


--【贰】--:

很强的佬啊,更新速度比我学的速度快