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

Jas*_*son 148 c security buffer-overflow

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

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

   strncpy(buffer,str,strlen(str));
   return 0;
}
Run Code Online (Sandbox Code Playgroud)

此代码容易受到缓冲区溢出攻击,我正在试图找出原因.我认为它与len被宣布为一个short而不是一个int,但我不是很确定.

有任何想法吗?

orl*_*rlp 193

在大多数编译器中,a的最大值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);
Run Code Online (Sandbox Code Playgroud)

  • `/ sizeof(buffer [0])` - 注意C中的`sizeof(char)`总是*1(即使一个字符包含很多位),所以当不可能使用不同的数据类型时这是多余的.仍然...赞成一个完整的答案(并感谢您对评论做出回应). (15认同)
  • @Controll因为如果你在某个时候更改`buffer`的大小,表达式会自动更新.这对于安全性至关重要,因为`buffer`的声明可能与实际代码中的检查相差甚远.因此,很容易改变缓冲区的大小,但忘记在每个使用大小的位置进行更新. (4认同)
  • 要防止缓冲区溢出,只需使用`len`作为strncpy的第三个参数.无论如何,再次使用strlen是愚蠢的. (3认同)
  • @ rr-:`char []`和`char*`不是一回事.有很多*情况,其中`char []`将被隐式转换为`char*`.例如,当用作函数参数的类型时,`char []`与`char*`完全相同.但是,`sizeof()`不会发生转换. (3认同)
  • @PatrickRoberts理论上,是的.但是你必须记住,10%的代码负责90%的运行时,所以你不应该在安全性之前放弃性能.请记住,随着时间的推移,代码会发生变化,这可能突然意味着之前的检查已经消失. (2认同)
  • "`strncpy(buf,str,strlen(str))`与`strcpy(buf,str)`完全相同` - 不,它没有; strncpy没有NUL终止(并且NUL-pad). (2认同)
  • 那我的另一点呢?使用sizeof(缓冲区)过多; `len`总是少而且足够.无论"len"的类型如何,它都有效 - 这里真正的问题是测试是针对`len`完成的,但实际的副本使用`strlen(str)`......这违反了DRY原则是不好的做法在很多方面,我们在这里看到其中一个. (2认同)
  • @JimBalter因为C(++)中的数组令人困惑,因为它们在传递给函数时会对指针进行处理,即使函数参数看起来像是声明为数组.这让人们误以为数组总是只是指针,而不是仅仅在传递给函数时进行转换. (2认同)
  • @JimBalter这不是愚蠢 - 缺乏知识.知识反直觉. (2认同)
  • @Controll"size_t是无符号的typedef(可能是短的)?" - `size_t`被定义为一个足以容纳*any*对象长度的类型,所以它只能在只有64K内存的机器上``unsigned short`. (2认同)
  • 在130多个upvotes之后,我很惊讶没有人在原始代码中找到__critical__错误.该字符串永远不会以空值终止! (2认同)

Dan*_*udy 28

问题出在这里:

strncpy(buffer,str,strlen(str));
                   ^^^^^^^^^^^
Run Code Online (Sandbox Code Playgroud)

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

strncpy(buffer,str, sizeof(buff) - 1);
buffer[sizeof(buff) - 1] = '\0';
Run Code Online (Sandbox Code Playgroud)

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

希望这可以帮助.

编辑:经过进一步审查和其他人的意见,可能编写的功能如下:

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] = '\0';
    return(0);
  }
Run Code Online (Sandbox Code Playgroud)

由于我们已经知道字符串的长度,因此我们可以使用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.
Run Code Online (Sandbox Code Playgroud)

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

编辑:结果,某些东西的大小从1开始,而访问从0开始,所以前面的-2是不正确的,因为它会返回任何> 98字节的错误,但它应该> 99字节.

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

这可能会也可能不会产生安全风险,具体取决于字符串的内容以及您使用它的内容.如果它只是人类可读的直文,那么通常没有问题.你只是得到一个截断的字符串.但是,如果它类似于URL或甚至是SQL命令序列,则可能会出现问题.

  • 没错,但这超出了问题的范围.代码清楚地显示了传递char指针的函数.在功能范围之外,我们不在乎. (2认同)

Pat*_*rts 11

即使你正在使用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;
}
Run Code Online (Sandbox Code Playgroud)

  • @ user3386109你指出的东西让orlp的回答和我的一样无效.我不明白为什么``strnlen``无法解决问题,如果orlp的建议无论如何都是正确的. (2认同)