繁体   English   中英

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

[英]An intended buffer overflow that does not always cause the program to crash

考虑以下最小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数据类型对齐有何关联?

我在这种情况下的另一个疑问是操作系统(在这种情况下是Windows)如何检测到错误的内存访问? 通常,根据Windows文档,默认堆栈大小为1MB 堆栈大小 所以我没有看到操作系统如何检测到被访问的地址是否在进程内存之外,特别是当最小页面大小通常为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

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

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

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

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

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

这种“方法”/错误被称为“ 缓冲区溢出攻击 ”,并且在黑客中广泛使用以使您的代码(以及更常见的以更高权限执行的OS / BIOS / VMM / SMM代码)执行黑客提供的恶意代码。 只需确保用您预先准备的代码的地址覆盖指令指针。

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

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

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

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

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

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM