深入理解计算机系统——stackoverflow

一、栈帧结构

在讲栈帧结构之前需要明确栈帧结构是针对程序设计中的一个过程,过程是对一段指定参数和一个可选的返回值代码的抽象,它提供了一种封装代码的方式,过程的形式在代码的实现过程中具体表现为:函数(function)、方法(method)、子例程(subroutine)、处理函数(handle)等。
当x86-64过程需要的存储空间超出寄存器能够存放的大小时,就会在栈上分配空间,这部分空间称为过程的栈帧。栈帧结构用来传递参数、存储返回信息、保存寄存器及局部存储。
过程运行采用栈数据结构“先进后出”的特点,假设过程P中调用过程Q,过程P与过程Q对应一段栈帧结构见下图,在P过程中call Q时会保存当前P运行的栈地址,然后将过程Q需要的局部变量存储空间压入栈帧,即栈指针%rsp向下增长过程Q需要的局部存储空间。当过程Q返回时,函数Q需要的帧空间会被释放,并回到过程P进入Q过程前保存的返回地址处继续执行。
在这里插入图片描述
为了提高空间和时间效率,X86-64过程只分配自己所需的栈帧部分。对于6个或者更少参数的过程,可以通过寄存器传递参数。当所有的局部变量可以保存在寄存器中,而且该函数是单一的原子的(即该过程不再包含任何其他过程)便可以省略栈帧的存储过程,因此,程序设计讲究单一职责原则。
虽然部分过程调用只需寄存器,但是局部数据必须存放在内存中,一般存在三种情况需要使用内存:
(1)寄存器的大小不够;
(2)对一个局部变量使用引用&,必须为该变量产生一个地址;
(3)针对数组或者结构体,必须通过数组或结构引用才能访问;

二、内存越界引用 & 缓冲区溢出

C语言对数组的引用不进行size检查,数组或者结构引用存放在栈空间,对数组的越界访问会破坏栈空间存储。例如,在栈上分配一定大小的字符数组保存字符串,但是字符串的长度超过数组分配的空间,就会引起缓冲区溢出

void gets(char* s) {
    int ch;
    char* dest = s;
    while((c == getchar()) != '\n' && c != EOF) {
        *dest++ = c;
    }

    if (c == EOF && dest == s) {
        return NULL:
    }
    *dest++ = '\0';
    return s;
}
void echo() {
    char buf[8];
    gets(buf);
}

上述代码中分配8个字符空间的数组buf,在gets函数中若输入的字符串长度超过8即会引起缓冲区溢出;很多C语言函数缺少数组目标缓冲区大小的检查而存在风险,使用过程中建议使用其他安全函数替代。
(1)strcpy & memcpy:

#include<string.h>
char *strcpy(char *destination, const char *source);

若源字符串的长度长于目标destination的allocate长度,当source复制到目的缓冲器destination时,它会改目的缓冲器后方的存储器,导致无法预期的结果。而且程序通常容易会出现区段错误(也就是常见的例外现象),但是熟练的黑客会利用缓存溢出来破解进入操作系统。
通常使用 memcpy 函数来替代字符串的复制。

void *memcpy( void *dest, const void *src, size_t count );

(2)sprintf & snprintf
sprintf函数和snprintf函数都是字符串格式化后输出,但是snprintf针对字符串长度做了安全检查,建议使用snprintf避免缓存溢出。

int sprintf(char *str, const char *format, ...);  
int snprintf(char *str, size_t size, const char *format, ...);

(3)strcat() & strncat()
strcat和strncat均是利用字符串连接,但是strncat相较于strcat具有长度检查,strncat只允许复制n字节长度的字符append于dest后。

char *strcat(char *dest, const char *src)
char *strncat(char *dest, const char *src, size_t n)

三、对抗缓冲区溢出

存在的风险:
若攻击者非常了解过程运行的栈结构,并覆盖过程返回地址为攻击代码的的代码指针,那么执行指令ret的效果等同于跳转到攻击代码处。
对抗缓冲区溢出的三种方式:
(1)栈随机化:
地址空间布局随机化,这意味着,每次运行程序的不同部分,包括程序代码、库代码、栈、全局变量和堆数据都会被加载到内存的不同区域。所以没台机器上运行同一过程会导致栈空间分布随机化。
(2)栈破坏检测
栈帧中任何局部缓存区与栈状态之间存储一个特殊的金丝雀,是程序每次运行时产生的。因此,攻击者没法简单的知道它是什么。在函数返回的前后必须保证该寄存器里面值的一致性,若发生改变,则程序异常中止。
(3)限制可执行代码区域
读和可执行代码分离。原来x86系统是读和可执行合并成1位标志位,任何被标记的可读页也会同时可被执行。但是后续是读和执行访问模式分开,栈被标记为可读和可写,但是不可执行,而检查页是否可执行由硬件完成。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值