正如其他回答中所指出的,gets()
不检查缓冲区空间。除了意外溢出问题之外,这个弱点可以被恶意用户用来制造各种混乱。
1988年发布的第一个广泛传播的蠕虫程序就利用了gets()
在互联网上进行自我传播。以下是Peter Van Der Linden在《专家C编程》一书中讨论它是如何工作的有趣摘录:
早期漏洞获取互联网蠕虫
C语言中的问题不仅限于语言本身。标准库中的一些例程具有不安全的语义。这在1988年11月通过在Internet网络上数千台计算机上爬行的蠕虫程序而得到了明显证明。当烟雾散去,调查完成时,确定蠕虫传播的其中一种方式是通过finger服务中的一个漏洞,该服务接受有关当前登录用户的网络查询。finger服务(in.fingerd)使用了标准I/O例程gets()
。
gets()
的名义任务是从数据流中读取一个字符串。调用者告诉它将输入字符放到哪里。但是gets()
不检查缓冲区空间;事实上,它无法检查缓冲区空间。如果调用者提供指向堆栈的指针,并且输入超过了缓冲区空间,gets()
将乐意覆盖堆栈。finger服务包含以下代码:
main(argc, argv)
char *argv[];
{
char line[512];
...
gets(line);
在这里,line
是一个自动分配在堆栈上的512字节数组。当用户提供给finger
守护进程的输入超过这个大小时,gets()
例程会一直将其放在堆栈上。大多数体系结构都容易被覆盖堆栈中现有条目的攻击,因为它们也覆盖了相邻的条目。在软件中检查每个堆栈访问的大小和权限的成本是禁止性的。一位有经验的恶意行为者可以通过将正确的二进制模式隐藏在参数字符串中来修改堆栈上的过程激活记录中的返回地址。这将使执行流不会返回到原来的位置,而是转到一个特殊指令序列(也小心翼翼地放置在堆栈上),该序列调用execv()
来替换运行中的图像,以获取与远程计算机上的shell通信的能力。Voilà,现在您正在与远程计算机上的shell通信,而不是与finger
守护进程通信,您可以发出命令将病毒复制到另一台计算机上。
具有讽刺意味的是,gets()
例程是一个过时的函数,它提供了与可移植I/O库的第一个版本兼容性,并在十多年前被标准I/O所取代。man页甚至强烈建议始终使用fgets()
。 fgets()
例程设置了读取字符数的限制,因此它不会超出缓冲区的大小。finger
守护进程通过两行修复变得更加安全,其替换如下:
gets(line);
按行:
if (fgets(line, sizeof(line), stdin) == NULL)
exit(1);
这个函数只能接受有限数量的输入,因此不能被程序运行者利用来覆盖重要的内存地址。然而,ANSI C标准并没有从语言中移除gets()
函数。因此,虽然这个特定程序变得更加安全了,但是C标准库中潜在的缺陷并未被修复。
fgets()
的结果:if (fgets(str, 10, stdin) != 0) { ...OK... } else { ...EOF 或错误... }
。不建议使用ferror()
或feof()
;我们刚刚修复了 AIX 上的性能 bug,该代码忽略了主要 I/O 函数(如fputs()
)的错误,并改用了ferror()
,这会导致明显的减速。 - Jonathan Lefflergets
会丢弃换行符,但fgets
会保留它。这两个函数都会在字符串结尾附加一个空终止符。 - casablanca