Glibc PWN 0x2 函数调用

Glibc PWN 0x2 函数调用

本次将主要介绍C语言下32和64位的堆栈调用,这部分是解决PWN问题的基础,务必要清晰掌握

0x1 基本概念

  • 程序的执行过程可看作连续的函数调用。当一个函数执行完毕时,程序要回到调用指令的下一条指令(紧接call指令)处继续执行。函数调用过程通常使用堆栈实现,每个用户态进程对应一个调用栈结构(call stack)。编译器使用堆栈传递函数参数、保存返回地址、临时保存寄存器原有值(即函数调用的上下文)以备恢复以及存储本地局部变量。
  • 不同处理器和编译器的堆栈布局、函数调用方法都可能不同,但堆栈的基本概念是一样的。
  • 栈是向下生长的,即栈顶元素处在内存中处于低地址位,栈底元素在内存中处于高地址位
  • 入栈顺序为实参N~1→主调函数返回地址→主调函数帧基指针EBP→被调函数局部变量1~N

0x2 寄存器分配

  • 寄存器是处理器加工数据或运行程序的重要载体,用于存放程序执行中用到的数据和指令。因此函数调用栈的实现与处理器寄存器组密切相关。 Intel 32位体系结构(简称IA32)处理器包含8个四字节寄存器,如下图所示:

img

  • 在x86处理器中,EIP是指令寄存器(不能直接访问),指向处理器下条等待执行的指令地址(代码段内的偏移量),每次执行完相应汇编指令EIP值就会增加。ESP是堆栈指针寄存器,存放执行函数对应栈帧的栈顶地址(也是系统栈的顶部),且始终指向栈顶;EBP是栈帧基址指针寄存器,存放执行函数对应栈帧的栈底地址,用于C运行库访问栈中的局部变量和参数。
  • 为了访问函数局部变量,必须能定位每个变量。局部变量相对于堆栈指针ESP的位置在进入函数时就已确定,理论上变量可用ESP加偏移量来引用,但ESP会在函数执行期随变量的压栈和出栈而变动。因此通常使用EBP加偏移量的方式来引用变量

0x3 寄存器使用约定

  • 程序寄存器组是唯一能被所有函数共享的资源。虽然某一时刻只有一个函数在执行,但需保证当某个函数调用其他函数时,被调函数不会修改或覆盖主调函数稍后会使用到的寄存器值。
  • 当函数调用时,若主调函数希望保持这些寄存器的值,则必须在调用前显式地将其保存在栈中;被调函数可以覆盖这些寄存器,而不会破坏主调函数所需的数据。
  • 寄存器eax、edx和ecx为主调函数保存寄存器(caller-saved registers)
  • 寄存器ebx、esi和edi为被调函数保存寄存器(callee-saved registers)

