就溢出而言,在哪些情况下 scanf 是安全的?在哪些情况下必须用另一个函数(例如 fgets)替换它?

In which cases is scanf safe in terms of overflow? And in which cases must it necessarily be replaced by another function such as fgets?

通常说scanf不是安全函数。 Clang 和 GCC 不会发出任何警告,但 MSVC 甚至不会编译(除非你包含 _CRT_SECURE_NO_WARNINGS):

Error C4996 'scanf': This function or variable may be unsafe.

另一个读取数据的函数是gets

通常建议使用 fgets.

作为 scanfgets 的替代方案

用于字符串处理的函数 scanfgets

scanf function is safe for string processing因为有一个特定的字段来分隔字符串的长度。这在以下示例中显示。

// Example 01   
#include <stdio.h>
#define SIZE 7

int main(void)
{
    char city[SIZE];
    printf("Insert the name of your city: ");    // Columbus
    scanf("%6s", city);
    printf("The city is: %s", city);             // Columb

    return 0;
}

/* ## Output ##
 * Insert the name of your city: Columbus
 * The city is: Columb
 */

如果用户输入城市名称 Columbus,输入 buffer overflow will not occur, since scanf will limit itself to trying to store only the first 6 characters of the string in city, according to the %6s instruction (in addition to inserting at the end [=26=], which is the null character: reference)。因此,当结果显示在屏幕上时,它显示 Columb 作为城市名称。

缺点是字符串长度限制不能作为参数直接输入,不像printf附件.

中有更多详细信息

也可以通过函数gets进行字符串处理。但是,gets 没有任何定界符字段,并且会一直读取直到找到换行符或文件结尾 (EOF)。为 gets 重写前面的示例:

// Example 02   
#include <stdio.h>
#define SIZE 7

int main(void)
{
    char city[SIZE];
    printf("Insert the name of the city: ");    // Columbus
    gets(city);
    printf("The city is: %s", city);            // ???

    return 0;
}

/* ## Possible Output ##
 * Insert the name of the city: Columbus
 * The city is: Columbus
 */

Gets试图在city中存储完整的字符串,这是不可能的,毕竟city不支持8个字符的字符串。在测试的情况下,gets侵入了相邻的内存地址,将无法存储的字符串部分写入city,导致缓冲区溢出。如果一个字符串足够长,预计除了缓冲区溢出,还会导致段错误(更多详情here and here). Buffer overflow is one of the main vulnerabilities exploited by hackers and therefore special attention should be paid to this issue (video: buffer overflow attack. Text: Buffer Overflow Exploitation). Thus, due to the lack of a field that defines the length of the string to be stored, it is impossible to read strings safely with gets (and reading strings is the only function of gets). Therefore, gets should never be used and has been completely removed from the language as of C11.

算术数据处理函数scanffgets

除了字符串,scanf 还读取算术数据(整数和浮点值)。但是,for this case, scanf is not safe, and there is no guarantee protection against undefined behavior. The following code illustrates this. Since the C standard specifies only the absolute minimum value of integer typesLONG_MINLONG_MAX的值都是implementation-dependent,但强制要求LONG_MIN <= -2147483647LONG_MAX >= +2147483647).

// Example 03    
#include <stdio.h>
#include <limits.h>
#include <errno.h>

#define SIZE 100

int main(void) {

    long a;
    char buffer[SIZE];

    printf("Enter a number: ");            // 2147483648 (LONG_MAX + 1)
    int success = scanf("%ld", &a);
    printf("a = %ld", a);
    getchar();

    printf("\nEnter a number: ");          // 2147483648
    fgets(buffer, SIZE, stdin);
    long b = strtol(buffer, NULL, 10);
    if (b == LONG_MAX && errno == ERANGE) {
        printf("b: Overflow!\n");
    }
    else if (b == LONG_MIN && errno == ERANGE) {
        printf("b: Underflow!\n");
    }
    printf("b = %ld", b);

    return 0;
}

/* ## Possible Output ##
 * Enter a number:  2147483648
 * a = -2147483648
 * Enter a number: 2147483648
 * b: Overflow!
 * b = 2147483647
 */

