这个缓冲区溢出的后果是什么?

20

我认为我发现了一个小的缓冲区溢出问题,这是我在审查别人代码时发现的。它立即让我感到不正确,而且可能很危险,但是坦率地说,我无法解释这个“错误”的实际后果(如果有的话)。

我编写了一个测试应用程序来演示错误,但是发现(令我失望的是),似乎无论溢出与否,它都能正常运行。我想相信这只是偶然发生的,但是想要一些反馈,以确定我的想法是否正确,或者是否真的存在一些问题,只是在我的测试应用程序中没有显示出来。

我认为以下是有问题的代码:

char* buffer = new char[strlen("This string is 27 char long" + 1)];
sprintf(buffer, "This string is 27 char long");
现在,这引起了我的关注,并且我想标记它为可能的缓冲区溢出的原因是因为第一个strlen。由于指针算术运算,“不正确”的+ 1的放置将导致strlen返回26而不是27(即“his string is 27 char long”的长度)。我认为sprintf然后会将27个字符打印到缓冲区中,并导致缓冲区溢出。

这是正确的评估吗?

我编写了一个测试应用程序来演示这一点,以向我查看其代码的人展示,并发现即使在调试器中,字符串也将正确打印。我还尝试在此代码之前和之后将其他变量放在堆栈和堆上,以查看是否可以影响相邻的内存区域,但仍然收到正确的输出。我意识到我的新分配的堆内存可能不相邻,这可能解释了缺乏有用的溢出,但我只是想确认其他人的意见,是否确实存在问题。

由于这是一个相当简单的“问题”,如果您能提供某种参考资料作为支持答案,那就太好了。虽然我珍视并欢迎您的意见,但我不会接受“是的”作为最终答案。非常感谢您的帮助。




更新:很多好的答案提供了很多额外的见解。不幸的是,我不能接受它们全部。感谢您分享您的知识,并成为我的“第二意见”。我感激您的帮助。


4
由于填充/对齐的原因,您可能不会在上述代码中遇到问题。请使用一个长度为64个字符的字符串重复您的实验,这样分配需要65个字符。在sprintf之前分配两个这样的字符串,并以不同的顺序填充它们。 - Christopher Creutzig
4
那段代码相当糟糕,它将一个原始字符串加上 +1!仅凭这个事实,我就会在代码审查中挂掉。 - C.J.
1
这就是为什么我们开发人员尽可能使用多个经过充分测试的库的原因...因为我们会犯一些愚蠢的错误! :-) @Johnson 我非常确定开发人员的意思是将1添加到长度,而不是字符串本身,因此出现了错误。 - corsiKa
它调用了未定义行为。要求未定义行为的定义是毫无意义的。我认为这个开发者也不需要它,他会在不到一分钟内修复这个错误。 - Hans Passant
11个回答

14

你的评估是正确的。 在James Curran提到的更正的补充下[edit]

很可能,你的测试应用程序没有显示问题,因为分配被舍入到4、8或16的下一个倍数(这些都是常见的分配粒度)。

这意味着你应该能够通过一个31个字符长的字符串来演示。

或者,使用一种“仪器化”的本地内存分析器,可以在这样一个分配周围紧密地放置防护字节。


6

您的评估是正确的,除了springf会将28个字符放入缓冲区,包括末尾的字符串NUL(这就是您首先需要错位“+1”的原因)

请注意,在我的经验中,如果某些东西在调试器外失败,但在调试器中进行单步调试时可以正常工作,那么100%的情况下,您已经超出了本地缓冲区。调试器会将更多内容推送到堆栈上,因此不太可能覆盖重要内容。


谢谢您对 '\0' 的纠正,我不确定为什么会忘记它 :) - KevenK

3
问题在于你写入的是内存中的某个位置,而不是栈上的位置。 因此,要查找错误是很困难的。 如果你想查看损坏情况,尝试在堆栈上分配字符串。
char buffer[strlen("This string is 27 char long" + 1)];

并写入它之前的内容。 其他变量将被写入,如果你真的知道二进制如何工作,也可以添加一些要执行的代码。

