assemblygccx86-64function-callstack-smash

why stack overflow attacks (modifying the returning address of a function call) caused segmentation fault in `_int_malloc`


I'm learning the structure of stack frames. And trying to implement a function that can call another function without an explicit call in C by modifying the returning address (in its stack frame) of the function call.

The code is like the following:

#include <stdio.h>
#include <stdlib.h>

void malfunc() {
    puts("hello world");
    exit(0);
}

void set_arr() {
    size_t a[2];
    a[0] = 114;
    a[1] = 514;
    a[3] = (size_t)malfunc;
    // a[3] points to the position of returning address in stack frame
}

int main() {
    set_arr();
    return 0;
}

In my expectation, the string hello world should be printed because the returning address of set_arr is modified to malfunc() by the assignment a[3] = (size_t)malfunc.

The stack frame for set_arr() should look like:

a[0]
------------------------------------
a[1]
------------------------------------
previous base pointer (rbp of main)  <--- current rbp, a[2]
------------------------------------
original return address (main)       <--- a[3], modified to malfunc

This code worked perfectly in Compiler Explorer, the link is here.

However, if I compile this code locally with the following compile options

gcc stk_ov.c -o stk_ov -fno-stack-protector -ggdb3

and run the code, a segmentation fault will be thrown.

And if I use gdb to catch the segmentation fault, I get the following output:

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7e2d540 in _int_malloc (av=av@entry=0x7ffff7fa2c80 <main_arena>, bytes=bytes@entry=640) at ./malloc/malloc.c:4375
4375    ./malloc/malloc.c: No such file or directory.
(gdb) bt
#0  0x00007ffff7e2d540 in _int_malloc (av=av@entry=0x7ffff7fa2c80 <main_arena>, 
    bytes=bytes@entry=640) at ./malloc/malloc.c:4375
#1  0x00007ffff7e2da49 in tcache_init () at ./malloc/malloc.c:3245
#2  0x00007ffff7e2e25e in tcache_init () at ./malloc/malloc.c:3241
#3  __GI___libc_malloc (bytes=bytes@entry=1024) at ./malloc/malloc.c:3306
#4  0x00007ffff7e07c24 in __GI__IO_file_doallocate (
    fp=0x7ffff7fa3780 <_IO_2_1_stdout_>) at ./libio/filedoalloc.c:101
#5  0x00007ffff7e16d60 in __GI__IO_doallocbuf (
    fp=fp@entry=0x7ffff7fa3780 <_IO_2_1_stdout_>) at ./libio/libioP.h:947
#6  0x00007ffff7e15fe0 in _IO_new_file_overflow (
    f=0x7ffff7fa3780 <_IO_2_1_stdout_>, ch=-1) at ./libio/fileops.c:744
#7  0x00007ffff7e14755 in _IO_new_file_xsputn (n=11, data=<optimized out>, 
    f=<optimized out>) at ./libio/libioP.h:947
#8  _IO_new_file_xsputn (f=0x7ffff7fa3780 <_IO_2_1_stdout_>, data=<optimized out>, 
    n=11) at ./libio/fileops.c:1196
#9  0x00007ffff7e09f9c in __GI__IO_puts (str=0x555555556004 "hello world")
    at ./libio/libioP.h:947
#10 0x0000555555555180 in malfunc () at stk_ov.c:6
#11 0x0000000000000001 in ?? ()
#12 0x00007ffff7db2d90 in __libc_start_call_main (
    main=main@entry=0x5555555551b0 <main>, argc=1, argc@entry=-11536, 
    argv=argv@entry=0x7fffffffd408) at ../sysdeps/nptl/libc_start_call_main.h:58
#13 0x00007ffff7db2e40 in __libc_start_main_impl (main=0x5555555551b0 <main>, 
    argc=-11536, argv=0x7fffffffd408, init=<optimized out>, fini=<optimized out>, 
    rtld_fini=<optimized out>, stack_end=0x7fffffffd3f8) at ../csu/libc-start.c:392
#14 0x00005555555550a5 in _start ()

However, if I pop the rbp register at the begging of malfunc, everything worked fine:

void malfunc() {
    asm volatile("pop rbp");
    puts("hello world");
    exit(0);
}
malfunc:
        push    rbp
        mov     rbp, rsp
        pop     rbp  ; newly added
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        mov     edi, 0
        call    exit

So I'm confused by the difference between these two and what caused this segmentation fault.

For the original version, after entered malfunc, the rbp register will be set to the stack pointer (mov rbp, rsp). But after I poped it, it stayed the same.

My environment is listed on the following, hope they will be useful:


Solution

  • Probably puts in recent builds of glibc segfaults on a misaligned stack pointer, especially on the first call where it has to allocate some buffers. Check what instruction faulted, it's probably movaps or movdqa to or from the stack, as in glibc scanf Segmentation faults when called from a function that doesn't align RSP

    (Update: thanks to Nate for confirming the segfault is on a movaps inside malloc.)


    GCC code-gen for malfunc of course assumes it will be entered with RSP % 16 == 8 from call pushing a return address (in a caller that has RSP % 16 == 0 before a call, as required/guaranteed by the ABI). But returning from a function will restore RSP % 16 == 0, so you're violating the ABI if that return address is the top of a function.

    One workaround could be to inject a second return address, so you initially return to a ret instruction anywhere (doing rsp-=8 to get there), and that ret does another rsp-=8 while popping malfunc into RIP.

    Or much simpler, instead of returning to the top of malfunc, return to the instruction after its push rbp. You don't need it to return to anywhere, so that push was useless. So is the mov rbp, rsp in this case; it doesn't deref RBP before making the function call you want. The RBP value is irrelevant, all that matters is RSP being an odd or even multiple of 8, i.e. how it's aligned relative to a 16-byte boundary.

    So you can actually skip the whole prologue, and just overwrite the return address with the address of the mov edi, OFFSET FLAT:.LC0 instruction.