用户输入了一个足够大的数字,long 无法存储(阅读注意)。 Scanf读取数和returns 1(scanf的return表示赋值成功的个数)。但是,溢出会发生,并且根据 C 标准整数溢出会导致未定义的行为。在用示例执行的测试中,变量 a 被赋值为 -2147483648。这表明存在所谓的 . However, 。可以通过设法对读取的值施加限制来缓解这种情况。考虑 long 其中 LONG_MAX+2147483647,可以通过写 scanf("%9ld", number) 来施加限制。请注意,具有 10 位数字的值 (%10ld) 已经为溢出留出了空间 (+9 999 999 999 > +2 147 483 647)。然而,施加 9 位数字的限制,发生的情况是存在一个有效数字范围(long 能够存储),但代码排除了这种可能性。另一方面,fgets 提供保护。首先,在代码中,fgets(buffer, SIZE, stdin) 限制读取的值,防止缓冲区溢出的发生,这可能很关键。接下来,strtol 执行到 long 的转换:long b = strtol(buffer, NULL, 10)。无法将值存储在 long 类型中,因此 strtol:

  1. Returns可能的最大整数:LONG_MAX。这样就避免了变量b.
  2. 溢出的发生
  3. 设置errno flag to ERANGE指示发生错误,特别是处理过大量值的值。

值得注意的是 ,防止在 scanf 中采用类似的策略。

注意fgets逻辑是安全的。即使尝试进行 overflow-based 攻击,所有行为都已明确定义。不会有缓冲区溢出,变量b也不会整数溢出,必然在有效范围内

如果是float类型,情况就比较微妙了。根据 IEEE 754,如果数字太大而无法存储在 float 类型中,则必须为变量分配特殊值 inf-inf(IEEE 754 主题 7.4 并涵盖 here [topic 2 Overflow and underflow] and here [主题:2.3.2 溢出])。但是,这不在 C 标准中。因此,编译器可能会或可能不会重现 IEEE 754 中描述的行为。如果符合 IEEE 754,scanf 的行为在读取过大数字时将分配给类型 float 特殊值 inf-inf。这是定义的行为,从这个意义上说,是安全的。下面的代码对此进行了说明。

// Example 04
#include <stdio.h>
#include <math.h>

int main(void) {

    float a;
    printf("Enter a number: ");               // 2E40
    int success = scanf("%f", &a);            // 1

    if (isinf(a)) {
        printf("Underflow or Overflow!\n");   // Underflow or Overflow!
    }
    printf("a = %f", a);                      // inf

    return 0;
}

/* ## Possible Output ##
 * Enter a number 2E40
 * Underflow or Overflow!
 * a = inf
 */

此代码已经在 MSVC、Clang、GCC 和 TCC 上进行了测试。在所有情况下,被分配给变量a的特殊值inf。但是,C 编译器不需要遵守 IEEE 754,因此 scanf 对于存储浮点数是不安全的。

另一方面,fgets 策略涉及两个顺序操作:

  1. fgets 将读取的值作为字符串存储在字符数组中
  2. strtof 将字符串转换为浮点数

第一个操作是安全的,如示例 3 所示。第二个操作也是安全的,因为它的行为由 C standard. If the value converted by strtof is outside the valid range, then HUGE_VALF is returned (reference) 决定。有了这个,就有了定义行为的确定性。

因此,fgets策略对浮点值的处理是安全的。下面的代码是示例 4 的 fgets 版本。

// Example 05
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#define SIZE 50    

int main(void) {

    char buffer[SIZE];
    float a;
    printf("Enter a number: ");           // 2E40        
    fgets(buffer, SIZE, stdin);
    buffer[strcspn(buffer, "\n")] = 0;    // remove '\n'    
    a = strtof(buffer, NULL);

    if (isinf(a)) {
        printf("Underflow or Overflow!\n");
    }
    printf("a = %f", a);                  // +inf

    return 0;
}

/* ## Output ##
 * Enter a number: 2E40
 * Underflow or Overflow!
 * a = inf
 */

p 函数 fgets处理字符串

除了算术数据处理,fgets还可以用于字符串处理,作为scanf的替代。 scanf 的第一个示例可以适用于 fgets.

