找到你要的答案

Q:An intended buffer overflow that does not always cause the program to crash

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

Consider The following minimal C program:

Case Number 1:

#include <stdio.h>
#include <string.h>

void foo(char* s)
{
    char buffer[10];
    strcpy(buffer,s);
}

void main()
{
    foo("01234567890134567");
}

This doesn't cause a crash dump

If add just one character, so the new main is:

Case Number 2:

void main()
{
    foo("012345678901345678");
                          ^   
}

The program crashes with a Segmentation fault.

Looks like additionally to the 10 characters reserved in the stack there's an additional room for 8 additional characters. Thus the first program doesn't crash. However, if you add one more character you start accessing invalid memory. My questions are:

  1. Why we do have these additional 8 characters reserved in the stack?
  2. Is this related somehow with the char data type alignment in the memory?

An other doubt I have in this case is how does the OS (Windows in this case) detects the bad memory access? Normally as per the Windows documentation the default stack size is 1MB Stack Size. So I don't see how the OS detects that the address being accessed is outside the process memory specially when the minimum page size is normally 4k. Does the OS use the SP in this case to check the address?

PD: I'm using the following environment for the testing
Cygwin
GCC 4.8.3
Windows 7 OS

EDIT:

This is the generated assembly from http://gcc.godbolt.org/# but using GCC 4.8.2, I can'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's happening in the foo function and why the extra char causes the seg fault

    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

考虑以下最小C程序:

案例编号1:

#include <stdio.h>
#include <string.h>

void foo(char* s)
{
    char buffer[10];
    strcpy(buffer,s);
}

void main()
{
    foo("01234567890134567");
}

这不会导致崩溃转储

如果只添加一个字符,那么新的主:

案例编号2:

void main()
{
    foo("012345678901345678");
                          ^   
}

该程序崩溃与分割故障。

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

  1. Why we do have these additional 8 characters reserved in the stack?
  2. Is this related somehow with the char data type alignment in the memory?

另一个疑问,我在这种情况下,如何操作系统(Windows在这种情况下)检测坏内存访问?通常根据Windows文件的默认堆栈大小为1MB的堆栈大小。所以我不知道如何操作系统检测到地址访问以外的过程记忆特别当最小页面大小通常是4K。没有操作系统使用在这种情况下,SP检查地址吗?

PD: I'm using the following environment for the testing
Cygwin
GCC 4.8.3
Windows 7 OS

编辑:

这是生成的组件从http://gcc.godbolt.org/ #但使用GCC 4.8.2,我不能看到可用的编译器gcc 4.8.3。但我猜生成的代码应该是相似的。我建立了没有任何标志的代码。我希望有人能用汇编知识什么是在foo函数发生了什么额外的字符导致段错误一些轻

    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
answer1: 回答1:

I believe you understand that you have implemented something that leads to Undefined Behavior. So it is hard to answer why it fails with the extra string and doesn't with the original. It is probably related to the internal compiler implementation + affected by the compilation flags (like alignments, optimizations, etc.).

You can try disassembling the binary or creating assembly code and seeing where exactly the buffer is put on the stack. You can do the same with different optimization levels to inspect the changes in the assembly code and the behavior.

how does the OS (Windows in this case) detects the bad memory access? Normally as per the Windows documentation the default stack size is 1MB Stack Size. So I don't see how the OS detects that the address being accessed is outside the process memory specially when the minimum page size is normally 4k. Does the OS use the SP in this case to check the address?

The OS doesn't monitor the code you execute. The HW (CPU) does (since it executes this code). Once your code tries to access an address which was not allocated for your process (was not mapped by the OS for your program) the OS will get an indication since the HW will fire a #PF (page fault) exception. Another case is that you try to access an address which was allocated for you but with improper permissions (for example you try to execute binary data from a DATA page which has no 'execute' permission) or go to the CODE page but with a wrong offset and the instruction that you read doesn't exist or (even worse) it exists and decodes to something you don't expect (did we say Undefined Behavior before?).

In general your code most likely doesn't fail on strcpy (it can if you write enough data to access some forbidden addresses but most likely it is not the case) - it fails when it returns from the foo function. strcpy just overwrote the next instruction pointer which points to the next instruction after the foo function. So the instruction pointer is filled with the data from the "012345678901345678" string and tries to fetch the next instruction from the 'junky' address and fails due to the mentioned above reasons.

This "method"/bug is called a "buffer overflow attack" and widely used among hackers to make your code (and more often OS/BIOS/VMM/SMM code which is executed with higher privileges) execute malicious code provided by the hacker. Just make sure to overwrite the instruction pointer with the address of the code that you prepared in advance.

我相信你明白你已经实施了一些导致不明确的行为。所以很难回答为什么它与额外的字符串失败,不与原来的。它可能与内部编译器实现+编译标志(如对齐,优化等)的影响。

你可以尝试拆卸或创建的二进制汇编代码和看到完全缓冲放在堆栈。可以使用不同的优化级别来检查程序集代码和行为的更改。

how does the OS (Windows in this case) detects the bad memory access? Normally as per the Windows documentation the default stack size is 1MB Stack Size. So I don't see how the OS detects that the address being accessed is outside the process memory specially when the minimum page size is normally 4k. Does the OS use the SP in this case to check the address?

