The stack is a memory segment just like the heap, or bss. It's used as a temporary frame
during function calls, and for local variables inside a function. Let's see how it works.
First, below is an illustration of the memory segmentation:
It's named stack because of how it works.
It follows the LIFO principle,
which is a data structure where we can push and pop objects to/from. The last pushed object will be the first one to be popped
off the stack. The first pushed object will be the last one to be popped off.
The stack grows towards low addresses, while the heap grows towards high ones.
For a simple C function call like strcmp(str1, str2);
, the compiler
would produce something like this:
Now some explanations. The Extended Base Pointer (EBP) register points to the stack base while the Extended Stack Pointer (ESP)
points to its top. Function arguments are pushed onto the stack using the simple push
instruction.
So when you push an arg onto the stack, ESP is decremented by 4.
In this case, str1
will be at the top of the stack, then we'll find str2.
Then, we find the actual function call. The call
instruction, amongst other things, pushes the return address on the stack.
This is the address the callee will jump to when its job is done.
After that, we jump straight to the called function. Finally, we clear the stack from the arguments we previously pushed.
To do that, we need to add 4*2 to esp (since the stack grows towards lower addresses).
Now let's have a quick overview of what happens inside a function. Say int add(int a, int b){return a+b;}
.
The two first instructions create a stack frame. We're now able to access arguments using ebp
, since esp
is
always moving as we push/pop stuff from the stack.
The first argument is at ebp+8
, the second at ebp+12
... Why ? Because we saved ebp
which is at the top of the stack. Right before it, we have the return address, and then we find the function parameters.
At the end of the function, ebp
is restored, and the ret
instruction pops the
return address from the stack and jumps to it.
This last step is where the fun part starts. By overflowing a local variable, we gain control over the return address.
Let's see how with the following vulnerable code:
This program really does nothing, except passing argv[1]
to a function that will call strcpy
.
That's enough for us.
Our goal here is to call the unreachable
function.
Also, since this paper uses very basic exploitation techniques which are now mostly outdated, we'll
need to turn some features off (ASLR, stack canary) for our exploit to work. Therefore compile this example program as
follows: gcc sbof.c -o sbof -m32 -no-pie -fno-stack-protector
The strcpy(char *dest, const char *src)
function copies the string src
to the buffer dest
. It does not check for overflow. The buffer in the regname
function
is 16 bytes, what if we enter a name longer than 16 characters ?
./sbof lol
Name 'lol' successfully registered
./sbof AAAAAAAAAAAAAAAAAAAA
Name 'AAAAAAAAAAAAAAAAAAAA' successfully registered
Oh well it looks ok. Let's try with a name way longer.
./sbof AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[1] 4309 segmentation fault (core dumped) ./sbof AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
It crashed. We've just smashed important stuff from the stack, like the return address, so the program jumps to an invalid location.
Let's see what happens in the stack when ran with ./sbof XYZ
We can see the argument we entered on top of the stack, right before the call to regname
.
Let's follow this call until the vuln.
The call to strcpy
is about to be executed. The stack contains both the destination and the source addresses.
We learn strcpy
will start copying from 0xffe22a20
, which, if you look at the stack, has been
zero'ed.
If you get back to the previous screenshot, you notice the next address after the call to regname
is
0x08048569
. This is where we'll be jumping after execution of this function.
Now look at the stack, you can see this address. Our goal is to smash everything before it and then write
our new return address, that is the address of unreachable
: 0x08048605
.
There are 28 bytes we need to smash before writing our return address.
Finally, our payload is: 'A'*28 + '\x05\x86\x04\x08'.
Let's try:
./sbof `perl -e 'print "A"x28 . "\x05\x86\x04\x08"' `
Name 'AAAAAAAAAAAAAAAAAAAAAAAAAAAA' successfully registered
WIN !
[1] 10745 segmentation fault (core dumped) ./sbof `perl -e 'print "A"x28 . "\x05\x86\x04\x08"' `
Got it! We managed to overwrite the return address. Of course this is a basic example, but it shows
that we can achieve way more using this technique, like injecting and running our own code for instance.