预期的缓冲区溢出并不总是导致程序崩溃

时间:2015-10-08 10:29:55

标签: c memory stack buffer-overflow

考虑以下最小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");
                          ^   
}

程序因Segmentation故障而崩溃。

除了堆栈中保留的10个字符外,还有8个额外字符的额外空间。因此第一个程序不会崩溃。但是,如果再添​​加一个字符,则会开始访问无效内存。我的问题是:

  1. 为什么我们在堆栈中保留了这些额外的8个字符?
  2. 这与内存中的char数据类型对齐有什么关系?
  3. 我在这种情况下的其他疑问是操作系统(在这种情况下是Windows)如何检测到错误的内存访问?通常,根据Windows文档,默认堆栈大小为1MB Stack Size。所以我没有看到操作系统如何检测到被访问的地址是否在进程内存之外,特别是当最小页面大小通常为4k时。在这种情况下,操作系统是否使用SP来检查地址?

    PD:我正在使用以下环境进行测试 Cygwin的
    GCC 4.8.3
    Windows 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
    

3 个答案:

答案 0 :(得分:2)

我相信你明白你已经实现了导致未定义行为的东西。所以很难回答为什么它失败的额外字符串而不是原始字符串。它可能与内部编译器实现+受编译标志(如对齐,优化等)的影响有关。

您可以尝试反汇编二进制文件或创建汇编代码,并查看缓冲区在堆栈中的确切位置。您可以使用不同的优化级别执行相同操作,以检查汇编代码和行为中的更改。

  

操作系统(在这种情况下是Windows)如何检测到错误的内存访问?   通常,根据Windows文档,默认堆栈大小为   1MB堆栈大小。所以我没有看到操作系统如何检测到该地址   被访问时特别是在进程内存之外   最小页面大小通常为4k。在这种情况下,操作系统是否使用SP   检查地址?

操作系统不会监视您执行的代码。 HW(CPU)执行(因为它执行此代码)。一旦您的代码尝试访问未为您的进程分配的地址(对于您的程序而言不是mapped by the OS),操作系统将获得指示,因为HW将触发#PF(页面错误)异常。另一种情况是您尝试访问为您分配但具有不正确权限的地址(例如,您尝试从没有'执行'权限的DATA页面执行二进制数据)或转到CODE页面但是有错误offset和你读的指令不存在或者(甚至更糟)它存在并解码为你不期望的东西(我们之前说过未定义的行为吗?)。

通常,您的代码很可能不会在strcpy上失败(如果您编写足够的数据来访问某些禁止的地址,但很可能不是这种情况) - 它从{返回时失败{1}}功能。 foo刚刚覆盖了指向strcpy函数之后的下一条指令的下一条指令指针。因此,指令指针用“012345678901345678”字符串中的数据填充,并尝试从'junky'地址获取下一条指令,并由于上述原因而失败。

这个“方法”/ bug被称为“buffer overflow attack”并且在黑客中广泛使用以使您的代码(以及更常见的以更高权限执行的OS / BIOS / VMM / SMM代码)执行恶意代码由黑客提供。只需确保用预先准备好的代码地址覆盖指令指针。

答案 1 :(得分:2)

官方,系统无关的答案是:

您的代码将数据写入目标数组的末尾,行为未定义,任何事情都可能发生,包括什么都没有空间探测器在火星表面上崩溃。您的观察结果在缓冲区末端之外的8个字节之外没有明显的影响,并且超出此范围的分段故障崩溃是未定义行为的可能影响,完全在预期结果范围内。

您感兴趣的额外实施细节:

实际行为取决于许多情况,例如您使用的编译器,OS和ABI(应用程序二进制接口)等。

您的程序是在64位Windows环境中编译和执行的。在这个环境中,堆栈在64位边界或可能的16字节边界上保持对齐,以允许从/向堆栈位置直接加载和存储MMX寄存器。数组buffer[10]占用堆栈上的16个字节。给定如何在此处理器上建立堆栈,它将位于函数foo使用的位置下方,以将任何已保存的寄存器和返回地址存储到调用函数main中。额外的6个字节是在数组之前还是之后是编译器的选择。它可以将此空间用于其他局部变量,或者只是忽略它。

如果填充在数组之后,超出buffer末尾的写入可能对多达6个字节无害,对另外8个字节可能没有任何明显的影响(破坏已保存的rbp寄存器,调用后在main中未使用的内容,但在此之后会产生不良副作用,因为您将覆盖返回地址。

当您覆盖返回地址时,处理器不会从函数foo返回到调用者main,而是返回到存储在堆栈中的任何地址,并且被违规代码破坏。如果这个损坏的地址指向可执行代码,那么该代码将被执行并带来潜在的有害后果......黑客正是这样做的:他们非常谨慎地制作一个漏洞,设法将某些有害代码存储在可执行内存中的已知位置并利用缓冲区溢出代码,用于将所述代码的地址存储在返回地址的堆栈位置。

在您的情况下,损坏的返回地址指向的位置可能无法执行,从而触发您观察到的分段错误。

我建议您尝试在此站点上编译代码,以查看在各种编译器选项下生成的实际汇编代码:http://gcc.godbolt.org/#

答案 2 :(得分:0)

堆栈中的下一个条目是64位系统中的函数地址,必须与8对齐,因此有足够的空间容纳16个字符。

您可以通过在数组后声明一个int变量来验证这一点。 Int将与4对齐,并且字符空间将减少,因此程序将在较低的数字上崩溃。