pwn学习

一、** 函数调用栈

32位程序:

寄存器介绍、寄存器使用约定、栈帧结构、函数调用在栈上的实现。

1、32位程序的寄存器:

4

其中有8个32位通用寄存器,其中包含4个数据寄存器(EAX、EBX、ECX、EDX)、2个变址寄存器(ESI和EDI)和2个指针寄存器(ESP和EBP);6个段寄存器(ES、CS、SS、DS、FS、GS);1个指令指针寄存器(EIP);1个标志寄存器(EFLAGS)。

我们重点关注数据寄存器、指针寄存器和指令指针寄存器。

数据寄存器用于保存操作数和运选结果等信息,所以在函数调用中用于保存函数的参数;而指针寄存器主要用于堆栈的访问,其中EBP基指针(Base Pointer)寄存器,ESP为堆栈指针(Stack Pointer)寄存器,关于他们的具体作用在之后会有所介绍;指令指针寄存器(EIP)用于存放下次将要执行指令的地址。

(1)、寄存器使用约定:

依照惯例,数据寄存器eax、edx、ecx为主调函数保存寄存器,用于保存主调函数的相关参数及运算数据。在函数调用的过程中,如果主调函数希望保存寄存器中的数值,就要在调用前将值保存在栈中,而后这些寄存器可以借给被调函数使用,在被调函数完成之后便可恢复寄存器的值;而寄存器ebx、esi、edi为被调函数保存寄存器,使用方法与上述类似。此外被调函数必须保持寄存器esp和ebp,并在函数返回后将其回复到调用前的值。具体过程将在后面有所提及。

(2)、栈帧结构

栈区:由高地址向低地址生长,在程序运行的时候用于保存函数调用信息和存放局部变量。

栈区在内存中的位置:

1

栈帧的定义:在堆栈中,函数占用的一段独立的连续区域,称为栈帧(Stack Frame).所以,栈帧是堆栈的逻辑片段。

栈帧作为堆栈的逻辑片段,那么其必然就有边界。栈帧的边界由EBP和ESP界定,EBP指向栈帧的高地址,我们称之为栈底,而ESP指向栈帧的低地址,我们称之为栈顶。ESP会随着数据的出入栈而移动,因此函数中对于大部分数据的访问都基于EBP进行,这个点与之后的学习密切相关。

2

上图中深色框框起来的部分就是一个栈帧。其中栈帧的具体结构由于参数与变量的不同而有所区别。

从图中可以看到,函数调用的入栈顺序为实参n~1、主调函数的帧基地址EBP、被调函数的局部变量……

所以可以根据上图给出函数调用栈的形成过程:主调函数按照调用约定依次入栈,然后将指令指针EIP入栈以保存主调函数的返回地址,也就是上图的返回地址处。而在进入被调函数时,被调函数将主调函数的帧基指针EBP入栈,并将主调函数的栈顶指针ESP值赋给被调函数的EBP(作为被调函数的栈底),接着改变ESP值来为函数局部变量预留空间。此时被调函数帧基指针指向被调函数的栈底。以该地址为基准,向上(栈底方向)可获取主调函数的返回地址、参数值,向下(栈顶方向)能获取被调函数的局部变量值,而该地址处又存放着上一层主调函数的帧基指针值。本级调用结束后,将EBP指针值赋给ESP,使ESP再次指向被调函数栈底以释放局部变量;再将已压栈的主调函数帧基指针弹出到EBP,并弹出返回地址到EIP。ESP继续上移越过参数,最终回到函数调用前的状态,即恢复原来主调函数的栈帧。如此递归便形成函数调用栈。

(3)、函数调用在栈上的实现

上面已经给出了函数调用栈的形成构成,现在将根据具体的例子进一步讲解函数调用在栈上的实现。

void swap(int *a,int *b)
{
    int tmp;
    tmp = *a;
    *a = *b;
    *b = tmp;
}
int main()
{
    int c=1,d=2;
    swap(&c,&d);
    return 0;
}

