1 为什么gets()函数还在我们的代码中?

好吧,最终还是发生了。我们遇到了一个非常严重,并且非常普遍的缓冲区溢出问题。这个问题造成了非常大的影响,修复这个问题的过程,将会非常艰难,非常 慢,代价非常高。在我看来,可能在这个世界上,会有不少软件产品经理这样问程序员们:“为什么你没有警告过我?”,估计这些被问到的程序员中,有很多都会 直接回答道:“我警告过你了,你什么没有听进去?“

在软件开发的过程中,总是存在一个矛盾:正确的解决问题和快速的解决问题。这个问题在安全领域更加的突出。因此在接下来的几周时间里,我们来聊聊这个矛盾。这个矛盾的如下两个方面,在我们聊的过程中相当的重要:

    不管你针对问题的解决方案有多完美,如果没有人使用这个解决方案,都是无用功
    不管是处于什么目的,如果你没有使用完美解决方案,那所有的考虑都是白费功夫。因为你的代码里没有实现该解决方案

让我们从这个看起来非常俗气的例子开始吧:C标准库中的 gets() 函数。 这个函数的定义如下:

char * gets ( char * str );

gets() 函数的形参只有一个指针。它会从标准输入流中读字符到一块连续的内存地址空间中。这块地址空间的开始位置就是指针 str 指向的位置。当在输入流中遇到文件结束符( EOF )或者换行符(n)时,读取操作结束。当读入换行符(n)时,该字符不会被放入那块连续的地址空间中。在读取结束时, gets() 会自动在内存空间的末尾追加一个 NULL 字符。经过上述这些操作,对于程序员来说,这个函数得到的就是从标准输入进来的,以 NULL 字符结尾的C字符串。如果读入的字符流是一整行的话,行尾的换行符将会被舍去。


这个函数方便,也有局限性。 C程序员们经常使用它读取标准输入。下面的代码是一种典型的应用场景:

 代码如下 复制代码

char input[100];
printf("Yes or no?n");
gets(input);
/* and so on… */



在过去的30年里,许多C编程社区的同仁们都已经意识到 gets() 函数不安全,而且在保证接口不变的情况下,也无法被改良。原因也比较直观,这个函数只有一个指针作为参数,该指针指向的内存空间将用于保存读入数据。但是 gets() 函数无法知道它需要使用多大的内存空间。如果在标准输入中读入足够长的,不包含换行符的字符留, gets() 函数肯定会覆盖掉指定的内存区域,而程序员对此无能为力。

此外,除了 gets() 函数缺乏安全性,还有它的小伙伴 fgets() 也有问题。 这个函数的原型如下:

 代码如下 复制代码

char * fgets ( char * str, int num, FILE * stream );



str 是一个指针,指向一块内存区域,读入的数据将会存储到这块内存空间。num 是一个整数,指定了内存空间的大小, stream 是一个文件指针,指定了可以从哪里读取。可能第一眼看过去,你会和我当时一样,觉得前面的那段不安全代码,可以使用 fgets() 函数重写,来避免遇到缓冲区溢出的问题。

 代码如下 复制代码

char input[100];
printf("Yes or no?n");
fgets(input, 100, stdin);
/* and so on… */



不过, gets() 函数和 fgets() 函数有个不同点。fgets() 函数会在遇到换行符时停止,并且其保存到内存中的数据会包含该换行符,而 gets() 函数会排除换行符。因此,就简单的这么重写代码无法实现完全同等的功能。而为了保证代码安全,又实现完全相同的功能,我们就需要检查内存地址中的字符,如 果在结尾有换行符,就将其删除。
因此我们可以用拍脑袋的方式, 得到下面的代码。这段代码既安全,又能保证和 gets() 函数的行为相同。

 代码如下 复制代码

/* This code doesn't work! */
char input[100];
printf("Yes or no?n");
fgets(input, 100, stdin);
char *last = input + strlen(input) – 1;
if (*last == 'n')
      *last = '';
/* and so on… */