的替代版本
// Example 06
#include <stdio.h>
#define SIZE 7

int main(void) {
    char city[SIZE];
    printf("Insert the name of the city: ");   // Columbus
    fgets(city, SIZE, stdin);
    printf("The city is: %s", city);           // Columb

    return 0;
}

/* ## Output ##
 * Insert the name of the city: Columbus
 * The city is: Columb
 */

scanf一样,fgets也提供缓冲区溢出保护处理字符串。但是,与 scanf 不同,在 fgets 中,读取字符数的最大值可以作为参数直接插入,在本例中是通过 SIZE 插入的(更多信息在 附件).

附件

使用 printf 可以通过参数插入分隔符字段的值。以下示例说明了这一点:

// Example 07
#include <stdio.h>
#define SIZE 6

int main(void) {

    char country[20] = "Canada";
    printf("%.*s \n", SIZE, country);    // Canada
    printf("%.6s \n", country);          // Canada

    return 0;
}

/* ## Output ##
 * Canada
 * Canada
 */

对于scanf,唯一的直接策略类似于第二个printf。这是一个缺点,因为与“硬编码”策略不同,通过参数的分隔符字段允许轻松处理值来自的情况:

  1. 来自另一个文件的变量
  2. 一个user-entered参数

而且多个printf.

使用还是很方便的

注意scanf可以通过andargument接收分隔符字段的值,但不能直接接收。详情 here and here.

备注

缓冲区溢出可以定义为入侵不属于变量的内存区域。在整数溢出(或浮动溢出)中,这种入侵不一定会发生。当提到这种类型的溢出时,它被暗示为 尝试 将值赋给一个变量,该变量由于其大小过大而无法存储这样的值。对于整数溢出,这会配置未定义的行为,这可能会导致缓冲区溢出(内存入侵)、回绕等。

附加主题:

scanfgetsfgets 处理字符串的特殊性

函数 scanf 的默认行为是在第一个 whitespace found (reference) 处停止阅读。但是,whitespace 留在输入缓冲区中。所以下面的代码是不正确的:

// Example 08
#include <stdio.h>
#define SIZE 20

int main() {

    char city[SIZE];
    char state[SIZE];

    printf("City: ");            // Columbus  
    scanf("%19s", city);
    printf("State: ");
    fgets(state, 20, stdin);

    return 0;
}

/* ## Possible Output ##
 * City: Columbus
 * State:
 */

发生的事情是 scanf 读取用户输入的城市并留在输入缓冲区 \n 中。这样,fgets 读取输入缓冲区的其余部分 (\n) 并将其存储在变量 state 中。结果,用户无法输入 state。要更正此代码,需要在每个 scanf 之后插入 getchar。但是,如果用户为变量 city 输入以下序列,此保护将失败:Columbus space enter。程序 returns 针对最初的问题:getchar 将从缓冲区中删除 space,但换行符将保留在缓冲区中。解决此问题的另一种方法是将 getchar 替换为 while ((c = getchar()) != EOF && c != '\n')。这将从输入缓冲区中清除 scanf 处理的最后一个字符之后的所有内容,直到找到换行符或文件末尾。所以对于这个解决方案,每个 getchar 将被替换为:

int c;
while ((c = getchar()) != EOF && c != '\n');

对于这两种情况,fgets 已经在本质上提供了必要的保护。这是因为 fgets 只有在找到换行符、文件末尾或达到最大字符数时才停止读取,以先到者为准 (reference). In this case, the newline is what happens first and it is included in the associated variable (in this case, city or state) and removed from the input buffer. Similarly, gets reads until a newline or the end of the file is encountered. If a new line is found, it is included in the associated variable (reference)。

最后,介绍scanf和fgets的几个特点here

结论

Does this mean that in some cases scanf is guarantee to not overflow and in others not?

是的。 scanf 可以安全地执行字符串处理。但是,在处理整数或浮点值时,无法保证。

Are there cases where gets can be used safely or should it be avoided altogether?

应该完全避免。 gets 函数仅在对 stdin 施加限制的环境中是安全的,这是一个非常特殊的情况。

How is fgets more secure?

函数 fgets 为字符串和算术数据处理(整数和浮点数)提供安全性。