有这样一段代码,它的功能是交换参数c和d的值,下面我们分析结合函数调用栈来分析这个程序函数调用过程。

​ 首先介绍一下函数调用过程中的主要指令:

入栈(push): 栈顶指针ESP减小4个字节(可理解为ESP向下移动);以字节为单位将寄存器数据压堆栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4指向的地址单元。

出栈(pop): 栈顶指针ESP指向的栈中数据被取回到寄存器;栈顶指针ESP增加4个字节(理解为ESP向上移动)。

调用(call):将当前的指令指针EIP(该指针指向紧接在call指令后的下条指令)压入堆栈,以备返回时能恢复执行下条指令;然后设置EIP指向被调函数代码开始处,以跳转到被调函数的入口地址执行。

离开(leave): 恢复主调函数的栈帧以准备返回。等价于指令序列mov %ebp, %esp(恢复原ESP值,指向被调函数栈帧开始处)和pop %ebp(恢复原ebp的值,即主调函数帧基指针)。

返回(ret):与call指令配合,用于从函数或过程返回。从栈顶弹出返回地址(之前call指令保存的下条指令地址)到EIP寄存器中,程序转到该地址处继续执行(此时ESP指向进入函数时的第一个参数)。若带立即数,ESP再加立即数(丢弃一些在执行call前入栈的参数)。使用该指令前,应使当前栈顶指针所指向位置的内容正好是先前call指令保存的返回地址。

​ 结合以上代码,我们能够得到这样的过程:

1、 首先,main函数的栈帧中存有上一个函数的的返回地址和ebp,它自己的声明的局部变量存入栈中。

2、 main函数将swap函数所要求的参数&c, &d压入栈中,这里依照的调用约定是C调用约定,所以参数是从右往左压入。这里用到了汇编指令push &d , push &c,同时ESP向下移动。

3、 接着main函数调用call指令,call指令有两个步骤:把下一条指令的地址EIP压入栈中(push eip);跳转到swap 函数(jmp swap)。到这里调用者main的任务就基本完成了。

4、 接下来来到swap函数的栈帧,首先将main函数ebp值压入栈中(push ebp)接着将esp和ebp移动到该处(mov ebp, esp),这样两个指针就达到在一起时的状态了。

5、 接下来swap函数会根据情况开辟一定的栈空间,这一步是通过esp的移动来实现的,用汇编代码表示就是sub esp ,4。这里的意思时esp向下移动了4个字节的空间,用于存放声明的变量tmp 。

6、 之后swap函数形参a, *b 的访问是通过ebp的偏移量来访问的。因为形参的值对应的时main函数中的变量,所以这里a 就是ebp+8 ,*b 就是ebp+12 , tmp就是ebp-4。

7、 接下来,swap函数完成了自己的任务了,接下来应该是栈帧回复到初始状态。

8、 首先swap调用leave指令,指令分为两步:返回esp , ebp 指针(mov esp , ebp),esp回到栈底,ebp回到了之前main函数ebp保存的位置;将栈中存放的ebp值弹出(pop ebp),这时esp也向上移动4个字节。

9、 接着调用return指令,也就是将eip的值弹出(pop eip),将main函数下一条指令的地址存入eip中执行。

10、 接下来main函数就不需要&c ,&d参数了,esp向上移动8个字节(add esp ,8)。至此,main函数又回到了调用swap函数前的栈帧状态。

2、64位程序

64位程序的函数调用栈与32位的基本一致,但是64位程序相对于32位,增加了寄存器的数量,并且寄存器的名称也有所变化。

64位有16个寄存器,而且实在32位寄存器的基础上增加了8个,只不过前8个寄存器在命名上与32位有所区别,将首字母e改成了r(比如esp改为了rsp)。

在堆栈中,64位传递参数的方式也与32位有所区别,32位参数通过栈传递,而64位是通过寄存器(rdi、rsi、rdx、r8、r9)存放参数,只有在参数的数量为7或以上时,才将参数存放到栈中。所以这导致了32位函数栈帧的构建与64位有所区别,他们的具体体现在之后会有所提及。