C如何处理Buffer溢出?

How does C treat Buffer overflows?

我了解到在 C 语言中,有些数组可以在声明时指定长度。我想知道这些长度声明是否仅供其他程序员查看和理解使用,或者编译器是否可以通过禁止读取超过缓冲区长度的字符来保护代码。当我读入一个字符串时,它只是继续前进,并开始覆盖存储在我要读入的缓冲区之后声明的变量中的数据。有没有安全的方法来读入数据?

char arr[5];                                                                
char buff[5] = "cat";                                                                                                                                        
printf("The buffer holds: %s\n", buff);                                     
printf("Input a word to be held in \"arr\": ");                             

scanf("%s", arr);                                                           

printf("The array holds:  %s\n", arr);                                      
printf("The buffer holds: %s\n", buff);                                     
printf("%c\n", arr[9]);      

如果读入 arr 的字符串足够长,"cat" 将被覆盖,编译标志的 none 似乎可以做任何事情(我用 -Wextra -Wall -Werror -std= 编译c99) 唯一抱怨的是 valgrind。如何在 C 中编写安全数组代码?

C 不会保护您免于越过数组末尾。虽然有一些方法可以检测到它。看到这个 post

Setting up a bounds-protected array

试试这个代码

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

#define ARRAY_SIZE 100

int main(void) {
  size_t i = 0;
  char   arr1[ARRAY_SIZE];
  char * arr2 = malloc(ARRAY_SIZE );
  for(i = 0; i < 200; i++) {
    arr1[i] = '1';
    arr2[i] = '2';
  }

  for(i = 0; i < 200; i++) {
    printf("%zu arr1[i]=%c  \n", i, arr1[i]);
    printf("%zu arr2[i]=%c  \n", i, arr2[i]);
  }
  return 0;
}

使用以下编译时选项(这仅适用于 gcc,即 clang 不会给出错误)

gcc -O3 -Wall -std=c11 -pedantic array_overflow_at_03.c

然后尝试使用

gcc -Wall -std=c11 -pedantic array_overflow_at_03.c

执行此操作的每种方法都有其优点,您的应用程序需求将决定使用哪一种。

C 遵循 "the programmer knows best" 和 "I ain't holding you hand"

的哲学

这就是为什么 C 如此之快,它不需要做任何检查。

为了安全的用户输入,您可以使用 fgets

大致如下:

fgets(arr, sizeof(arr), stdin);

arr 会将输入保持到指定大小。有关更多信息,我推荐 fgets 的手册页 http://linux.die.net/man/3/fgets

您可能需要多次调用它才能从标准输入中获取所有输入。

C 中的数组大小仅告诉编译器为数组保留多少内存。 C 不会插入代码来检查您是否超出了数组边界。 int a[5]; 中的大小“5”未存储在已编译程序中。它仅在源代码中。其他能看到源码的程序员都能看到;没有其他人可以。

由于 C 不会检查您的操作并握住您的手(请参阅 Lyle Rolleman 的回答),因此 C 不会 "detect" 缓冲区溢出。因此,当这种情况发生时,行为是未定义的(所谓的 "Undefined Behavior",或 UB)。经常发生的是堆栈被覆盖,堆栈上是调用者的 return 地址。这被覆盖,当当前函数想要 return 时,它跳转到 "nowhere" (或某个地方,因为这种行为被黑客小心地覆盖堆栈的 "stack exploits" 使用,所以跳转是至 "their-where").

从某种意义上说,C语言本身既不会保护你也不会保护你不让你越界。更准确地说,C 编译器不需要执行边界检查,但允许这样做。 (很少有编译器利用该权限。非常默认情况下很少这样做。)

例如,如果你写:

int arr[10];
arr[20] = 42;

行为未定义。这并不意味着您的程序会崩溃。这并不意味着将 或不会 检测到错误。引用 ISO C 标准,

behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements

典型的 C 编译器可能生成的代码采用 arr 的基地址,向其添加 20 * sizeof (int) 的偏移量,然后尝试将 42 存储在结果中地点。如果没有显式或隐式检查,这可能会破坏其他一些数据结构,它可能会写入您的进程拥有但未用于任何其他用途的内存,或者它可能会终止您的程序。 (或者 #include <stdjoke.h> 它会让恶魔从你的鼻子里飞出来。)

但是符合标准的 C 编译器 可以 添加代码来检查索引是否在 0 到 9 的范围内,如果不在范围内则采取一些明智的措施。 C 不禁止边界检查;它只是不需要它。

在这种特殊情况下,可以(但不是必需)在编译时检测到数组访问越界,因此编译器可以发出编译时警告。 (如果直到 运行 时间才知道索引值,这是不可能的。)

最终,避免越界访问的责任落在了程序员身上。不要假设编译器会为您检查它——也不要假设它不会。