计算机体系结构速成课程
在 Intel x86 和 x64 架构中,有一种称为堆栈的东西。这基本上是存储确定执行路径的所有内容的地方。函数的参数、局部变量和返回地址都存储在堆栈中。CPU 寄存器跟踪程序在堆栈中执行的位置。您可以将值和内存地址推入堆栈,直至达到体系结构的位大小。
我的意思是什么?如果您使用的是 32 位系统,那么您压入堆栈的每个值基本上都是unsigned int
32 位大小(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
和将g
是h
8 字节值。 g
可能只有相等0x10
,但是当你将它推入堆栈时,它会看起来像0x0000000000000010
.
参数压入堆栈后,call
执行指令。这会将返回地址压入堆栈,并跳转到函数执行。因为每次你将东西压入堆栈时,堆栈都会向低地址空间增长,它会接近零。在上图中,您还将看到局部变量xx
、yy
和zz
。它们中的每一个也都在堆栈向下移动到较低的内存。当然,要意识到程序总是在操作栈顶。
堆栈溢出
假设您创建了一个局部变量,它是一个最多 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,我忽略了这一点,因为它对讨论并不重要。记住这一点非常重要,但现在暂时忽略它的存在。