不总是导致程序崩溃的预期缓冲区溢出
Posted
技术标签:
【中文标题】不总是导致程序崩溃的预期缓冲区溢出【英文标题】:An intended buffer overflow that does not always cause the program to crash 【发布时间】:2016-01-05 22:08:22 【问题描述】:考虑以下最小的 C 程序:
案例编号 1:
#include <stdio.h>
#include <string.h>
void foo(char* s)
char buffer[10];
strcpy(buffer,s);
int main(void)
foo("01234567890134567");
这不会导致崩溃转储
如果只添加一个字符,那么新的主要是:
案例编号 2:
void main()
foo("012345678901345678");
^
程序因分段错误而崩溃。
看起来除了堆栈中保留的 10 个字符之外,还有一个额外的空间可容纳 8 个额外的字符。因此第一个程序不会崩溃。但是,如果您再添加一个字符,您将开始访问无效内存。我的问题是:
-
为什么我们在堆栈中保留了这 8 个额外的字符?
这是否与内存中的 char 数据类型对齐有关?
在这种情况下我还有一个疑问是操作系统(在这种情况下是 Windows)如何检测到错误的内存访问?通常,根据 Windows 文档,默认堆栈大小为 1MB Stack Size。所以我看不到操作系统如何检测到正在访问的地址在进程内存之外,特别是当最小页面大小通常为 4k 时。操作系统在这种情况下是否使用SP来检查地址?
PD:我正在使用以下环境进行测试 赛格温 GCC 4.8.3 视窗 7 操作系统
编辑:
这是从http://gcc.godbolt.org/# 生成的程序集,但使用 GCC 4.8.2,我在可用的编译器中看不到 GCC 4.8.3。但我猜生成的代码应该是相似的。我构建了没有任何标志的代码。我希望具有汇编专业知识的人可以阐明 foo 函数中发生了什么以及为什么额外的 char 会导致 seg 错误
foo(char*):
pushq %rbp
movq %rsp, %rbp
subq $48, %rsp
movq %rdi, -40(%rbp)
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
movq -40(%rbp), %rdx
leaq -32(%rbp), %rax
movq %rdx, %rsi
movq %rax, %rdi
call strcpy
movq -8(%rbp), %rax
xorq %fs:40, %rax
je .L2
call __stack_chk_fail
.L2:
leave
ret
.LC0:
.string "01234567890134567"
main:
pushq %rbp
movq %rsp, %rbp
movl $.LC0, %edi
call foo(char*)
movl $0, %eax
popq %rbp
ret
【问题讨论】:
访问冲突通常由虚拟内存系统和MMU/MPU硬件处理。 我认为它会因机器而异,甚至可能因编译器而异。 在任何一种情况下,请注意写入未初始化的内存是未定义的行为,尤其是不保证会产生运行时错误。 是的,我知道 :) .. 我正在询问详细信息这是如何执行的。页面大小通常为 4K,而 TMP 只知道页面,因此如何在字节级别检测到错误访问。正如您从问题中看到的那样,由于某种我不明白的原因,没有检测到第一个案例。 您假设 segv 是在写入溢出期间的某个时刻直接引起的。这可能是真的,也可能不是(可能不是)。溢出更有可能成功地覆盖了随后用于有效地址计算的堆栈部分 - 例如返回地址。然后在从这个无效的有效地址加载期间发生 segv。分析堆栈框架布局将更准确地了解发生的情况。 【参考方案1】:与系统无关的官方答案是:
您的代码在目标数组末尾写入数据,行为未定义,任何事情都可能发生,包括什么都没有或太空探测器坠毁在火星表面。您没有观察到超出缓冲区末尾 8 个字节的明显影响,以及超出此范围的分段错误导致的崩溃可能是未定义行为的影响,完全符合预期结果。
您感兴趣的额外实现细节:
实际行为取决于许多情况,例如您使用的编译器、操作系统和 ABI(应用程序二进制接口)等。
您的程序在 64 位 Windows 环境中编译和执行。在这个环境中,堆栈在 64 位边界或可能的 16 字节边界上保持对齐,以允许从堆栈位置直接加载和存储 MMX 寄存器。数组buffer[10]
在堆栈上占用 16 个字节。鉴于堆栈是如何在此处理器上建立的,它将位于函数 foo
用于将任何保存的寄存器和返回地址存储到调用函数 main
的位置下方。额外的 6 个字节是在数组之前还是之后是编译器做出的选择。它可以将此空间用于其他局部变量,也可以忽略它。
如果填充在数组之后,超出buffer
末尾的写入可能最多 6 个字节是无害的,对于另外 8 个字节可能没有任何明显影响(破坏已保存的 rbp
寄存器,该寄存器在main
调用后),但会开始产生除此之外的不良副作用,因为您将覆盖返回地址。
当您覆盖返回地址时,处理器不会从函数 foo
返回到调用者 main
,而是返回到存储在堆栈中并被违规代码破坏的任何地址。如果这个损坏的地址指向可执行代码,那么该代码将被执行并带来潜在的有害后果......黑客正是这样做的:他们精心设计了一个漏洞,设法将一些有害代码存储在可执行内存中的一个已知位置,并利用缓冲区溢出代码将所述代码的地址存储在返回地址的堆栈位置中。
在您的情况下,损坏的返回地址指向的位置可能无法执行,从而触发您观察到的分段错误。
我建议您尝试在此站点上编译您的代码,以查看在各种编译器选项下生成的实际汇编代码:http://gcc.godbolt.org/#
【讨论】:
【参考方案2】:我相信您了解您已经实现了导致未定义行为的某些内容。所以很难回答为什么它会因额外的字符串而失败,而不会因原始字符串而失败。它可能与内部编译器实现有关 + 受编译标志的影响(如对齐、优化等)。
您可以尝试反汇编二进制文件或创建汇编代码并查看缓冲区在堆栈上的确切位置。您可以对不同的优化级别执行相同的操作,以检查汇编代码和行为的变化。
操作系统(在本例中为 Windows)如何检测错误的内存访问? 通常根据 Windows 文档,默认堆栈大小是 1MB 堆栈大小。所以我看不到操作系统如何检测到地址 被访问是在进程内存之外,特别是当 最小页面大小通常为 4k。在这种情况下操作系统是否使用 SP 查看地址?
操作系统不会监控您执行的代码。硬件(CPU)会(因为它执行此代码)。一旦您的代码尝试访问未为您的进程分配的地址(不是为您的程序分配的mapped by the OS),操作系统将获得指示,因为硬件将触发#PF(页面错误)异常。另一种情况是您尝试访问为您分配但权限不正确的地址(例如,您尝试从没有“执行”权限的 DATA 页面执行二进制数据)或转到 CODE 页面但错误偏移量和您阅读的指令不存在,或者(更糟糕的是)它存在并解码为您不期望的东西(我们之前是否说过未定义的行为?)。
一般来说,您的代码很可能不会在 strcpy
上失败(如果您写入足够的数据来访问某些禁止的地址,但很可能并非如此) - 当它从 foo
返回时会失败功能。 strcpy
只是覆盖了指向foo
函数之后的下一条指令的下一条指令指针。因此,指令指针被“012345678901345678”字符串中的数据填充,并尝试从“垃圾”地址获取下一条指令,但由于上述原因而失败。
这种“方法”/错误被称为“buffer overflow attack”,在黑客中广泛使用,以使您的代码(以及更常见的以更高权限执行的 OS/Bios/VMM/SMM 代码)执行由黑客。只需确保用您提前准备的代码地址覆盖指令指针即可。
【讨论】:
【参考方案3】:堆栈中的下一项是函数地址,在 64 位系统中必须对齐到 8,因此有足够的空间容纳 16 个字符。
您可以通过在数组后声明一个 int 变量来验证这一点。 Int 将与 4 对齐,字符空间将减少,因此程序将在较小的数字上崩溃。
【讨论】:
这不会发生。我添加了一个“int a = 0;”就在数组之后,但第一种情况不受影响。无论如何我认为生成的可执行文件是 32 位(PE32 可执行文件) 使用声明的整数。有时编译器会对未使用的变量进行优化并跳过它们。另一种选择是将缓冲区减少到 8 个字符,看看会发生什么 我尝试使用它(a++,然后将 foo 更改为返回 int)但结果相同。以上是关于不总是导致程序崩溃的预期缓冲区溢出的主要内容,如果未能解决你的问题,请参考以下文章
2018-2019-1 20165228 《信息安全系统设计基础》缓冲区溢出漏洞实验报告