首页 文章

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

提问于
浏览
3

考虑以下最小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 回答

  • 2

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

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

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

    操作系统不会监视您执行的代码 . 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代码)执行黑客提供的恶意代码 . 只需确保用您预先准备的代码的地址覆盖指令指针 .

  • 2

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

    您的代码将数据写入目标数组的末尾,行为未定义,任何事情都可能发生,包括根本没有任何内容或空间探测器在火星表面上崩溃 . 您的观察结果在缓冲区末端之外的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/#

  • 0

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

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

相关问题