要利用这样的缓冲区溢出,需要先写入所需的数据,然后找到一种方式“跳转”到该数据以便执行。


这仍然不能保证会导致问题,因为堆栈分配通常也对4或8字节边界进行对齐。 - Roddy

1

我尝试使用堆分配,但在这种情况下变量在内存中不是连续的。这就是为什么在这种情况下很难发生缓冲区溢出的原因。

不过可以尝试使用栈溢出。

#include "stdio.h"
#include "string.h"

int main()
{
     unsigned int  y      = (0xFFFFFFFF);
     char buffer[strlen("This string is 27 char long" + 1)];
      unsigned int  x      = (0xFFFFFFFF);
      sprintf(buffer, "This string is 27 char long");

      printf("X (%#x) is %#x, Y (%#x) is %#x, buffer '%s' (%#x) \n", &x, x,&y, y, buffer, buffer);
      return 0;
  }

你会发现Y是损坏的。

1

是的,你说得对。分配的缓冲区将比字符串所需的空间小2个字节。

由于这是在堆上分配的,这可能导致堆破坏。然而,这种情况发生的可能性取决于此之前发生的其他内存分配和释放以及使用的堆管理器。有关更多信息,请参见堆溢出


1
你说的没错,在这个例子中使用指针算术运算会产生一个不正确(更短)的长度传递给 new。你无法使它崩溃的最大可能原因是存在一些不确定性,即内存分配实际提供了多少缓冲区空间。
库允许提供比请求的更大的缓冲区。此外,您的缓冲区之后的任何内容都可能以分配头为前缀,该头受机器字对齐规则的影响。这意味着在接下来的分配头之前可能有多达三个填充字节(取决于平台)。
即使您覆盖了下一个分配头(用于管理分配的内存块),除非该下一个块的所有者尝试将其返回到堆中,否则它不会表现为问题。

1
许多历史悠久的malloc实现将簿记数据放置在分配块之前和/或之后。这可能会覆盖这些数据,如果是这种情况,您在尝试释放内存(或者也许是释放下一个块)之前不会看到任何错误/崩溃。同样,后续分配的簿记信息可能会稍后覆盖您的字符串。
我怀疑现代malloc实现会通过填充分配的完整性检查数据来保护堆栈免受损坏,因此如果您很幸运,就不会发生任何不良事件,或者您可能会在稍后的分配/释放操作期间收到警告消息。

0

正确的陈述。由于您将字符串的第二个字符的地址传递给strlen(),因此结果会少一个字符的长度。除此之外,主要问题在于sprintf(),这也是它不安全的原因之一。

即使这样编译和执行(也可能崩溃)。

    char* x = new char;
    sprintf(x, "This is way longer than one character");
    printf("%s", x);

为了避免这个危险问题,你应该在GCC下使用snprintf()或asprintf()这样的安全版本函数,或者在MSVC下使用sprintf_s()。
作为参考,请查看关于此的The GNU C Library documentation,以及MSDN的sprintf()文章中的安全注意事项。

0

正如其他人所说,你完全正确地认为这是不好的,而你看不到它的原因是填充。尝试在此上使用valgrind,这应该能够确定地找到该错误。


0

你真正的问题在于你正在编写代码

char* buffer = new char[strlen("This string is 27 char long" + 1)];

替代

char* buffer = new char[strlen("This string is 27 char long") + 1];

这意味着在第一个字符串中,你给了strlen()一个不是你字符串开头的地址

尝试使用以下代码:

const char szText[] = "This string is 27 char long";
char* buffer = new char[strlen(szText) + 1];
sprintf(buffer, szText);

他已经知道了。在发布答案之前请先阅读问题。 - manneorama
1
我知道这个问题,这就是让我注意到它并把我带到这里的原因。这是在别人的代码中,我只需要第二个意见,正如预期的那样,在这里获得了额外的知识和洞察力。谢谢你的帮助! - KevenK
你没有回答问题。他知道+1放错了位置。他在问题中明确地说了。他想知道他对错误的评估是否正确,因为他无法通过测试产生错误行为。 - Evan Teran

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接