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


148
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被声明为a short而不是a 有关int,但我不确定。

有任何想法吗?


3
此代码存在多个问题。回想一下C字符串是空终止的。
Dmitri Chubarov

4
@DmitriChubarov(不为null终止字符串)仅在调用之后使用字符串时才是问题strncpy。在这种情况下,不是。
R Sahu

43
该代码中的问题直接来自于strlen计算出的事实,用于有效性检查,然后又荒谬地再次计算了 -这是DRY失败。如果将第二strlen(str)个替换为len,则无论的类型如何,缓冲区都不会溢出len。答案没有解决这一点,他们只是设法避免了这一点。
吉姆·巴尔特

3
@CiaPan:Wenn向其传递一个非空终止的字符串,strlen将显示未定义的行为。
Kaiserludi 2015年

3
@JimBalter Nah,我想我把它们留在那里。也许其他人也会有同样愚蠢的误解并从中汲取教训。如果他们激怒了您,请随时举报他们,有人可能会过来并将其删除。
Asad Saeeduddin

Answers:


192

在大多数编译器上,an的最大值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);

2
@PatrickRoberts从理论上讲,是的。但是您必须记住,只有10%的代码负责90%的运行时,因此,不应让性能先于安全性。并且请记住,随着时间的推移,代码会发生变化,这可能突然意味着先前的检查已消失。
orlp 2015年

3
为了防止缓冲区溢出,只需将其len用作strncpy的第三个参数。无论如何,再次使用strlen是愚蠢的。
吉姆·巴尔特

15
/ sizeof(buffer[0])-请注意,sizeof(char)C中的值始终为 1(即使一个char包含一个gazillion位),因此在无法使用其他数据类型的情况下,这是多余的。仍然...恭喜您获得完整的答案(并感谢您对评论的回应)。
吉姆·巴尔特

3
@ rr-:char[]char*不是同一回事。在许多情况下,a char[]都会隐式转换为char*。例如,char[]与用作char*函数参数的类型时完全相同。但是,不会发生的转换sizeof()
Dietrich Epp 2015年

4
@Controll因为如果您buffer在某个时候更改了大小,则表达式会自动更新。这对于安全性至关重要,因为的声明buffer可能与实际代码中的检查相差很多行。因此,更改缓冲区的大小很容易,但是忘记在每个使用该大小的位置进行更新。
orlp 2015年

28

问题在这里:

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

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

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

这是将复制的数据量限制为缓冲区的实际大小减去空终止符的数量。然后,我们将缓冲区中的最后一个字节设置为空字符,以此作为附加保护措施。原因是因为strncpy将复制最多n个字节,包括终止空值,如果strlen(str)<len-1。串。

希望这可以帮助。

编辑:经过进一步检查并从其他人输入,该函数的可能编码如下:

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);
  }

由于我们已经知道了字符串的长度,因此可以使用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.

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

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

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

这本身可能会或可能不会带来安全风险,具体取决于字符串的内容及其用途。如果只是可读的纯文本,那么通常就没有问题。您只会得到一个截断的字符串。但是,如果它是URL或SQL命令序列之类的内容,则可能会出现问题。


2
是的,但这超出了问题的范围。该代码清楚地显示了传递给char指针的函数。在功能范围之外,我们不在乎。
丹尼尔·鲁迪

“存储str的缓冲区”-这不是缓冲区溢出,这是这里的问题。而且每个答案都有这个“问题”,这是给定func...和其他任何以NUL终止的字符串作为参数的C函数的签名所无法避免的。提出输入不被NUL端接的可能性是完全毫无头绪的。
吉姆·巴尔特

“这超出了问题的范围” –可悲的是,这超出了某些人的理解能力。
吉姆·巴尔特

“问题出在这里”-没错,但是您仍然缺少关键问题,即len >= 100对一个值执行了test(),但是副本的长度却赋予了一个不同的值……这违反了DRY原则。只需调用即可strncpy(buffer, str, len)避免缓冲区溢出的可能性,并且比strncpy(buffer,str,sizeof(buffer) - 1)... 做的工作要少,尽管在这里它的执行速度仅比...慢memcpy(buffer, str, len)
吉姆·巴尔特

@JimBalter这超出了问题的范围,但是我离题了。我知道测试使用的值和strncpy中使用的值是两个不同的值。但是,一般编码实践说,复制限制应为sizeof(buffer)-1,因此复制上str的长度无关紧要。当strncpy命中null或复制n个字节时,它将停止复制字节。下一行保证缓冲区中的最后一个字节为空字符。该代码是安全的,我坚持我之前的声明。
Daniel Rudy 2015年

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;
}

