目录

  1. 1. 前言
  2. 2.
  3. 3. 栈帧
  4. 4. 函数调用实例
    1. 4.1. 函数的调用
    2. 4.2. 函数的返回

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

C调用过程原理及函数栈帧分析(Intel)

2024/8/10 Pwn
  |     |   总文章阅读量:

前言

看CVE-2024-2961的时候卡在了需要二进制基础的部分,于是从这里开始正式接触一下二进制安全的内容

参考原文:

https://www.yuque.com/cyberangel/rg9gdm/gcz7x2


在x86的计算机系统中,内存空间中的栈主要用于保存函数的参数,返回值,返回地址,本地变量

一切的函数调用都要将不同的数据、地址压入或者弹出栈

LIFO(后进先出),数据结构已经学了这里就不多写了

这种形式的数据结构正好满足我们调用函数的方式: 父函数调用子函数,父函数在前,子函数在后;返回时,子函数先返回,父函数后返回

栈支持两种基本操作:push(入栈)和 pop(出栈,将栈中的数据弹出并存储到指定寄存器或者内存中)

push 0x50 ; 将0x50的压入栈

image-20240810183814570

pop 寄存器名称 ; 将栈中的0x50弹出到某个寄存器中

image-20240810183835497

注意:

  • 上面例子中栈的生长方向是从高地址到低地址的,对应到图片中栈是向下生长的

  • pop操作后,栈中的数据并没有被清空,只是该数据我们无法直接访问(但是仍然可以访问)


栈帧

stack frame,本质上就是一种栈,只是这种栈专门用于保存函数调用过程中的各种信息(参数,返回地址,本地变量等)

栈帧有栈顶栈底之分,其中栈顶的地址最低,栈底的地址最高,SP(栈指针)就是一直指向栈顶的

在 x86-32bit 中,我们ebp指向栈底、用esp指向栈顶

栈帧示意图如下:

image-20240810183742188

一般来说,我们将ebpesp之间区域当做栈帧

整个栈空间不只有一个栈帧,每调用一个函数,就会生成一个新的栈帧

在函数调用过程中,我们将调用函数的函数称为“调用者(caller)”,将被调用的函数称为“被调用者(callee)”:

  1. “调用者”需要知道在哪里获取“被调用者”返回的值
  2. “被调用者”需要知道传入的参数在哪里
  3. 返回的地址在哪里

同时,我们需要保证在“被调用者”返回后,**ebpesp 等寄存器的值应该和调用前一致**。因此,我们需要使用栈来保存这些数据。


函数调用实例

函数的调用

简单写一个demo:

#include <stdio.h>
int MyFunction(int x, int y, int z)
{
    int a, b, c;
    a = 10;
    b = 5;
    c = 2;
    return;
}
int main(){
    return 0;
}

在32位环境的linux下编译

gcc -o functest ./functest.c

然后把生成的可执行文件拖进ida反编译,可以看到MyFunction()的汇编代码

_MyFunction:
    push ebp            ; 保存ebp的值
    mov  ebp, esp       ; 将esp的值赋给ebp,使新的ebp指向栈顶
    sub  esp, 0x12  		; 分配额外空间给本地变量
   	mov  qword ptr [ebp-4], 10  ;  对栈中的内存进行赋值操作
    mov  qword ptr [ebp-8], 5  ;   对栈中的内存进行赋值操作
    mov  qword ptr [ebp-12], 2  ;  对栈中的内存进行赋值操作

此时的栈:

image-20240810185616949

一开始两个栈帧都视为是空的,在这个过程中,调用者做了两件事情:

  1. 将被调用函数的参数压入栈中
  2. 将返回地址压入栈中

这两件事都是调用者负责的,因此压入的栈应该属于调用者的栈帧

然后看被调用者:

  1. 将旧的(调用者的) ebp压入栈,此时esp指向它
  2. esp的值赋给ebpebp就有了新的值,它也指向存放旧ebp的栈空间,这时它成了是函数 MyFunction() 栈帧的栈底

这样,我们就保存了“调用者”函数的 ebp,并且建立了一个新的栈帧

接下来,在 ebp 更新后,我们先分配一块0x12字节的空间用于存放本地变量,这步使用sub实现

通过使用mov转移指令配合字节数ptr [offset]我们便可以给 a, b 和 c 赋值了

image-20240810190928975


函数的返回

和调用函数时相反,当函数完成自己的任务后,它会将 esp 移到 ebp 处,然后再弹出旧的 ebp 的值到 ebp 寄存器

这样 ebp 就恢复到了函数调用前的状态了

demo:

int MyFunction( int x, int y, int z )
{
    int a, int b, int c;
    ...
    return;
}

汇编大致如下:

_MyFunction:
    push ebp
    mov  ebp, esp
    ...
    mov esp, ebp
    pop ebp
    ret

最后的ret指令,相当于pop + jump,它首先将数据(返回地址)弹出栈并保存到eip中,然后处理器根据这个地址无条件地跳到相应位置获取新的指令

image-20240810191735985