C 和 C++ 不能够自动地做边界检查,边界检查的代价是效率。一般来讲,C 在大多数情况下注重效率。然而,获得效率的代价是,C 程序员必须十分警觉以避免缓冲区溢出问题。
C语言标准库中的许多字符串处理和IO流读取函数是导致缓冲区溢出的罪魁祸首。我们有必要了解这些函数,在编程中多加小心。
一、字符串处理函数
1. strcpy()
strcpy()函数将源字符串复制到缓冲区。没有指定要复制字符的具体数目!如果源字符串碰巧来自用户输入,且没有专门限制其大小,则有可能会造成缓冲区溢出!
我们也可以使用strncpy来完成同样的目的:
- strncpy(dst, src, dst_size-1);
如果 src 比 dst 大,则该函数不会抛出一个错误;当达到最大尺寸时,它只是停止复制字符。注意上面调用 strncpy() 中的 -1。如果 src 比 dst 长,则那给我们留有空间,将一个空字符放在 dst 数组的末尾。
但是! strncpy()也不完全安全,也有可能把事情搞糟。即使“安全”的调用有时会留下未终止的字符串,或者会发生微妙的相差一位错误。
确保 strcpy() 不会溢出的另一种方式是,在需要它时就分配空间,确保通过在源字符串上调用 strlen() 来分配足够的空间。
- dst = (char *)malloc(strlen(src));
- strcpy(dst, src);
2. strcat()
strcat()函数非常类似于 strcpy(),除了它可以将一个字符串合并到缓冲区末尾。它也有一个类似的、更安全的替代方法 strncat()。如果可能,使用 strncat() 而不要使用 strcat()。
3. sprintf()、vsprintf()
函数 sprintf()和 vsprintf()是用来格式化文本和将其存入缓冲区的通用函数。它们可以用直接的方式模仿 strcpy() 行为。换句话说,使用 sprintf() 和 vsprintf() 与使用 strcpy() 一样,都很容易对程序造成缓冲区溢出。
sprintf() 的许多版本带有使用这种函数的更安全的方法。可以指定格式字符串本身每个自变量的精度。sprintf 采用”*”来占用一个本来需要一个指定宽度或精度的常数数字的位置,而实际的宽度或精度就可以和其它被打印的变量一样被提供出来。
例如:
- sprintf(usage, "USAGE: %*s\n", BUF_SIZE, argv[0]);
二、字符读取函数
1. gets()
永远不要使用 gets()。该函数从标准输入读入用户输入的一行文本,它在遇到 EOF 字符或换行字符之前,不会停止读入文本。也就是:gets() 根本不执行边界检查。因此,使用 gets() 总是有可能使任何缓冲区溢出。
作为一个替代方法,可以使用方法 fgets()。它可以做与 gets() 所做的同样的事情,但它接受用来限制读入字符数目的大小参数,因此,提供了一种防止缓冲区溢出的方法。
2. getchar()、fgetc()、getc()、read()
如果在循环中使用这些函数,确保检查缓冲区边界
3. scanf()系列
sscanf()、fscanf()、vfscanf()、vscanf()、vsscanf()
scanf系列的函数也设计得很差。目的地缓冲区也可能会发生溢出。
同样地,我们用设置宽度也可以解决这个问题。
4. getenv()
使用系统调用getenv() 的最大问题是您从来不能假定特殊环境变量是任何特定长度的。
三、使用安全版本的代码库
微软对于有缓冲溢出危险的API使用其开发的安全版本的库来替代。
SafeCRT自Visual Studio 2005起开始支持。当代码中使用了禁用的危险的CRT函数,Visual Studio 2005编译时会报告相应警告信息,以提醒开发人员考虑将其替代为Safe CRT中更为安全。
1. 有关字符串拷贝的API
例如:strcpy, wcscpy等
替代的Safe CRT函数:strcpy_s
2. 有关字符串合并的API
例如:strcat, wcscat等
替代的Safe CRT函数:strcat_s
3. 有关sprintf的API
例如:sprintf, swprintf等
替代的Safe CRT函数:
- _snprintf_s
- _snwprintf_s
其它被禁用的API还有scanf, strtok, gets, itoa等等。 ”n”系列的字符串处理函数,例如strncpy等,也在被禁用之列。
举个栗子
破解下面的密码防护代码:
- #include <stdio.h>int main(int argc, char *argv[])
- {
- int flag = 0;
- char passwd[10];
- memset(passwd,0,sizeof(passwd));
- strcpy(passwd, argv[1]);
- if(0 == strcmp("LinuxGeek", passwd))
- {
- flag = 1;
- }
- if(flag)
- {
- printf("\n Password cracked \n");
- }
- else {
- printf("\n Incorrect passwd \n");
- }
- return 0;
- }
如果把命令行输入的文字当作密码的话,会有很大的漏洞:
首先如果我输入11个字符且最后一个字符是大于0的话,就惨了,strcpy是要copy到’/0’的。他会一直把这11个字符都copy到passwd数组中,此时数组越界了,最后一个字符就把flag标志位个赋值了,if条件就满足了,密码就被破解了!
经过上面我们的讨论,我们可以对用户输入动态分配同样大小的空间,而不是提前分配固定的空间。
- passwd = (char *)malloc(strlen(argv[1]));
- strcpy(passwd, argv[1]);
注意:
不要用strncpy(),它会造成最后一位的丢失,造成隐藏的错误。
四、关于缓冲区溢出问题
由于函数调用栈头部会保存其调用者栈的基地址%ebp,如果破坏了存储%ebp的值,那么基址寄存器就不能正确地恢复,因此调用者就不能正确地引用它的局部变量或参数。
如果破坏了存储的返回地址,那么ret指令会使程序跳转到完全意想不到的地方。
缓冲区溢出的一个更加致命的使用就是让程序执行它本来不愿意执行的函数。这是一种最常见的通过计算机网络攻击系统安全的方法。通常,输入给程序一个字符串,这个字符串包含一些可执行代码的字节编码,称为攻击代码,另外还有一些字节会用一个指向攻击代码的指针覆盖返回地址。那么,执行ret指令的效果就是跳转到攻击代码。
五、对抗缓冲区溢出攻击
1. 栈随机化
为了在系统中插入攻击代码,攻击者不但要插入代码,还要插入指向这段代码的指针,这个指针也是攻击字符串的一部分。产生这个指针需要知道这个字符串放置的栈地址。在过去,程序的栈地址非常容易预测,在不同的机器之间,栈的位置是相当固定的。
栈随机化的思想使得栈的位置在程序每次运行时都有变化。因此,即使许多机器都运行相同的代码。它们的栈地址都是不同的。
实现的方式是:程序开始时,在栈上分配一段0--n字节之间的随机大小空间。程序不使用这段空间,但是它会导致程序每次执行时后续的栈位置发生了变化。
在Linux系统中,栈随机化已经变成了标准行为。(在linux上每次运行相同的程序,其同一局部变量的地址都不相同)
2. 栈破坏检测
在C语言中,没有可靠的方法来防止对数组的越界写,但是,我们能够在发生了越界写的时候,在没有造成任何有害结果之前,尝试检测到它。
最近的GCC版本在产生的代码中加入了一种栈保护者机制,用来检测缓冲区越界,其思想是在栈中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀值。这个金丝雀值是在程序每次运行时随机产生的,因此,攻击者没有简单的办法知道它是什么。
在恢复寄存器状态和从函数返回之前,程序检查这个金丝雀值是否被该函数的某个操作或者函数调用的某个操作改变了。如果是,那么程序异常终止。
3. 限制可执行代码区域
限制那些能够存放可执行代码的存储器区域。在典型的程序中,只有保存编译器产生的代码的那部分存储器才需要是可执行的,其他部分可以被限制为只允许读和写。
现在的64位处理器的内存保护引入了”NX”(不执行)位。有了这个特性,栈可以被标记为可读和可写,但是不可执行,检查页是否可执行由硬件来完成,效率上没有损失。
【本文是51CTO专栏作者elknot的原创文章,转载请通过51CTO获取授权】