操作系统不监视您执行的代码。硬件(CPU)做(因为它执行此代码)。一旦你的代码试图访问一个地址不分配给你的过程(不是由你的程序的OS操作系统映射)会得到一个指示由于硬件将火# PF(页故障)例外。另一种情况是,你试图访问一个地址,是分配给你但不当的权限(例如您尝试执行的二进制数据从一个数据页没有“执行”的权限)或去代码页,但错误的偏移和指导你读不存在或(更糟糕)它存在和解码的东西你别指望(没有我们说的未定义行为吗?)。

一般来说你的代码可能不在strcpy(它可以如果你写了足够多的数据来访问一些禁止的地址,但最有可能的情况是不)-它失败时,它从foo函数返回。strcpy只写下一个指令指针指向下一条指令的foo函数后。因此,指令指针充满了从“012345678901345678”的字符串数据,试图从“垃圾”地址取下一条指令并没有因上述原因。

这个“法”的错误称为“缓冲区溢出攻击”和广泛使用的黑客之间使你的代码(通常操作系统DSP/BIOS / VMM / SMM是执行高特权代码)执行恶意代码的黑客提供。只要确保用你预先准备好的代码的地址重写指令指针。

answer2: 回答2:

The official, system agnostic answer is:

Your code writes data beyond the end of the destination array, the behaviour is undefined, anything can happen, including nothing at all or space probe crashed on Mars surface. Your observing no noticeable effect up to 8 bytes beyond the end of the buffer and a crash with a segmentation fault beyond that are possible effects of undefined behaviour, well within the expected outcome.

The extra implementation details you are interested in:

Actual behaviour will depend on many circumstances, for example which compiler you use, which OS and ABI (Application Binary Interface) etc.

Your program is compiled and executed in a 64 bit Windows environment. In this environement, the stack is kept aligned on 64 bit boundaries, or possibly 16 byte boundaries to allow direct loading and storing of the MMX registers from/to stack locations. The array buffer[10] occupies 16 bytes on the stack. Given how the stack is established on this processor, it will be located just below locations used by function foo to store any saved registers and the return address into the caller function main. Whether the extra 6 bytes are before or after the array is a choice for the compiler to make. It could use this space for other local variables or just ignore it.

Writing beyond the end of buffer may be harmless for up to 6 bytes if the padding is after the array, might not have any noticeable effect for another 8 bytes (clobbering the saved rbp register, which is unused in main after the call), but will start having bad side effects beyond that, because you will be overwriting the return address.

When you overwrite the return address, the processor will not return from function foo to the caller main, but to whatever address is stored on the stack and was corrupted by the offending code. If this corrupted address points to executable code, that code will be executed with potential harmful consequences... Hackers do exactly this: they carefuly craft an exploit that manages to store some harmful code at a known location in executable memory and take advantage of the buffer overflow code to store the address of said code in the stack location for the return address.

In your case, the location pointed to by the corrupted return address might not be executable, triggering the segmentation fault you observe.

I suggest your try and compile your code on this site to see the actual assembly code generated under various compiler options: http://gcc.godbolt.org/#

官方系统不可知论者的回答是:

你的代码写的数据超出目标数组的结束,行为是不确定的,任何事情都可能发生,包括什么都没有或太空探测器坠毁在Mars表面。您的观察没有明显的效果高达8字节以外的缓冲区结束和崩溃的分割故障超出了可能的影响未定义的行为,以及在预期的结果。

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

实际的行为取决于许多情况下,例如,你使用的操作系统和编译器,ABI(应用程序接口)等。

在64位Windows环境中编译和执行程序。在这样的环境,堆栈保持对齐在64位或16字节边界的边界,允许直接加载和存储的MMX寄存器/堆栈位置。阵列缓冲[ 10 ]占用堆栈上的16字节。鉴于堆栈是建立在这个处理器,它将位于下面的函数foo用来存储任何保存的寄存器和返回地址到调用者函数主要地点。无论是额外的6字节之前或之后的数组是编译器的选择。它可以使用这个空间为其他局部变量或忽略它。

写作结束后缓冲可能如果填充后的阵列最多6个字节是无害的,不可能有另一个8字节的任何明显的影响(痛击保存登记,未使用RBP,主要调用之后),但会有不良副作用之外,因为你将重写返回地址。

当你将改写返回地址,处理器将不会返回到调用主函数foo,但是任何地址存储在堆栈和被冒犯的代码损坏。如果该损坏的地址指向可执行代码,则该代码将执行潜在的有害后果…黑客这样做:他们精心制作一个漏洞,设法在一个已知位置可执行内存存储一些有害代码和利用缓冲区溢出代码存储地址码的返回地址堆栈中的位置表示。

在您的情况下,被损坏的返回地址指向的位置可能不可执行,引发您观察到的分割错误。

我建议你试着编译你的代码在这个网站上看到实际的汇编代码的编译器选项下产生的:http://gcc.godbolt.org/ #

answer3: 回答3:

The next entry in stack is function address witch in 64 bit system must be aligned to 8, thus there is enough space for 16 characters.

You can verify this by declaring an int variable after array. Int will be aligned to 4 and there will be less space for characters so program will crash on lower number.

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

您可以验证通过声明一个int变量后的数组。Int将与4有特征空间那么程序将崩溃的低一些。

c  memory  stack  buffer-overflow