考虑以下最小C程序:
Case Number 1 :
#include <stdio.h>
#include <string.h>
void foo(char* s)
{
char buffer[10];
strcpy(buffer,s);
}
int main(void)
{
foo("01234567890134567");
}
This doesn't cause a crash dump
如果只添加一个字符,那么新主要是:
Case Number 2 :
void main()
{
foo("012345678901345678");
^
}
程序因Segmentation故障而崩溃 .
除了堆栈中保留的10个字符之外,还有一个额外的空间可容纳8个额外字符 . 因此第一个程序不会崩溃 . 但是,如果再添加一个字符,则会开始访问无效内存 . 我的问题是:
-
为什么我们在堆栈中保留了这些额外的8个字符?
-
这与内存中的char数据类型对齐有何关联?
我在这种情况下的另一个疑问是操作系统(在这种情况下是Windows)如何检测到错误的内存访问?通常,根据Windows文档,默认堆栈大小为1MB Stack Size . 因此,我没有看到操作系统如何检测到被访问的地址是否在进程内存之外,特别是当最小页面大小通常为4k时 . 在这种情况下操作系统是否使用SP来检查地址?
PD:我正在使用以下环境进行测试
Cygwin的
GCC 4.8.3
Windows 7操作系统
EDIT :
这是从http://gcc.godbolt.org/#生成的程序集,但是使用GCC 4.8.2,我可以在foo函数中发生't see the GCC 4.8.3 in the available compilers. But I guess the generated code should be similar. I built the code without any flags. I hope somebody with Assembly expertise could shed some light about what',以及为什么额外的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
3 回答
我相信你明白你已经实现了导致未定义行为的东西 . 所以很难回答为什么它失败的额外字符串而不是原始字符串 . 它可能与受编译标志影响的内部编译器实现有关(如对齐,优化等) .
您可以尝试反汇编二进制文件或创建汇编代码,并查看缓冲区在堆栈中的确切位置 . 您可以使用不同的优化级别执行相同操作,以检查汇编代码和行为中的更改 .
操作系统不会监视您执行的代码 . HW(CPU)执行(因为它执行此代码) . 一旦您的代码尝试访问未为您的进程分配的地址(对于您的程序而言不是mapped by the OS),操作系统将获得指示,因为HW将触发#PF(页面错误)异常 . 另一种情况是,您尝试访问为您分配但具有不正确权限的地址(例如,您尝试从没有'execute'权限的DATA页面执行二进制数据)或转到CODE页面但偏移量错误,你读的指令没有预期(我们之前说过未定义的行为吗?) .
一般来说,你的代码很可能在
strcpy
上没有失败(如果你写了足够的数据来访问一些禁止的地址,但很可能不是这种情况) - 它从foo
函数返回时失败 .strcpy
刚刚覆盖了指向foo
函数之后的下一条指令的下一条指令指针 . 因此,指令指针用"012345678901345678"字符串中的数据填充,并尝试从'junky'地址获取下一条指令,并由于上述原因而失败 .这个"method" / bug被称为“buffer overflow attack”并且在黑客中被广泛使用以使您的代码(以及更常见的以更高权限执行的OS / BIOS / VMM / SMM代码)执行黑客提供的恶意代码 . 只需确保用您预先准备的代码的地址覆盖指令指针 .
官方,系统无关的答案是:
您的代码将数据写入目标数组的末尾,行为未定义,任何事情都可能发生,包括根本没有任何内容或空间探测器在火星表面上崩溃 . 您的观察结果在缓冲区末端之外的8个字节内没有明显影响,并且超出此范围的分段故障崩溃可能是未定义行为的影响,完全在预期结果范围内 .
您感兴趣的额外实施细节:
实际行为取决于许多情况,例如您使用的编译器,OS和ABI(应用程序二进制接口)等 .
您的程序在64位Windows环境中编译和执行 . 在这个环境中,堆栈在64位边界或可能的16字节边界上保持对齐,以允许从/向堆栈位置直接加载和存储MMX寄存器 . 数组
buffer[10]
在堆栈上占用16个字节 . 给定如何在此处理器上 Build 堆栈,它将位于函数foo
使用的位置下方,以将任何已保存的寄存器和返回地址存储到调用程序函数main
中 . 额外的6个字节是在之前还是之后array是编译器的选择 . 它可以将此空间用于其他局部变量,或者只是忽略它 .如果填充在数组之后,超出
buffer
末尾的写入对于最多6个字节可能是无害的,对于另外8个字节可能没有任何明显的影响(破坏保存的rbp
寄存器,在调用之后main
中未使用),但是除此之外会产生不良副作用,因为你将覆盖返回地址 .当您覆盖返回地址时,处理器将不会从函数
foo
返回到调用者main
,而是返回到存储在堆栈中的任何地址,并且被违规代码破坏 . 如果这个损坏的地址指向可执行代码,那么该代码将被执行,带来潜在的有害后果......黑客正是这样做的:他们非常谨慎地制作一个漏洞,设法将某些有害代码存储在可执行内存中的已知位置,并利用缓冲区溢出代码,用于将所述代码的地址存储在返回地址的堆栈位置中 .在您的情况下,损坏的返回地址指向的位置可能无法执行,从而触发您观察到的分段错误 .
我建议您尝试在此站点上编译代码,以查看在各种编译器选项下生成的实际汇编代码:http://gcc.godbolt.org/#
堆栈中的下一个条目是64位系统中的函数地址,必须与8对齐,因此有足够的空间容纳16个字符 .
您可以通过在数组后声明一个int变量来验证这一点 . Int将与4对齐,并且字符空间将减少,因此程序将在较低的数字上崩溃 .