转载自:https://wdxtub.com/2016/04/16/thin-csapp-2/
缓冲区溢出
这一节是机器代码的最后一部分,主要说说由缓冲区溢出引起的攻防大战。我们先来看看程序在内存中是如何组织的(x86-64 Linux):
最上面是运行时栈,有 8MB 的大小限制,一般用来保存局部变量。然后是堆,动态的内存分配会在这里处理,例如 malloc()
, calloc()
, new()
等。
然后是数据,指的是静态分配的数据,比如说全局变量,静态变量,常量字符串。最后是共享库等可执行的机器指令,这一部分是只读的。
可以见到,栈在最上面,也就是说,栈再往上就是另一个程序的内存范围了,这种时候我们就可以通过这种方式修改内存的其他部分了。
举个例子:
typedef struct { int a[2]; double d; } struct_t;
double fun(int i) { volatile struct_t s; s.d = 3.14; s.a[i] = 1073741824; // 可能会越界 return s.d; }
不同的 i 可能的执行结果是:
fun(0)
-> 3.14fun(1)
-> 3.14fun(2)
-> 3.1399998664856fun(3)
-> 2.00000061035156fun(4)
-> 3.14fun(6)
-> Segmentation fault
之所以会产生这种错误,是因为访问内存的时候跨过了数组本身的界限从而修改了 d 的值。(结构体中a比d先定义,所以d在内存中被分配在a[2]后面的相邻位置)
数组元素可以使用指针来访问,所以对数组的引用没有边界约束。
你没看错,这是个大问题!如果不检查输入字符串的长度,就很容易出现这种问题,尤其是针对在栈上有界限的字符数组。
在 Unix 中,gets()
函数的实现是这样的:
// 从 stdin 中获取输入 char *gets(char *dest) { int c = getchar(); char *p = dest; while (c != EOF && c != ' ') { *p++ = c; c = getchar(); } *p = '