C语言入坑指南-缓冲区溢出

前言

缓冲区溢出通常指的是向缓冲区写入了超过缓冲区所能保存的最大数据量的数据。如果说之前所提到的一些问题可能只是影响部分功能的实现,那么缓冲区溢出将可能会造成程序运行终止,被不安全代码攻击等严重问题,因此我们不得不特别重视。

一个缓冲区溢出的例子

对于下面的程序:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <string.h>
int main(void)
{
char buff[8] = {0};
char *p = "0123456789";
strcpy(buff,p);
printf("%s\n",buff);
return 0;
}

定义一个字符数组buff,数组长度为8,使用strcpy函数将p所指向的字符串常量拷贝到buff中。
运行程序,结果如下:

1
2
3
0123456789
*** stack smashing detected ***: ./buff terminated
已放弃 (核心已转储)

可以看到,由于p所指向的字符串长度大于buff的长度,拷贝时由于缓冲区溢出而破坏了栈中的内容而导致程序终止。

实际上,有时候缓冲区溢出导致程序马上运行出错是幸运的,因为我们至少能够知道这里出错了。而不幸的情况是,如果超出buff的部分存储在了栈帧不属于它自己的位置,即覆盖了栈帧上存储的其他信息,就有可能导致程序在其他位置出错,造成问题难以定位。

当然也有很幸运的时候,那就是超出buff的部分存储在了未被使用的栈空间上。但是我们绝对不可以对此抱有侥幸心理。

如何避免

对于前面所示的例子中,我们可以很明显地看到要拷贝的字符串长度大于buff的长度,我们可以选择将buff的长度增大。但是实际编程中,我们经常难以察觉是否会超过缓冲区大小。
比如,对于gets函数:

1
2
char buff[255];
gets(buff);

我们不会知道用户在控制台会输入多少字符,但是可以肯定的是,输入字符很有可能会超过255,甚至字符中包含了可以执行代码的字节编码(当然,编译器和操作系统有很多机制,例如栈随机化,栈破坏检测,限制可执行代码区域等来对抗缓冲区溢出攻击),这个时候,灾难就会发生了。

因此我们不应该使用像gets这样不安全的函数,而选择fgets这样的可替代函数。查看gets函数的手册发现,手册中甚至直言不讳地说道:Never use gets()。

同样的,库函数中还有一些也可能造成缓冲区溢出,我们应该尽量避免使用它们,而选择使用更加安全的版本。

作用 不推荐使用函数 推荐使用函数
拷贝字符串 strcpy strncpy
字符串比较 strcmp strncmp
字符串连接 strcat strncat
格式化字符串并存入缓冲区 sprintf snprintf
读取字符串 gets fgets
复制字符串 strdup strndup
字符串比较忽略大小写 strcasecmp strncasecmp

其中推荐使用的函数特点是,限定了操作内容的大小,从而避免了缓冲区溢出。当然,可能也会带来另外一个问题,就是截断。
我们修改一下前面的程序:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <string.h>
int main(void)
{
char buff[8] = {0};
char *p = "01234567890123";
strncpy(buff,p,sizeof(buff));
printf("%s\n",buff);
return 0;
}

运行结果如下:

1
01234567

可以看到,虽然部分字符串没有被拷贝,但是避免了缓冲区溢出,程序不再异常终止。

总结

缓冲区溢出造成的危害非常大,可能导致程序运行终止或程序运行异常且难以定位问题。当然有时候,也能够正常运行,但我们不能够抱有侥幸心理。因此在实际编程中,尽量选择那些更加安全的函数来避免缓冲区溢出。而有些时候并不一定有更加安全的函数可替代,这种时候,我们需要自己特别关注。

思考

以下代码有什么问题:

1
2
char buff[10];
strcpy(buff,"0123456789");

守望 wechat
关注公众号[编程珠玑]获取更多原创技术文章
出入相友,守望相助!