如果堆栈向下增长,缓冲区溢出如何覆盖变量上方的内容?

信息安全 缓冲区溢出 堆栈溢出
2021-09-01 02:25:50

我意识到缓冲区溢出是如何工作的,但我在理解溢出的方向时遇到了问题。因此,如果堆栈向下增长,则意味着返回地址高于变量的保留空间。当该变量现在溢出时,它不应该覆盖下面而不是上面的内存吗?

tl; dr:如果堆栈向下增长,缓冲区溢出如何覆盖变量上方的内容?

地址空间布局

4个回答

如果堆栈向下增长,稍后调用的函数会在较低的内存地址处获取堆栈帧。此外,返回地址在为局部变量保留空间之前被压入堆栈,因此返回地址的地址高于局部变量。但是数组和缓冲区仍然在内存中向上索引,因此写入数组末尾之后会很好地命中堆栈上的下一个返回地址。

示例,具有强制性 ASCII 艺术:

考虑从不受信任的来源获取输入并将其复制到本地缓冲区的普通函数:

void foo(char *s)
{
    char buf[8];
    strcpy(buf, s);
    return;
}

堆栈看起来有点像这样:

   <---- stack grows to the left
    memory addresses increase to the right -->
  0x8000                        0x8010
  +--------+----------+---------++------------
  + buf[8] | ret addr | char *s ||   ....... 
  +--------+----------+---------++--------------
   <--------- foo() ----------->  <---- caller --

堆栈从右到左填充,从函数参数开始,然后是返回地址,然后是函数的局部变量。很容易看出,从buf地址增加的简单溢出将很好地命中返回地址。

那么,如果堆栈是倒置的,并且向上增长呢?然后溢出缓冲区将以与堆栈增长相同的方式运行,朝向堆栈的空白部分。

听起来不错,但如果foo()调用另一个函数来进行复制也无济于事。这并不罕见,我只是用strcpy. 现在堆栈看起来像这样:

    stack grows to the right -->
    memory addresses increase to the right -->
            0x8000                          0x8010
------------++---------+----------+---------++-----------+-------------+
  ....      || char *s | ret addr | buf[8]  || ret addr  | locals  ... |
------------++---------+----------+---------++-----------+-------------+
 caller --->  <-------- foo() ------------->  <---- strcpy() ---------->

现在,溢出(向右)foo()堆栈帧中的缓冲区将很好地覆盖返回地址strcpy(),而不是foo()不过没关系,我们仍然会跳转到由溢出的、攻击者控制的数据设置的位置。

只有在分配了某些东西时,堆栈才会向下增长。另一方面,读取数组的末尾意味着在内存中向上读取。

假设堆栈指针为 0x1002。在这个地址是你关心的返回指针。例如,如果您分配一个 2 字节数组,则堆栈向下增长:堆栈指针更改为 0x1000,并且数组的地址设置为该新值。将第一个字节写入数组使用地址 0x1000,写入第二个字节使用 0x1001。写完最后使用 0x1002,并覆盖重要的东西。

您可以在您发布的图像中看到这一点,因为表示缓冲区 B 的灰色框在写入超出其边界时向上增长。

缓冲区溢出和堆栈溢出是有区别的。分配的缓冲区可能不使用堆栈,而是使用堆。这取决于它们的分配方式以及编译器会更好/更快/等等。

堆栈溢出确实会覆盖下面的内存,这些内存可能已分配给另一个(先前的)调用或最终分配给堆。堆向上增长,在某些时候它们可能会发生碰撞。这是操作系统抛出分段错误、停止程序或发送信号的好时机。堆栈溢出的问题是它们可以与来自同一进程的其他线程接触。现在想象一个线程,它有一个用于网络发送/接收的大缓冲区,另一个线程堆栈的内容可能会覆盖这个内存区域,从而导致网络上的内存泄漏。

缓冲区溢出更为常见,它们超出了自己的内存范围。只要访问的内存仍在堆区域内,就没有问题(从操作系统的角度来看)。当堆太小并且需要分配额外的数据时,它会变得更加危险。在这一点上,当达到界限时会发生什么还没有定论。当绑定访问超出堆边界的一个字节时,操作系统应该使用 SIGSEGV 终止进程。

堆栈通过 push 指令向下增长,但向上写入和读取。例如,如果您的堆栈占用地址 10 到 5,这是一个长度为 6 的可用地址,当您有推送指令时,堆栈将下降并添加额外的内存,这将导致您的堆栈占用地址 10-4,即7 个可用地址。但是当您写入堆栈并且指令指针位于 4 时,它将从 4 向上写入到 10,并且当您传递地址 10 时,就会出现缓冲区溢出