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:
gcc (Ubuntu 11.2.0-19ubuntu1) 11.2.0
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.