栈溢出实例 #
先看一下代码:
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_LEN 24
void bad() {
printf("Haha, I am hacked.\n");
exit(1);
}
void copy(char* dst, char* src, int n) {
int i;
for (i = 0; i < n; i++) {
dst[i] = src[i];
}
}
void test(char* t, int n) {
char s[16];
copy(s, t, n);
}
int main() {
char t[BUFFER_LEN] = {
'w', 'o', 'l', 'd',
'a', 'b', 'a', 'b', 'a', 'b',
'a', 'b', 'a', 'b', 'a', 'b',
};
int n = BUFFER_LEN - 8;
int i = 0;
for (; i < 8; i++) {
t[n+i] = (char)((((long)(&bad)) >> (i*8)) & 0xff);
}
test(t, BUFFER_LEN);
printf("hello\n");
}
你可以用 gcc 编译器来编译上面这个程序:
gcc -O1 -o bad bad.c -g -fno-stack-protector
执行它,你可以看到,虽然在 main 函数里我们并没有调用 bad 函数,但它却执行了。最后运行结果是“Haha, I am hacked”。
当我们在调用 test 函数的时候,会把返回地址,也就是 rip 寄存器中的值,放到栈上,然后就进入了 test 的栈帧,CPU 接着就开始执行 test 函数了。
test 函数在执行时,会先在自己的栈帧里创建数组 s,数组 s 的长度是 16。
通过计算,我们可以知道返回地址是变量 s 的地址 + 16 的地方,这就是我们要攻击的目标。我们只要在这个地方把原来的地址替换为函数 bad 的入口地址(第 26 至 34 行所做的事情),就可以改变程序的执行顺序,实现了一次缓冲区溢出。
简单地说,数组 s 的长度是 16,理论上我们只能修改以 s 的地址开始、长度为 16 的数据。但是我们现在通过 copy 函数操作了大于 16 的数据,从而破坏了栈上的关键数据。也就是说我们针对函数调用的返回地址发起了一次攻击。所以,test 函数的实现是不安全的。
其实这种缓冲区溢出,就是指通过一定的手段,来达成修改不属于本函数栈帧的变量的目的,而这种手段多是通过往字符串变量或者数组中写入错误的值而造成的。
有两种常见的手段可以对这一类攻击进行防御。第一,对入参进行检查,尽量使用 strncpy 来代替 strcpy。因为 strcpy 不对参数长度做限制,而 strncpy 则会做检查。比如上述例子中,如果我们对参数 n 做检查,要求它的值必须大于 0 且小于缓冲区长度,就可以阻击缓冲区溢出攻击了。第二,可以使用 gcc 自带的栈保护机制,这就是 -fstack-protector 选项。你查 gcc 手册(在 Linux 系统使用“man gcc”就能查到)可以看到它的一些相关信息。