0x4栈帧结构

  • 栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定(指针存放在相应寄存器中)。EBP指向当前栈帧底部(高地址),EBP在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行

  • 为更具描述性,以下称EBP为帧基指针, ESP为栈顶指针,并在引用汇编代码时分别记为ebp和esp。

  • 函数调用栈的典型内存布局如下图所示:

  • img

  • 从图中可以看出,栈内存布局为

    「高地址 栈底」上一个栈帧 → 上一个栈帧的栈基指针[EBP] → 上一个栈帧的局部变量*n → 本栈帧参数*n →

    本栈帧返回地址[ret] → 本栈帧栈基指针[ebp] → 本栈帧局部变量*n → 下一个栈帧参数*n → 下一个栈帧返回值[ret]「低地址栈顶」

    • 其中局部变量和参数不是函数栈帧的必须部分
  • 函数调用时的入栈顺序

    实参N~1→主调函数返回地址→主调函数帧基指针EBP→被调函数局部变量1~N

  • 函数调用过程:

    • 主调函数将参数按照调用约定依次入栈(图中为从右到左),然后将指令指针EIP入栈以保存主调函数的返回地址(下一条待执行指令的地址)。

    • 进入被调函数时,被调函数将主调函数的帧基指针EBP入栈,并将主调函数的栈顶指针ESP值赋给被调函数的EBP(作为被调函数的栈底),接着改变ESP值来为函数局部变量预留空间。[push ebp; mov ebp, esp]

    • 此时被调函数帧基指针指向被调函数的栈底。以该地址为基准,向上(栈底方向)可获取主调函数的返回地址、参数值,向下(栈顶方向)能获取被调函数的局部变量值,而该地址处又存放着上一层主调函数的帧基指针值(也就是说,当前帧基指针指向的内存地址,其中存放的是上一层主调函数的帧基指针

      应该是 因为push ebp把上一个主调函数的帧基指针地址入栈,而esp始终指向栈顶,所以esp指向存放ebp寄存器地址的地址,之后mov ebp, esp,则将ebp指向存放上一个帧基指针的地址。

    • 本级调用结束后,将EBP指针值赋给ESP,使ESP再次指向被调函数栈底以释放局部变量;mov esp, ebp; pop ebp

    • 再将已压栈的主调函数帧基指针弹出到EBP,并弹出返回地址到EIP。

    • ESP继续上移越过参数,最终回到函数调用前的状态,即恢复原来主调函数的栈帧。

    • 如此递归便形成函数调用栈。

  • 当涉及到结构体时:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # include <stdio.h>
    # include <string.h>
    //比如结构体如下
    struct Strt{
    int member1;
    int member2;
    int member3;
    };
    // 在压栈过程中结构体tStrt中的成员变量memberx地址 = tStrt首地址 + member x偏移量
    // 即越靠近tStrt首地址的成员变量其内存地址越小,结构体成员变量的入栈顺序与其在结构体中声明的顺序相反
    // 入栈顺序 memeber3 memeber2 memeber1
  • 为什么改变函数内部的形参,其所对应的实参不会被修改 → 因为形参和实参所对应的储存地址不同,其不在一个栈帧上

0x5 堆栈操作

  • 函数调用时的具体步骤如下:

    1. 主调函数将被调函数所要求的参数,根据相应的函数调用约定,保存在运行时栈中。该操作会改变程序的栈指针。

      • 注:x86平台将参数压入调用栈中。而x86_64平台具有16个通用64位寄存器,故调用函数时前6个参数通常由寄存器传递,其余参数才通过栈传递。
    2. 主调函数将控制权移交给被调函数(使用call指令)。函数的返回地址(待执行的下条指令地址)保存在程序栈中(压栈操作隐含在call指令中)。

    3. 若有必要,被调函数会设置帧基指针,并保存被调函数希望保持不变的寄存器值。

    4. 被调函数通过修改栈顶指针的值,为自己的局部变量在运行时栈中分配内存空间,并从帧基指针的位置处向低地址方向存放被调函数的局部变量和临时变量。

    5. 被调函数执行自己任务,此时可能需要访问由主调函数传入的参数。若被调函数返回一个值,该值通常保存在一个指定寄存器中(如EAX)。

    6. 一旦被调函数完成操作,为该函数局部变量分配的栈空间将被释放。这通常是步骤4的逆向执行。

    7. 恢复步骤3中保存的寄存器值,包含主调函数的帧基指针寄存器。

    8. 被调函数将控制权交还主调函数(使用ret指令)。根据使用的函数调用约定,该操作也可能从程序栈上清除先前传入的参数。

    9. 主调函数再次获得控制权后,可能需要将先前的参数从栈上清除。在这种情况下,对栈的修改需要将帧基指针值恢复到步骤1之前的值。

  • 以下介绍函数调用过程中的主要指令。

    • **压栈(push)**:栈顶指针ESP减小4个字节;以字节为单位将寄存器数据(四字节,不足补零)压入堆栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4指向的地址单元。
  • **出栈(pop)**:栈顶指针ESP指向的栈中数据被取回到寄存器;栈顶指针ESP增加4个字节。

    • **调用(call)**:将当前的指令指针EIP(该指针指向紧接在call指令后的下条指令)压入堆栈,以备返回时能恢复执行下条指令;然后设置EIP指向被调函数代码开始处,以跳转到被调函数的入口地址执行。
      • 在call指令之前,程序会先把调用函数的参数入栈
      • 在call执行之后,会执行以下指令:
        • push ebp # 保存主调函数栈基址
        • move ebp,esp #将ebp指向栈底
        • sub esp,n # 为接下来的局部变量开空间
    • **离开(leave)**: 恢复主调函数的栈帧以准备返回。等价于指令序列mov esp, ebp(恢复原ESP值,指向被调函数栈帧开始处)和pop ebp(恢复原ebp的值,即主调函数帧基指针)。
      • leave指令会执行以下两条指令
        • mov esp, ebp
        • pop ebp
      • leave指令一般跟在ret前面
    • **返回(ret)**:与call指令配合,用于从函数或过程返回。从栈顶弹出返回地址(之前call指令保存的下条指令地址)到EIP寄存器中,程序转到该地址处继续执行(此时ESP指向进入函数时的第一个参数)。若带立即数,ESP再加立即数(丢弃一些在执行call前入栈的参数)。使用该指令前,应使当前栈顶指针所指向位置的内容正好是先前call指令保存的返回地址。

参考:

https://www.cnblogs.com/clover-toeic/p/3755401.html