栈帧


图中展示的是函数 F(a, b, c) 的栈帧(Stack frame)结构。栈帧是程序运行时在栈(Stack)上为函数调用所分配的一块内存区域,用于管理函数调用的相关信息

  • 参数部分:从高地址开始依次存储函数的参数 c、b、a。在许多编程语言和架构中,参数是从右向左压入栈中的
  • 返回地址(Return addr):存储函数执行完毕后返回的地址,对应保存的扩展指令指针寄存器(EIP)的值,它指示函数结束后程序继续执行的位置
  • 保存的基指针(Saved EBP):保存调用函数前的基指针(EBP),EBP 用于标识栈帧的底部,方便在函数内访问栈中的变量
  • 局部变量(Local var):用于存储函数内部定义的局部变量,如 Local var 1、Local var 2 等。随着局部变量的增多,栈向下(低地址方向)增长
  • 保留空间(Reserved space):为压入的参数或内存对齐而预留的空间,栈指针寄存器(ESP)指向栈顶,即该保留空间的位置

缓冲区溢出

#include<stdio.h>
int main(void)
{
    char s[10];
    scanf("%s",s);
    return 0;
}

上面的程序中,由于scanf不会检查输入的字符串的长度,如果输入的字符串超过10个字符,就会发生缓冲区溢出

上图表示了栈结构

  • 地址方向:图中显示内存地址从高到低的方向,栈的增长方向是从高地址向低地址增长(stack grow)
  • 栈帧元素:右侧列出了栈帧中的一些元素,包括 argv(命令行参数数组指针)、argc(命令行参数个数)、saved eip(保存的指令指针,函数返回地址)、previous ebp(之前的基指针)。其中 0xffffd884 等是对应的内存地址,$ebp=$esp 表示基指针和栈指针在某一时刻相等

缓冲区溢出风险
在上面的程序中,如果用户输入的字符串长度超过10个,超出的部分会覆盖栈中相邻的内存区域,可能会回覆盖到previous ebp和saved eip等重要数据。
将上面程序编译为32位可执行文件

gcc -m32 -o hello hello.c

用gdb调试

gdb hello
b main
r
info frame
x/10x $ebp

上面命令依次为打开gdb调试,在main函数处设置断点,,运行,查看栈帧信息,查看内存内容,运行后内容如下:

  • 栈帧信息:显示栈级别为0,帧地址在0xffffcff0,eip(指令指针)当前值为0x5655619c,保存的eip为0xf7c24d43,参数列表地址在 0xffffcfd8等等
  • 内存内容:冒号后面共四列内容,依次为previous ebp,eip,argc,argv

接下来给scanf下断点
可得

栈帧信息、内存内容看上面图片,同理上


可以明显看到发生了缓冲区的溢出

计算从缓冲区(buffer)到保存的指令指针(EIP)的偏移量(offset)

F(a,b,c){
    int x,y;
    char buf[12];
    scanf("%s",buf);
}

上图展示了函数 F 的栈帧结构,从高地址到低地址依次为函数参数 c、b、a,接着是保存的返回地址(即 Saved EIP)、保存的基指针(Saved EBP),然后是局部变量 x、y,以及缓冲区 buf[12] 等。栈的增长方向是从高地址向低地址

偏移量计算

  • EIP 与 EBP 的关系:EIP = EBP + 4,这表示保存的指令指针(EIP)距离保存的基指针(EBP)是 4 个字节,因为在 32 位系统中,地址通常占 4 个字节
  • buf[0] 与 EBP 的关系:buf[0] = EBP - 12(buf) - 8(2 int) - 8(align)。其中 12(buf) 表示缓冲区 buf 的大小为 12 字节,8(2 int) 表示两个整型变量 x 和 y 共占用 8 字节(每个 int 通常是 4 字节),8(align) 表示为了内存对齐所占用的 8 字节。计算得出 buf[0] 距离 EBP 是 28 字节
  • 最终偏移量:通过上述计算可知,从缓冲区 buf 到 EIP 的偏移量是 28 字节。在缓冲区溢出攻击中,攻击者需要填充 28 字节的数据,才能开始覆盖保存的 EIP 地址,进而控制程序的执行流程

以上是对课件的整理,如有不足,请各位大佬指点