就溢出而言,在哪些情况下 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.
- 这是否意味着在某些情况下
scanf
保证不会溢出,而在其他情况下则不会?
- 如果有,具体是哪些案例?
另一个读取数据的函数是gets
- 在某些情况下
gets
可以安全使用还是应该完全避免?
通常建议使用 fgets
.
作为 scanf
和 gets
的替代方案
fgets
如何更安全?
用于字符串处理的函数 scanf
和 gets
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.
算术数据处理函数scanf
和fgets
除了字符串,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 types,LONG_MIN
和LONG_MAX
的值都是implementation-dependent,但强制要求LONG_MIN <= -2147483647
和LONG_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
:
值得注意的是 ,防止在 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
策略涉及两个顺序操作:
fgets
将读取的值作为字符串存储在字符数组中
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
。这是一个缺点,因为与“硬编码”策略不同,通过参数的分隔符字段允许轻松处理值来自的情况:
- 来自另一个文件的变量
- 一个user-entered参数
而且多个printf
.
使用还是很方便的
注意:scanf
可以通过andargument接收分隔符字段的值,但不能直接接收。详情 here and here.
备注
缓冲区溢出可以定义为入侵不属于变量的内存区域。在整数溢出(或浮动溢出)中,这种入侵不一定会发生。当提到这种类型的溢出时,它被暗示为 尝试 将值赋给一个变量,该变量由于其大小过大而无法存储这样的值。对于整数溢出,这会配置未定义的行为,这可能会导致缓冲区溢出(内存入侵)、回绕等。
附加主题:
scanf
、gets
和 fgets
处理字符串的特殊性
函数 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
为字符串和算术数据处理(整数和浮点数)提供安全性。
通常说scanf
不是安全函数。 Clang 和 GCC 不会发出任何警告,但 MSVC 甚至不会编译(除非你包含 _CRT_SECURE_NO_WARNINGS):
Error C4996 'scanf': This function or variable may be unsafe.
- 这是否意味着在某些情况下
scanf
保证不会溢出,而在其他情况下则不会? - 如果有,具体是哪些案例?
另一个读取数据的函数是gets
- 在某些情况下
gets
可以安全使用还是应该完全避免?
通常建议使用 fgets
.
scanf
和 gets
的替代方案
fgets
如何更安全?
用于字符串处理的函数 scanf
和 gets
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.
算术数据处理函数scanf
和fgets
除了字符串,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 types,LONG_MIN
和LONG_MAX
的值都是implementation-dependent,但强制要求LONG_MIN <= -2147483647
和LONG_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
。这表明存在所谓的 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
:
值得注意的是 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
策略涉及两个顺序操作:
fgets
将读取的值作为字符串存储在字符数组中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
。这是一个缺点,因为与“硬编码”策略不同,通过参数的分隔符字段允许轻松处理值来自的情况:
- 来自另一个文件的变量
- 一个user-entered参数
而且多个printf
.
注意:scanf
可以通过andargument接收分隔符字段的值,但不能直接接收。详情 here and here.
备注
缓冲区溢出可以定义为入侵不属于变量的内存区域。在整数溢出(或浮动溢出)中,这种入侵不一定会发生。当提到这种类型的溢出时,它被暗示为 尝试 将值赋给一个变量,该变量由于其大小过大而无法存储这样的值。对于整数溢出,这会配置未定义的行为,这可能会导致缓冲区溢出(内存入侵)、回绕等。
附加主题:
scanf
、gets
和 fgets
处理字符串的特殊性
函数 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
为字符串和算术数据处理(整数和浮点数)提供安全性。