@ user3386109 strlen然后也不会访问缓冲区末尾吗?
Patrick Roberts

2
@ user3386109您所指出的内容使orlp的答案与我的一样无效。我不明白为什么strnlenorlp所建议的无论如何都不能解决问题。
帕特里克·罗伯茨

1
“我不认为strnlen在这里能解决任何问题”,当然可以。它防止溢出buffer。“因为str可以指向2个字节的缓冲区,但都不是NUL。” -无关紧要,因为任何实现都是如此func。这里的问题是关于缓冲区溢出,而不是UB,因为输入不是NUL终止的。
吉姆·巴尔特

1
“传递给strnlen的第二个参数必须是第一个参数指向的对象的大小,否则strnlen毫无价值” –这是完整的,而且毫无意义。如果strnlen的第二个参数是输入字符串的长度,则strnlen等效于strlen。您甚至将如何获得该号码,如果有,为什么还要致电str [n] len?这根本不是strnlen的目的。
吉姆·巴尔特

1
+1尽管此答案并不完美,因为它不等同于OP的代码-strncpy NUL-pads不会NUL终止,而strcpy NUL-terminates而不是NUL-pad,它确实解决了问题,与上面的荒谬,无知的评论。
吉姆·巴尔特

4

包装的答案是正确的。但是有一个问题,我想如果if(len> = 100)

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

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


有问题的字符串可以解决:只需使用适当的函数即可。即 不是 strncpy()和朋友,而是喜欢strdup()和朋友的内存分配功能。它们符合POSIX-2008标准,因此虽然在某些专有系统上不可用,但它们具有相当的可移植性。
cmaster-恢复莫妮卡2015年

“取决于适当的结束字符串的任何其他函数”- buffer在此函数本地,并且在其他地方未使用。在实际的程序中,我们必须检查它的用法...有时NUL终止是不正确的(strncpy最初的用途是创建UNIX的14字节目录条目-NUL填充而不是NUL终止)。“来自C的有问题的字符串是恕我直言无法解决的”-C是一种令人讨厌的语言,已经被更好的技术所取代,但是,如果使用足够的规范,则可以在其中编写安全的代码。
吉姆·巴尔特

在我看来,您的观察被误导了。if (len >= 100)是检查失败的条件,而不是检查通过的条件,这意味着不会复制正好100字节没有NUL终止符的情况,因为该长度包含在失败条件中。
帕特里克·罗伯茨

@ cmaster。在这种情况下,您错了。这是无法解决的,因为人们总是可以超越界限写作。是的,这是一种不可思议的行为,但是没有办法完全阻止它。
Friedrich

@吉姆·巴尔特 不要紧。我可能可以覆盖此本地缓冲区的边界,因此总是有可能破坏其他一些数据结构。
Friedrich

3

除了涉及strlen多次调用的安全性问题外,通常不应该在长度确切知道的字符串上使用字符串方法[对于大多数字符串函数,只有非常狭窄的情况下才应使用它们-在最大长度的字符串上长度可以保证,但确切的长度未知]。一旦知道了输入字符串的长度并且知道了输出缓冲区的长度,就应该弄清楚应该复制多大的区域,然后memcpy()实际使用它进行复制。尽管仅复制1-3个字节左右的字符串时,strcpy性能可能会表现不佳memcpy(),但在许多平台memcpy()上,处理较大的字符串时,速度可能会快两倍以上。

尽管在某些情况下安全性是以性能为代价的,但是在这种情况下,安全方法也是一种更快的方法。在某些情况下,如果提供输入的代码可以确保它们表现良好,并且防范不良行为的输入会妨碍性能,那么编写一些对行为异常的输入并不安全的代码可能是合理的。确保字符串长度只检查一次提高性能和安全性,但一个额外的东西可以手动跟踪字符串长度,即使做帮助守卫安全:每预计将尾随的空字符串,写结尾的空明确而比期望源字符串有它。因此,如果有人在写一篇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终止的目标字符串。


3
您能否解释多次通话涉及的安全性问题strlen
Bogdan Alexandru

1
@BogdanAlexandru:一旦调用strlen并根据返回的值采取了某些操作(这大概是首先调用它的原因),那么重复调用(1)总是会得到与第一个相同的答案,在这种情况下,这只是浪费工作,或者(2)有时(由于其他原因-也许是另一个线程-同时修改了字符串)会产生不同的答案,在这种情况下,代码会对长度做一些事情(例如分配缓冲区)的大小可能与执行其他操作(复制到缓冲区)的代码的大小不同。
supercat 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.