【问题标题】:How to prevent scanf causing a buffer overflow in C?如何防止scanf在C中导致缓冲区溢出?
【发布时间】:2010-12-09 22:25:17
【问题描述】:

我使用这个代码:

while ( scanf("%s", buf) == 1 ){

什么是防止可能的缓冲区溢出的最佳方法,以便可以传递随机长度的字符串?

我知道我可以通过调用例如来限制输入字符串:

while ( scanf("%20s", buf) == 1 ){

但我更希望能够处理用户输入的任何内容。 或者这不能使用 scanf 安全地完成,我应该使用 fgets?

【问题讨论】:

    标签: c scanf buffer-overflow


    【解决方案1】:

    在他们的书The Practice of Programming(非常值得一读)中,Kernighan 和 Pike 讨论了这个问题,他们通过使用snprintf() 创建具有正确缓冲区大小的字符串以传递给scanf() 系列来解决这个问题的功能。效果:

    int scanner(const char *data, char *buffer, size_t buflen)
    {
        char format[32];
        if (buflen == 0)
            return 0;
        snprintf(format, sizeof(format), "%%%ds", (int)(buflen-1));
        return sscanf(data, format, buffer);
    }
    

    请注意,这仍将输入限制为作为“缓冲区”提供的大小。如果您需要更多空间,则必须进行内存分配,或者使用非标准库函数为您分配内存。


    请注意,scanf() 系列函数的 POSIX 2008 (2013) 版本支持字符串输入(%s%c%[)的格式修饰符 m(分配分配字符) .它不采用char * 参数,而是采用char ** 参数,并为其读取的值分配必要的空间:

    char *buffer = 0;
    if (sscanf(data, "%ms", &buffer) == 1)
    {
        printf("String is: <<%s>>\n", buffer);
        free(buffer);
    }
    

    如果sscanf() 函数未能满足所有转换规范,则它为%ms-like 转换分配的所有内存在函数返回之前被释放。

    【讨论】:

    • @Sam:是的,应该是buflen-1 — 谢谢。然后您必须担心无符号下溢(包装到相当大的数字),因此需要 if 测试。我非常想用assert() 替换它,或者在开发过程中触发的if 之前用assert() 备份它,如果有人不小心将0 作为大小传递。我没有仔细查看文档以了解 %0ssscanf() 的含义——测试可能比 if (buflen &lt; 2) 更好。
    • 所以snprintf 将一些数据写入字符串缓冲区,sscanf 从创建的字符串中读取。这在哪里替换了scanf,因为它从标准输入中读取?
    • 您在结果字符串中使用“格式”一词并因此将“格式”作为第一个参数传递给snprintf,这也很令人困惑,但它不是实际的格式参数。跨度>
    • @krb686:编写此代码以便要扫描的数据在参数data 中,因此sscanf() 是合适的。如果您想改为从标准输入中读取,请删除 data 参数并改为调用 scanf()。至于在对sscanf() 的调用中成为格式字符串的变量名称format 的选择,如果您愿意,您有权对其进行重命名,但其名称并非不准确。我不确定哪种替代方案有意义; in_format 会更清楚吗?我不打算在此代码中更改它;如果你在自己的代码中使用这个想法,你可能会。
    • @mabraham:在 macOS Sierra 10.12.5(截至 2017 年 6 月 6 日)下仍然如此——macOS 上的 scanf() 没有记录为支持 %ms,尽管它会很有用是。
    【解决方案2】:

    如果您使用 gcc,您可以使用 GNU 扩展 a 说明符让 scanf() 为您分配内存来保存输入:

    int main()
    {
      char *str = NULL;
    
      scanf ("%as", &str);
      if (str) {
          printf("\"%s\"\n", str);
          free(str);
      }
      return 0;
    }
    

    编辑:正如 Jonathan 指出的,您应该查阅 scanf 手册页,因为说明符可能不同 (%m),并且您可能需要在编译时启用某些定义。

    【讨论】:

    • 这更多的是使用 glibc(GNU C 库)而不是使用 GNU C 编译器。
    • 请注意,POSIX 2008 标准提供了 m 修饰符来完成相同的工作。见scanf()。您需要检查您使用的系统是否支持此修饰符。
    • GNU(无论如何都可以在 Ubuntu 13.10 上找到)支持%ms。符号%a%f 的同义词(在输出时,它请求十六进制浮点数据)。 scanf() 的 GNU 手册页说:_ 如果程序使用 gcc -std=c99 或 gcc -D_ISOC99_SOURCE 编译,则它不可用(除非还指定了 _GNU_SOURCE),在这种情况下 a 被解释为说明符对于浮点数(见上文)。_
    【解决方案3】:

    大多数时候,fgetssscanf 的组合可以完成这项工作。如果输入格式正确,另一件事是编写自己的解析器。另请注意,您的第二个示例需要进行一些修改才能安全使用:

    #define LENGTH          42
    #define str(x)          # x
    #define xstr(x)         str(x)
    
    /* ... */ 
    int nc = scanf("%"xstr(LENGTH)"[^\n]%*[^\n]", array); 
    

    上面丢弃输入流直到但不包括换行符(\n)字符。您需要添加 getchar() 才能使用它。还要检查你是否到达了流的末尾:

    if (!feof(stdin)) { ...
    

    就是这样。

    【讨论】:

    • 您能否将feof 代码放到更大的上下文中?我问,因为该功能经常被错误地使用。
    • array 必须是 char array[LENGTH+1];
    • 在没有任何上下文或解释的情况下提出臭名昭著的 !feof 模式并且 5 年内未修复它,投反对票。
    【解决方案4】:

    直接使用scanf(3) 及其变体会带来许多问题。通常,用户和非交互式用例是根据输入行定义的。很少看到这样的情况,如果没有找到足够的对象,更多的行会解决问题,但这是 scanf 的默认模式。 (如果用户不知道在第一行输入数字,那么第二行和第三行可能无济于事。)

    至少如果您fgets(3) 知道您的程序需要多少输入行,并且您不会有任何缓冲区溢出...

    【讨论】:

      【解决方案5】:

      限制输入的长度肯定更容易。您可以通过使用循环来接受任意长的输入,一次读取一点,根据需要为字符串重新分配空间......

      但这需要大量工作,因此大多数 C 程序员只是将输入截断为任意长度。我想你已经知道了,但是使用 fgets() 不会让你接受任意数量的文本 - 你仍然需要设置一个限制。

      【讨论】:

      • 那么有谁知道如何用 scanf 做到这一点?
      • 在循环中使用 fget 可以让您接受任意数量的文本 - 只需保持 realloc()ing 缓冲区即可。
      【解决方案6】:

      创建一个为字符串分配所需内存的函数并不需要太多工作。 这是我前段时间写的一个小c函数,我总是用它来读取字符串。

      如果发生内存错误,它将返回读取字符串 NULL。 但请注意,您必须释放()您的字符串并始终检查它的返回值。

      #define BUFFER 32
      
      char *readString()
      {
          char *str = malloc(sizeof(char) * BUFFER), *err;
          int pos;
          for(pos = 0; str != NULL && (str[pos] = getchar()) != '\n'; pos++)
          {
              if(pos % BUFFER == BUFFER - 1)
              {
                  if((err = realloc(str, sizeof(char) * (BUFFER + pos + 1))) == NULL)
                      free(str);
                  str = err;
              }
          }
          if(str != NULL)
              str[pos] = '\0';
          return str;
      }
      

      【讨论】:

      • sizeof (char) 定义为 1。你在这里不需要它。
      • 通常最好将指针分配/释放保持在同一级别,这意味着您的函数不应自行分配内存,因为调用者必须释放它。大多数标准库/posix 函数通过返回静态字符串(如strerror(3))或期望传入的预分配字符串(如(strerror_r(3) - 或scanf(3))来遵守这一原则......跨度>
      • 这段代码无论如何都不正确。 str[pos] = getchar() 无法检查函数返回的特殊值 EOF。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-06-15
      • 1970-01-01
      • 2011-02-17
      • 1970-01-01
      相关资源
      最近更新 更多