C缓冲区溢出的解释

3

我尝试理解缓冲区溢出。这是我的代码:

#include <stdio.h>

int main() 
{
    char buf[5] = { 0 };
    char x = 'u';

    printf("Please enter your name: ");
    gets(buf);

    printf("Hello %s!", buf);

    return 0;
}

buf数组大小为5,并且已初始化为0。因此(包括空终止符),我有四个字符的空间。如果我输入五个字符(例如“stack”),我会覆盖空终止符,由于随后的变量xprintf应该打印“Hello stacku!”但事实并非如此。它只会打印“stack”。请问为什么?


4
仅仅因为你将变量直接放在彼此之后声明,并不意味着它们最终在内存中的位置也是相邻的。特别是因为 buf 是指向某个内存地址的指针,而在这种情况下该地址可能会被零值内存所跟随。 - fredrik
1
顺带一提,你提供的代码甚至没有尝试覆盖空终止符。 - fredrik
通常情况下,您应该避免使用 gets 函数:https://dev59.com/nXI-5IYBdhLWcg3wwLS3 - Bob__
缓冲区溢出很容易发生,不要这么做。 - Martin James
1
@fredrik buf 不是指针。 - Gerhardh
显示剩余6条评论
3个回答

12
简单来说,仅仅因为在“buf”之后的源代码行上声明了“x”,并不意味着编译器会将它们放在堆栈上相邻的位置。在所示代码中,“x”根本没有被使用,所以它可能根本没有被放置在任何地方。即使你使用了“x”(必须是一种防止它被存储到寄存器中的方式),很有可能编译器会精确地将其排序“buf”之下,这样就不会被溢出“buf”的代码覆盖。
你可以通过struct结构强制该程序覆盖“x”,例如:
#include <stdio.h>

int main() 
{
    struct {
        char buf[5];
        char x[2];
    } S = { { 0 }, { 'u' } };

    printf("Please enter your name: ");
    gets(S.buf);

    printf("Hello %s!\n", S.buf);
    printf("S.x[0] = %02x\n", S.x[0]);

    return 0;
}

因为一个struct的字段在内存中的顺序总是按照它们在源代码中出现的顺序进行布局。1原则上,在S.bufS.x之间可能有填充,但char必须具有1的对齐要求,因此ABI可能不要求这样。

但即使你这样做,它也不会打印出“Hello stacku!”,因为gets总是写入一个终止的NUL。看:

$ ./a.out 
Please enter your name: stac
Hello stac!
S.x[0] = 75

$ ./a.out 
Please enter your name: stack
Hello stack!
S.x[0] = 00

$ ./a.out 
Please enter your name: stacks
Hello stacks!
S.x[0] = 73

看看它总是打印你输入的东西,但是x [0]会被覆盖,首先是用NUL覆盖,然后用“s”覆盖?

(你已经读过Smashing the Stack for Fun and Profit了吗? 你应该读一下。)


1 pedants的脚注:如果涉及位字段,则在内存中的字段顺序部分地变成实现定义的。但这对于此问题并不重要。


感谢您详细的回答!我真的以为内存布局会完全按照我声明变量的方式进行... 为什么您把x改成了一个数组?当x是一个简单的变量时,它也可以工作吗?! - lukasl1991
我将 x 更改为数组,这样我就可以在那里有第二个字符,它将始终保持 NUL,作为对 printf 打印垃圾的额外防护。虽然 gets 总是写入 NUL,但在其他缓冲区溢出情况下,您可能没有这种保障,因此这不是严格必要的。 - zwol
此外,感谢您指出gets会写入终止NUL!我通常很困惑何时需要注意这个NUL。如此处所述,char x[]="asdf";是自动获取终止NUL的唯一方法,对吗?如果我做类似于char *foo = "bar";char c[5] = { 'H', 'E', 'L', 'L', 'O' }的事情呢? - lukasl1991
这是一个完全不同的问题,我相信在这个网站的其他地方已经详细解释了,但简短的版本是,如果您使用字符串字面值并且没有用数组长度截断它,那么您将获得一个终止的NUL。 char *foo =“bar”char foo [] =“bar”char foo [4] =“bar”都有一个NUL,但char foo [3] =“bar”没有。 char c [5] = {'H','E','L','L','O'}没有终止的NUL,而char c [6] = {'H','E','L','L','O'}则有,但在这种情况下,这实际上是因为数组元素的默认初始化为0。 - zwol
你能提供一下完整版本的参考资料吗?我试着找了一下,但没有成功。 - lukasl1991

6
正如其他答案所指出的那样,在内存中,不能保证x紧跟在buf之后。但即使是这样,gets也会将其覆盖掉。请记住:gets没有办法知道目标缓冲区有多大(这是它致命的缺陷),它总是写入它读取到的整个字符串以及终止符\0。因此,如果x恰好紧跟在buf之后,那么如果您键入一个五个字符的字符串,printf很可能会正确地打印它(就像您看到的那样),如果您检查x的值:
printf("x = %d = %c\n", x, x);

如果你运行程序,它可能会告诉你现在的x值为0,而不是'U'

以下是内存初始状态:

     +---+---+---+---+---+
buf: |   |   |   |   |   |
     +---+---+---+---+---+

     +---+
  x: | U |
     +---+

当你输入“stack”后,它看起来像这样:
     +---+---+---+---+---+
buf: | s | t | a | c | k |
     +---+---+---+---+---+

     +---+
  x: |\0 |
     +---+

如果你输入“大象”,它将显示为:

     +---+---+---+---+---+
buf: | e | l | e | p | h |
     +---+---+---+---+---+

     +---+
  x: | a | n   t  \0
     +---+

不用说,那些三个字符nt\0很可能会引起更多的问题。

这就是为什么人们说永远不要使用gets。它无法安全使用。


谢谢,解释得很好。我研究gets的行为以获得一些安全见解。 - lukasl1991

2

局部变量通常在堆栈上创建。在大多数实现中,堆栈向下增长而不是向上分配内存。因此,buf 可能位于比 x 更高的地址。这就是为什么当 buf 溢出时,它不会覆盖 x

您可以通过编写 buf[-1]='v';printf("%c\n",x); 来确认这一点,尽管这可能受到填充的影响。还可以使用 printf("%i\n",buf - &x); 比较地址-如果结果为正数,则 buf 位于比 x 更高的地址。

但这完全取决于实现方式,并且可能会根据各种编译器选项而改变。正如其他人所说,您不应依赖任何这些内容。


是的,你说得对!我可以准确地复现这种行为,并用我想要的值覆盖x。谢谢! - lukasl1991
然而,以不同的方式声明更多的char数组会导致不同的行为。打印地址是观察这一点的好方法。 - lukasl1991

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