可是,虽然代码变复杂了,但是还是存在一个隐藏问题,该问题会导致程序崩溃,或者有安全隐患。当程序执行时,如果标准输入流已经得到了所有可用的字符,但 是还没有遇到文件结束符( EOF), fgets() 函数将会通过将 input[0] 标记为 NULL 字符的形式,直接返回一个 NULL 字符串。此时, strlen(intput) 的返回值为0, 因此导致 last 指针指向 input 数组之前的那个字符。因为不能确定这个字符到底是什么,这段代码的行为将因此无法判断。

做个随堂小练习吧, 请自行修复一下这段代码。 点击这里查看修复方法

在我过去工作过的一家公司里,曾经的经理是一个对安全非常敏感的人,他要求 gets() 函数从所有本地的C库中移除。这个要求,就导致我们经常需要重写从其他地方拿到的代码。所以有下面这段对话,也就不足为奇了。

    A:你发给我的那段代码,你看了吗?我们需要重写里面的部分代码,去掉对 gets() 函数的调用
    B:为什么 gets() 函数不能出现在代码中?
    A:<长篇大论的解释>此处忽略5421个字
    B:哈,有意思
    A:如果你需要的话,我们很乐意发给你修改后的代码
    B: 好的,我很乐意,发给我吧。不过现在我能告诉你的是,我们暂时还不能做什么,因为我们只能在客户发现并报告此问题的情况下,才能修改代码。

虽然 gets() 函数早就被公认为不安全的,但是它仍然存在于 C89 和 C99 标准,并最终在 C2011 标准中移除了。但这仅仅是在语言标准中的移除,当我检查自己的一些代码时,发现仍有地方用到了它。而且以我目前对C的了解,更有意思的是,目前在C语言库中,还没有一个安全并且方便的取代 gets() 函数的方法。

各位通读了文章的朋友,能否回答如下几个问题:

    在读此文之前,你知道 gets() 函数是不安全的吗?
    你所工作的地方,有限制使用 gets() 函数的相关规定吗?
    你曾经冲写过代码来避免使用 gets() 函数吗?
    关于 gets() 函数,你有什么想了解的吗?

请下周继续关注此讨论。

2 实战:如何解决 gets() 函数的安全问题


2.1 工具链的安全警告

目前GCC默认就会为包含对 gets() 函数调用的代码,报出警告信息。

比如下面的代码:

 代码如下 复制代码
#include<stdio.h>
int main(void)
{
  char c[5];
  gets(c);
  puts(c);
}



就会给出下面的提示信息:

 代码如下 复制代码
gets_warn.c:(.text+0xd): warning: the `gets’ function is dangerous and should not be used.



2.2 安全的gets()实现


C11 标准(ISO/IEC 9899:201x)中, gets() 函数被删除, 引入了新的函数 gets_s().

C11 K.3.5.4.1 The gets_s function

 代码如下 复制代码
#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);



因为目前GCC中还没有完全实现此标准, 因此 gets_s() 函数尚未包含在目前的GNU 工具链中。Clang里也暂时没有增加对 gets_s 的支持。

所以最通用的做法,可能是自己实现一个。 如下是一种实现方式:

 代码如下 复制代码
char *gets_s(char * str, int num)
{
    if (fgets(str, int, stdin) != 0)
    {
        size_t len = strlen(str);
        if (len > 0 && buffer[len-1] == 'n')
            buffer[len-1] = '';
        return buffer;
    }
    return 0;
}


2.3 C标准库中其他存在安全隐患的函数

除了像 gets() 函数这类,非常不安全的函数外。C语言中因为缺少对数组越界的检查,指针的广泛使用,导致不少函数如果使用不当,容易被***利用, 存在安全隐患。

    strcpy : 建议使用 strncpy
    strcat : 建议使用 strncat
    sprintf : 建议使用 snprintf

如果你想自己实现一些字符串操作函数,那么下面这种接口设计值得推荐。即务必要规定好目标地址空间的大小:

size_t foobar(char *dest, size_t buf_size, /* operands here */)

微软在MSDN中也就如何安全的使用C语言标准库接口给出了建议,感兴趣的朋友,可以看看 https://msdn.microsoft.com/en-us/library/bb288454.aspx。