I am stuck on the following problem.
Consider this code:
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
void runme() {
printf("Hello, world!n");
exit(0);
}
int main(char argc, char **argv) {
if (argc != 2) return 1;
void (*buffer[8])(void);
buffer[(int8_t) argv[1][0]] = runme;
return 0;
}
The program reads the first argument, that will serve as an index. If you write the right number you will be able to overwrite the return address, with the address of the function runme.
I understand that if I overwrite the return address, when main finish it’s execution, the program will continue executing the runme function. So with that behaviour in mind, I have calculated which number should I introduce as a first argument.
Then if I try the following command: ./program "$(echo -ne 'x0b')"
the output is segmentation fault.
I have tried to debug it in gdb and the output is the following (segfault also):
In addition, I have looked at dmesg’s output, which is the second image.
Yesterday I was looking through the internet what could have caused the segmentation fault but I found nothing. What am I missing? Does libc developers have included a new protection mechanism?
My machine information
- Amd Ryzen 5700U
- Ubuntu 22.04
- Gcc 11.03
- Binutils 2.38
- Glibc 2.35
2
Answers
That version of the code works "as intended" (between quotes, because, for this kind of code, it depends on who is intending…).
The only difference is the
puts
. It works also if you printf anything (but an empty string, but in this case gcc acts as if there were no printf at all).But not for any function call. For example I’ve tried with a
fflush(stdout)
at this place, and it is still segfaulting (I’ve chose this just to be sure that it wasn’t a strange thing related to usage of any FILE functions).Old answer (unaware that it was on purpose)
What a strange way to pass integer argument.
I was about to reply "
argv[1]
" is a string and cannot be cast to an int, when I saw that you were usingargv[1][0]
, which is an integer (a char). Then to say "you’ll get the ascii code of 1st char of whatever argument you pass to your program, which is too big". But even that, you already know, since you pass a specific, non-ascii byte as an argument.But, well, that byte is
x0b
which is 11. And you allocate an array of 8 pointer. So, still, it is normal that you have a seg fault.The crash is caused by stack misalignment. See Why does the x86-64 / AMD64 System V ABI mandate a 16 byte stack alignment?
The jump-by-ret results in entering
runme
with the stack misaligned, which violates the ABI, and some libc functions do in fact break when called with a misaligned stack. It doesn’t happen on my system, but apparently yourmalloc
implementation (whichprintf
calls) requires stack alignment.Disassembling the code bytes, the faulting instruction is
movaps [rsp+0x10], xmm1
, whose memory operand must be aligned to 16 bytes. However,rsp
has a hex value ending in8
, sorsp+0x10
is not aligned.I don’t off the top of my head see a simple way to have the exploit work around this.
Here is a brief explanation of the principle of stack alignment and how it leads to the crash.
It simply means that when the
movaps
instruction is executed, the value inrsp
is not a multiple of 16 (which is mathematically equivalent to saying that its last hex digit is not 0). The compiler is careful to ensure that it generates code that always adjusts the stack pointer by multiples of 16, such that if it was properly aligned by the caller of this function, then the calls made by this function will also occur with proper alignment.The rule set out by the x86-64 SysV ABI, which Linux compilers conform to, is that
rsp
must be a multiple of 16 (i.e. must end in 0) when a call instruction is issued. This means that when the called function begins to execute, thenrsp
is 8 less than a multiple of 16 (i.e. ends in 8), because of the 8-byte return address that was pushed by call. So whenmain
reaches itsret
instruction, with your modified return address on the stack,rsp
likewise ends in 8 (all stack modification done withinmain
has been undone at this point). Theret
pops the stack once, so you end up atrunme
withrsp
ending in 0, which is wrong.This "parity error" propagates down through
printf
and intomalloc
. The_int_malloc
function expects to be entered withrsp
ending in 8, so it presumably subtracts an additional 8 bytes (possibly just by pushing) somewhere before executingmovaps
. As such,rsp
would end in 0 at that point and all would be well. But since the situation was reversed on entry torunme
, it stays reversed._int_malloc
got entered withrsp
ending in 0 instead, and so its subtraction of 8 bytes left it not ending in 0 whenmovaps
executed.To your comment: At the level of C, stack alignment is the job of the compiler, not the programmer. So a C program can freely define a local array of size 17, and the compiler will then have to know to actually adjust the stack pointer by 32 bytes, leaving the other 15 bytes unused (or using them for other local variables). It isn’t something that a C programmer normally has to worry about, but it becomes relevant when you are hacking internals like this.