为什么获取功能如此危险以至于不应该使用它?

vin*_*rak 210 c gets fgets buffer-overflow

当我尝试编译使用gets()GCC函数的C代码时,

我明白了

警告:

(.text + 0x34):警告:`gets'函数很危险,不应该使用.

我记得这与堆栈保护和安全性有关,但我不确定为什么?

有人可以帮我删除这个警告并解释为什么会有这样的使用警告gets()

如果gets()是如此危险,为什么我们不能删除它?

Tho*_*ens 161

为了gets安全使用,您必须确切地知道要读取的字符数,以便您可以使缓冲区足够大.只有在您确切知道要阅读的数据时,您才会知道.

gets您想要使用fgets具有签名的,而不是使用

char* fgets(char *string, int length, FILE * stream);
Run Code Online (Sandbox Code Playgroud)

(fgets如果它读取整行,则会留'\n'在字符串中;您将不得不处理它.)

它仍然是该语言的官方部分,直到1999年的ISO C标准,但它已被2011标准正式删除.大多数C实现仍然支持它,但至少gcc会对使用它的任何代码发出警告.

  • 它实际上不是gcc警告的,它是glibc,其中包含`gets()`的pragma或属性,导致编译器在使用时发出警告. (72认同)
  • @fuz实际上,甚至不仅仅是编译器发出警告:OP中引用的警告是由链接器打印的! (3认同)

Jon*_*ler 145

为什么gets()危险

第一个互联网蠕虫(Morris Internet蠕虫)在大约30年前(1988-11-02)逃脱,它使用gets()缓冲区溢出作为其从系统传播到系统的方法之一.基本问题是函数不知道缓冲区有多大,所以它继续读取直到找到换行符或遇到EOF,并且可能溢出它给出的缓冲区的边界.

你应该忘记你曾经听说过gets()存在过的.

C11标准ISO/IEC 9899:2011 gets()作为标准功能被淘汰,即A Good Thing™(在ISO/IEC 9899:1999/Cor.3:2007中正式标记为'过时'和'弃用' - 技术勘误3表示C99,然后在C11中删除).可悲的是,由于向后兼容的原因,它将在图书馆中存在多年(意为"数十年").如果由我决定,实施gets()将成为:

char *gets(char *buffer)
{
    assert(buffer != 0);
    abort();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

鉴于你的代码无论如何都迟早会崩溃,最好不要迟早解决问题.我准备添加一条错误消息:

fputs("obsolete and dangerous function gets() called\n", stderr);
Run Code Online (Sandbox Code Playgroud)

现代版本的Linux编译系统会在您链接时生成警告gets()- 以及其他一些也存在安全问题的函数(mktemp(),...).

替代品 gets()

与fgets()

正如其他人所说,规范替代方案gets()fgets()指定stdin为文件流.

char buffer[BUFSIZ];

while (fgets(buffer, sizeof(buffer), stdin) != 0)
{
    ...process line of data...
}
Run Code Online (Sandbox Code Playgroud)

还没有人提到的是,gets()不包括换行符,但fgets()确实如此.因此,您可能需要使用包装器fgets()来删除换行符:

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
    if (fgets(buffer, buflen, fp) != 0)
    {
        size_t len = strlen(buffer);
        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';
        return buffer;
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

或更好:

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
    if (fgets(buffer, buflen, fp) != 0)
    {
        buffer[strcspn(buffer, "\n")] = '\0';
        return buffer;
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

此外,如咖啡厅指出了评论和paxdiablo显示了他的答案,与fgets()你可能有一行遗留数据.我的包装器代码留下了下次要读取的数据; 如果您愿意,可以随时修改它以吞噬剩余的数据行:

        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';
        else
        {
             int ch;
             while ((ch = getc(fp)) != EOF && ch != '\n')
                 ;
        }
Run Code Online (Sandbox Code Playgroud)

剩下的问题是如何报告三种不同的结果状态 - EOF或错误,行读取和未截断,以及部分行读取但数据被截断.

这个问题不会出现,gets()因为它不知道你的缓冲区在哪里结束并快速地践踏到最后,对你漂亮的内存布局造成严重破坏,如果缓冲区被分配,通常会搞乱返回堆栈(Stack Overflow)如果缓冲区是动态分配的,则堆栈或践踏控制信息,或者如果缓冲区是静态分配的,则通过其他宝贵的全局(或模块)变量复制数据.这些都不是一个好主意 - 它们集中体现了"未定义的行为"这一短语.


还有TR 24731-1(C标准委员会的技术报告),它提供了各种功能的更安全的替代方案,包括gets():

§6.5.4.1 gets_s功能

概要

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);
Run Code Online (Sandbox Code Playgroud)

运行约束

s不应该是空指针.n不得等于零,也不得大于RSIZE_MAX.读取n-1字符时,应出现换行符,文件结束符或读取错误 stdin.25)

3如果存在运行时约束违规,s[0]则将其设置为空字符,并读取和丢弃字符,stdin直到读取换行符,或发生文件结束或读取错误.

描述

4该gets_s函数最多读取一个小于n 指向的流所指定的字符数,该数字指向stdin的数组s.在换行符(被丢弃)之后或文件结束之后,不会读取其他字符.丢弃的换行符不计入读取的字符数.在读入数组的最后一个字符后立即写入空字符.

5如果遇到文件结尾且没有字符读入数组,或者在操作期间发生读取错误,则s[0]设置为空字符,其他元素s采用未指定的值.

推荐做法

6该fgets函数允许正确编写的程序安全地处理输入行太长而无法存储在结果数组中.通常,这要求呼叫者fgets注意结果数组中是否存在换行符.考虑使用fgets(以及基于换行符的任何所需处理)而不是 gets_s.

25)gets_s函数不同gets,该函数使得一行输入的运行时约束违反溢出缓冲区以存储它.不同的是fgets,gets_s在输入行和成功调用之间保持一对一的关系gets_s.使用gets期望这种关系的程序.

Microsoft Visual Studio编译器实现了TR 24731-1标准的近似,但Microsoft实现的签名与TR中的签名之间存在差异.

C11标准ISO/IEC 9899-2011包括附件K中的TR24731作为库的可选部分.不幸的是,它很少在类Unix系统上实现.


getline() - POSIX

POSIX 2008还提供了一个安全的替代gets()getline().它动态地为该行分配空间,因此您最终需要释放它.因此,它消除了线路长度的限制.它还返回读取的数据的长度,或-1(而不是EOF!),这意味着可以可靠地处理输入中的空字节.还有一个名为"选择你自己的单字符分隔符"的变体getdelim(); 例如,如果要处理find -print0文件名末尾用ASCII NUL '\0'字符标记的输出,这可能很有用.

  • 值得指出的是,`fgets()`和你的`fgets_wrapper()`版本将在输入缓冲区中留下过长行的尾部,由下一个输入函数读取.在许多情况下,您需要读取并丢弃这些字符. (5认同)
  • "忘记你曾经听说过'gets()`存在." 当我这样做时,我再次遇到它并回到这里.你是否正在攻击stackoverflow以获得upvotes? (5认同)
  • 我想知道为什么他们没有添加一个fgets()替代方案,允许一个人使用它的功能,而不必进行愚蠢的strlen调用.例如,返回读入字符串的字节数的fgets变量可以使代码轻松查看读取的最后一个字节是否为换行符.如果为缓冲区传递空指针的行为被定义为"读取并丢弃最多n-1个字节,直到下一个换行符",这将允许代码轻松地丢弃超长行的尾部. (4认同)
  • @supercat:其他问题`与fgets()`是,如果该文件包含一个空字节,你不能告诉有多少数据是空字节达线(或EOF)结束后.`strlen()`只能报告数据中的空字节; 在那之后,这是猜测,因此几乎肯定是错误的. (3认同)
  • @supercat:是的,我同意 - 很可惜.最近的方法可能是POSIX [`getline()`](http://pubs.opengroup.org/onlinepubs/9699919799/functions/getline.html)及其相对的`getdelim()`,它确实返回长度由命令读取的'line',根据需要分配空间以便能够存储整行.如果你最终得到一个大小为几千兆字节的单行JSON文件,那么即使这样也会导致问题; 你能承受所有的记忆吗?(当我们在它的时候,我们可以使用`strcpy()`和`strcat()`变体来返回指向结尾的空字节的指针吗?等等.) (2认同)

Jac*_*ack 21

因为getsstdin获取字节并将它们放在某处时不进行任何类型的检查.一个简单的例子:

char array1[] = "12345";
char array2[] = "67890";

gets(array1);
Run Code Online (Sandbox Code Playgroud)

现在,首先你可以输入你想要多少个字符,gets不关心它.其次,超过你放置它们的数组大小的字节(在这种情况下array1)将覆盖它们在内存中找到的任何内容,因为gets它们会写入它们.在前面的示例中,这意味着如果您输入"abcdefghijklmnopqrts"可能,不可预测,它也会覆盖array2或者其他任何内容.

该函数不安全,因为它假定输入一致.永远不要用它!

  • 使`gets`完全无法使用的原因是它没有一个数组长度/计数参数; 如果它在那里,它只是另一个普通的C标准功能. (3认同)
  • 顾名思义,@supercat `gets` 旨在从 `stdin` 获取字符串,但是没有 _size_ 参数的理由可能来自 C_ 的精神_:相信程序员。此函数在 _C11_ 中被删除,给定的替换 [`gets_s`](http://en.cppreference.com/w/c/io/gets) 接受输入缓冲区的大小。不过,我对 `fgets` 部分一无所知。 (2认同)

pax*_*blo 16

你不应该使用,gets因为它无法阻止缓冲区溢出.如果用户输入的数据多于缓冲区中可容纳的数据,则很可能最终导致损坏或更糟.

实际上,ISO实际上采取了从C标准中删除 的步骤(gets从C11开始,尽管它在C99中已被弃用),考虑到它们对向后兼容性的评价程度,它应该表明该函数有多糟糕.

正确的做法是将fgets函数与stdin文件句柄一起使用,因为您可以限制从用户读取的字符.

但是这也有它的问题,例如:

  • 用户输入的额外字符将在下次拍摄时被拾取.
  • 没有快速通知用户输入太多数据.

为此,在他们职业生涯的某个阶段几乎每个C编码员都会写一个更有用的包装器fgets.这是我的:

#include <stdio.h>
#include <string.h>

#define OK       0
#define NO_INPUT 1
#define TOO_LONG 2
static int getLine (char *prmpt, char *buff, size_t sz) {
    int ch, extra;

    // Get line with buffer overrun protection.
    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }
    if (fgets (buff, sz, stdin) == NULL)
        return NO_INPUT;

    // If it was too long, there'll be no newline. In that case, we flush
    // to end of line so that excess doesn't affect the next call.
    if (buff[strlen(buff)-1] != '\n') {
        extra = 0;
        while (((ch = getchar()) != '\n') && (ch != EOF))
            extra = 1;
        return (extra == 1) ? TOO_LONG : OK;
    }

    // Otherwise remove newline and give string back to caller.
    buff[strlen(buff)-1] = '\0';
    return OK;
}
Run Code Online (Sandbox Code Playgroud)

一些测试代码:

// Test program for getLine().

int main (void) {
    int rc;
    char buff[10];

    rc = getLine ("Enter string> ", buff, sizeof(buff));
    if (rc == NO_INPUT) {
        printf ("No input\n");
        return 1;
    }

    if (rc == TOO_LONG) {
        printf ("Input too long\n");
        return 1;
    }

    printf ("OK [%s]\n", buff);

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

它提供了相同的保护fgets,因为它可以防止缓冲区溢出,但它也会通知调用者发生了什么,并清除多余的字符,这样它们就不会影响您的下一个输入操作.

随意按照您的意愿使用它,我特此在"做你真该想要的"许可下发布它:-)


Thi*_*ira 12

fgets.

从stdin读取:

char string[512];

fgets(string, sizeof(string), stdin); /* no buffer overflows here, you're safe! */
Run Code Online (Sandbox Code Playgroud)

  • 更好的是:`fgets(string,sizeof string,stdin);` (9认同)

Ger*_*ima 6

在不破坏API的情况下,您无法删除API函数.如果愿意,许多应用程序将不再编译或运行.

这是一个参考文献给出的原因:

读取溢出s指向的数组的行会导致未定义的行为.建议使用fgets().


pmg*_*pmg 5

我最近在USENET 的comp.lang.c一篇文章中读到,它gets()正在从标准中删除。呜呼

你会很高兴知道委员会刚刚投票(结果一致)也从草案中删除了 gets() 。

  • 从标准中删除它真是太好了。但是,由于向后兼容性,大多数实现将至少在未来 20 年内将其作为“现在的非标准扩展”提供。 (3认同)
  • 是的,没错,但是当您使用 `gcc -std=c2012 -pedantic ...` 进行编译时, gets() 将无法通过。(我刚刚编写了`-std`参数) (2认同)

Yu *_*Hao 5

在 C11(ISO/IEC 9899:201x) 中,gets()已删除。(在 ISO/IEC 9899:1999/Cor.3:2007(E) 中已弃用)

此外fgets(),C11 引入了一个新的安全替代方案gets_s()

C11 K.3.5.4.1gets_s功能

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);
Run Code Online (Sandbox Code Playgroud)

但是,在推荐实践部分,fgets()仍然是首选。

fgets函数允许正确编写的程序安全地处理太长而无法存储在结果数组中的输入行。通常,这要求调用者fgets注意结果数组中是否存在换行符。考虑使用fgets(以及任何需要的基于换行符的处理)而不是 gets_s.


小智 5

gets()很危险,因为用户可能会在提示中输入太多内容而导致程序崩溃。它无法检测到可用内存的末尾,因此如果为此目的分配的内存量太小,可能会导致段错误和崩溃。有时,用户将 1000 个字母键入一个人名的提示中似乎不太可能,但作为程序员,我们需要使我们的程序防弹。(如果用户发送过多数据可能导致系统程序崩溃,也可能存在安全风险)。

fgets() 允许您指定从标准输入缓冲区中取出多少个字符,这样它们就不会溢出变量。