为什么缓冲区溢出会按照它们所在的方向执行?

信息安全 缓冲区溢出 记忆
2021-08-18 10:34:45

我在这里关注 The Security Tube 的视频

他概述了缓冲区溢出,并提到了内存是如何在堆栈中从最高到最低执行的(至少我假设他的实现是这样)。因此,我们将程序中未调用的函数的内存地址传递到 3 字缓冲区中。我们用 12 个字符的字符串溢出该缓冲区,然后将内存地址向后溢出。所以它看起来像这样:

printf "123456789abc\x32\x07\x45\xb4" | ./demo

实际地址是(b4450732)。

为什么我们将内存地址向后显示,但在堆栈的末尾?如果内存是从高到低读的,不应该是倒过来的,而是在我们传给程序的字符串的开头吗?显然,事实并非如此——因为他证明它是有效的。然而; 在我看来,堆栈不会溢出并且值\23x\cba987654321会被执行。

2个回答

计算机体系结构速成课程

在 Intel x86 和 x64 架构中,有一种称为堆栈的东西。这基本上是存储确定执行路径的所有内容的地方。函数的参数、局部变量和返回地址都存储在堆栈中。CPU 寄存器跟踪程序在堆栈中执行的位置。您可以将值和内存地址推入堆栈,直至达到体系结构的位大小。

我的意思是什么?如果您使用的是 32 位系统,那么您压入堆栈的每个值基本上都是unsigned int32 位大小(4 个字节)。如果您在 64 位系统上,它将是 64 位大小(8 字节)。以下是函数堆栈的示例[1] :

uint32_t function(int a, int b, int c, int d, int e, int f, int g, int h);

在此处输入图像描述

上面的示例适用于 x64。CPU 寄存器 RDI、RSI、RDX、RCX、R8 和 R9 存储前 6 个参数,其余的被压入堆栈。return address和将gh8 字节值。 g可能只有相等0x10,但是当你将它推入堆栈时,它会看起来像0x0000000000000010.

参数压入堆栈后,call执行指令。这会将返回地址压入堆栈,并跳转到函数执行。因为每次你将东西压入堆栈时,堆栈都会向低地址空间增长,它会接近零。在上图中,您还将看到局部变量xxyyzz它们中的每一个也都在堆栈向下移动到较低的内存。当然,要意识到程序总是在操作栈顶

堆栈溢出

假设您创建了一个局部变量,它是一个最多 12 个字节的缓冲区。像这样的东西,unsigned char buffer[12];堆栈为 12 个字节的数据腾出空间。假设我们用它填充这个缓冲区,它在堆栈1"012345678912" 上看起来像这样

High
...
32313938
37363534
33323130
...
Low

因为缓冲区的开始总是朝向较低的内存[2]因此,当您开始写入缓冲区时,您总是从低位写入高位。如果您没有分配足够的空间并且您写入的数据多于分配的数据,那么您就会出现缓冲区溢出。

在此处输入图像描述


字节序

现在您要覆盖堆栈上的返回地址。你把它放在你的字符串的末尾,这样当副本写入更高的内存时,它会用你想要的地址覆盖你的返回地址,而不是2英特尔又向你扔了一个弧线球。x86 和 x64 系统是 Little Endian。这意味着最低有效字节位于最小地址中。

因此,您需要将地址 ( 0x32) 的最低有效字节写入较低的内存。因此,您会将地址向后写入内存,因为您是从低内存写入高内存。从高到低查看地址时(如上例所示),内存地址看起来是正确的。但是,当从低到高观看时,它会倒退。要记住的重要方面是架构要求将 LSB 写入较低的内存。


1 - 我在这里使用 32 位系统,因为 4 字节宽度更容易绘制出来。
2 - 保存了 EBP,我忽略了这一点,因为它对讨论并不重要。记住这一点非常重要,但现在暂时忽略它的存在。

这里有两件事:

  1. 在 x86 和 x86-64(以及大多数其他硬件)上,堆栈从内存顶部向下增长。因此,函数使用的数据(例如,您正在溢出的缓冲区)发生在比调用函数时使用的数据(例如,函数完成后要返回的地址,您是试图溢出到)。

  2. 在 x86 和 x86-64(以及各种其他硬件)上,多字节值(例如地址)以little-endian 顺序存储,即。从阅读它的人的角度来看,“倒退”。