为什么这段代码容易受到缓冲区溢出攻击?

Why is this code vulnerable to buffer overflow attacks?

int func(char* str)
{
   char buffer[100];
   unsigned short len = strlen(str);

   if(len >= 100)
   {
        return (-1);
   }

   strncpy(buffer,str,strlen(str));
   return 0;
}

此代码容易受到缓冲区溢出攻击,我正在尝试找出原因。我认为这与 len 被声明为 short 而不是 int 有关,但我不太确定。

有什么想法吗?

在大多数编译器上,unsigned short 的最大值是 65535。

高于该值的任何值都会被环绕,因此 65536 变为 0,而 65600 变为 65。

这意味着正确长度的长字符串(例如 65600)将通过检查,并溢出缓冲区。


使用size_t来存储strlen()的结果,而不是unsigned short,并将len与直接编码buffer大小的表达式进行比较.例如:

char buffer[100];
size_t len = strlen(str);
if (len >= sizeof(buffer) / sizeof(buffer[0]))  return -1;
memcpy(buffer, str, len + 1);

即使您使用的是 strncpy,截断的长度仍然取决于传递的字符串指针。您不知道该字符串有多长(即空终止符相对于指针的位置)。因此,单独调用 strlen 会使您容易受到攻击。如果您想更安全,请使用 strnlen(str, 100).

更正的完整代码为:

int func(char *str) {
   char buffer[100];
   unsigned short len = strnlen(str, 100); // sizeof buffer

   if (len >= 100) {
     return -1;
   }

   strcpy(buffer, str); // this is safe since null terminator is less than 100th index
   return 0;
}

问题出在这里:

strncpy(buffer,str,strlen(str));
                   ^^^^^^^^^^^

如果字符串大于目标缓冲区的长度,strncpy 仍会复制它。您将字符串的字符数作为要复制的数字而不是缓冲区的大小。正确的做法如下:

strncpy(buffer,str, sizeof(buff) - 1);
buffer[sizeof(buff) - 1] = '[=11=]';

这样做是将复制的数据量限制为缓冲区的实际大小减去一个空终止字符。然后我们将缓冲区中的最后一个字节设置为空字符作为额外的保护措施。这样做的原因是因为如果 strlen(str) < len - 1,strncpy 将复制最多 n 个字节,包括终止空值。如果不是,则不会复制空值并且您会遇到崩溃情况,因为现在您的缓冲区有一个未终止的字符串.

希望这对您有所帮助。

编辑:经过进一步检查和其他人的输入,该功能的可能编码如下:

int func (char *str)
  {
    char buffer[100];
    unsigned short size = sizeof(buffer);
    unsigned short len = strlen(str);

    if (len > size - 1) return(-1);
    memcpy(buffer, str, len + 1);
    buffer[size - 1] = '[=12=]';
    return(0);
  }

由于我们已经知道字符串的长度,我们可以使用memcpy将字符串从str引用的位置复制到缓冲区中。请注意,根据 strlen(3) 的手册页(在 FreeBSD 9.3 系统上),说明如下:

 The strlen() function returns the number of characters that precede the
 terminating NUL character.  The strnlen() function returns either the
 same result as strlen() or maxlen, whichever is smaller.

我解释为字符串的长度不包括空值。这就是为什么我复制 len + 1 个字节以包含 null,并且测试检查以确保长度 < 缓冲区大小 - 2。减一是因为缓冲区从位置 0 开始,减去另一个以确保有空间对于 null.

编辑:事实证明,某些内容的大小以 1 开头,而访问权限以 0 开头,所以之前的 -2 是不正确的,因为它 return 对于任何 > 98 字节的内容都会出错,但它应该 > 99 字节。

编辑:虽然关于 unsigned short 的答案通常是正确的,因为可以表示的最大长度是 65,535 个字符,但这并不重要,因为如果字符串长于该长度,该值将环绕。这就像取 75,231(即 0x000125DF)并屏蔽掉前 16 位得到 9695(0x000025DF)。我看到的唯一问题是超过 65,535 的前 100 个字符,因为长度检查将允许复制, 但在所有情况下它只会复制字符串的前 100 个字符,并且 null 终止字符串。所以即使有环绕问题,缓冲区仍然不会溢出。

这本身可能会也可能不会带来安全风险,具体取决于字符串的内容和您使用它的目的。如果它只是人类可读的纯文本,那么通常没有问题。你只是得到一个截断的字符串。但是,如果它类似于 URL 甚至 SQL 命令序列,您可能会遇到问题。

带包装的答案是正确的。但是有一个问题我觉得没有提到 如果(len >= 100)

好吧,如果 Len 为 100,我们将复制 100 个元素,并且没有尾随 \0。这显然意味着依赖于正确结束字符串的任何其他函数将超越原始数组。

恕我直言,C 中有问题的字符串无法解决。你最好在打电话之前有一些限制,但即使这样也无济于事。没有边界检查,因此缓冲区溢出总是会发生,不幸的是会发生....

除了多次调用 strlen 所涉及的安全问题之外,通常不应在长度已知的字符串上使用字符串方法 [对于大多数字符串函数,只有在极少数情况下才应该使用被用在可以保证最大长度但不知道精确长度的字符串上]。一旦知道输入字符串的长度和输出缓冲区的长度,就应该计算出应该复制多大的区域,然后使用 memcpy() 实际执行所讨论的复制。尽管在复制只有 1-3 个字节左右的字符串时 strcpy 的性能可能优于 memcpy(),但在许多平台上 memcpy() 在处理更大的字符串时可能比 memcpy() 快两倍以上字符串。

尽管在某些情况下安全性会以牺牲性能为代价,但在这种情况下,安全方法更快。在某些情况下,如果提供输入的代码可以确保它们的行为良好,并且如果防止行为不当的输入会影响性能,那么编写对异常行为输入不安全的代码可能是合理的。确保字符串长度只检查一次可以提高 both 性能和安全性,尽管可以做一件额外的事情来帮助保护安全性,即使在手动跟踪字符串长度时也是如此:对于每个预期具有尾随空值,显式写入尾随空值而不是期望源字符串具有它。因此,如果有人正在写一个 strdup 等价物:

char *strdupe(char const *src)
{
  size_t len = strlen(src);
  char *dest = malloc(len+1);
  // Calculation can't wrap if string is in valid-size memory block
  if (!dest) return (OUT_OF_MEMORY(),(char*)0); 
  // OUT_OF_MEMORY is expected to halt; the return guards if it doesn't
  memcpy(dest, src, len);      
  dest[len]=0;
  return dest;
}

请注意,如果 memcpy 已处理 len+1 字节,则通常可以省略最后一条语句,但另一个线程要修改源字符串,结果可能是非 NUL 终止的目标字符串。