This is continuation of my guide on binary exploitation, in this part we are going to cover return-to-libc attack which was invented to defeat DEP/Non executable stack. As you can remember, we have used the fact that stack is executable in the previous part of this guide.
Cybersecurity is a big war, when there is a protection in place, after some time someone finds a way around it, so in this post I'll show you what the bad guys figured out to exploit stack based buffer overflow even when DEP/NX is turned on.
For example source code we will use exactly the same one as previously.
Only compilation parameters will change, we are goin to remove execstack flag and add '-m32' switch, because I'll show you how to bypass NX on x86 (32 bit):
Why 32bit vs 64bit matter at all? Isn't it all the same thing when it comes to exploitation?
Turns out that not really, the reason behind that are different calling conventions in those architectures. I've already linked a great paper by Agner Fog where you can find (16th) page called 'Function calling conventions'. Looking at the table provided it's easy to spot the differences. In C programming language 'stdcall' is the call that we are interested in, it's a convention in which all the arguments are pushed on to the stack.
So stack after calling a function with following signature
void function(int a, int b, int c);
would look like this:
| ebp | eip | c | b | a | ...
We are going to use this feature in our exploit.
In short, we want to override eip with address of some function from libc(standard C library) and pass arguments to it using stack.
Let's say we want to return to system() function.
( Quick look into gdb and we know what the address of system() is)
So for this to happen, our stack must be overwritten to look like so:
| ebp | eip | c | b | a | ...
| JUNK | 0xf7e28d80 | JUNK(new eip) | argv[1] | ...
What will happen is that after function returns, it will return to system() function which needs one argument from the stack so it will pop that from it. If we make it valid argument, call will be successful.
Because we are not calling our new function with call instruction, we need to supply next eip on the stack by ourselves to keep offsets to the arguments right.
Our goal will be to call:
system("/bin/sh"); or system("sh");
We will use latter option (I leave first one as exercise for the reader (Tip: Put it on the stack or inside env variable in case of self exploitation)).
For that we will use very convenient feature that pwndbg has of finding sequence of bytes inside a binary.
We got everything we need:
system(): 0xf7e28d80
'sh': 0xf7dfc5f7
You should already know how to find out which bytes of overflown buffer are landing inside eip. If you don't remember check out part 1.
Take note of dq -> dd change, that is caused by the pointer size change from quad word to double word (8 bytes -> 4 bytes).
And we got shell again! :)
Exercise: try to return from shell cleanly, without segmentation fault error. (Tip: what is the return address of system() call, how about calling exit() after all ?).
And there you have it, you have survived another part of this tutorial, good job!
5. DEP/NX
Today all of the standard software is compiled without executable stack, this is a protection that was introduced as a response to the attack that I've showed you the last time. Do you recall '-z execstack' option? If we were to use the last exploit on binary which is not compiled with that flag, we wouldn't get shell at all. Processor when jumping on to the stack would know that this memory shouldn't be executed, this is rw(read and write) section only.Cybersecurity is a big war, when there is a protection in place, after some time someone finds a way around it, so in this post I'll show you what the bad guys figured out to exploit stack based buffer overflow even when DEP/NX is turned on.
For example source code we will use exactly the same one as previously.
Only compilation parameters will change, we are goin to remove execstack flag and add '-m32' switch, because I'll show you how to bypass NX on x86 (32 bit):
gcc exploit102.c -o exploit102 -fno-stack-protector -m32
Why 32bit vs 64bit matter at all? Isn't it all the same thing when it comes to exploitation?
Turns out that not really, the reason behind that are different calling conventions in those architectures. I've already linked a great paper by Agner Fog where you can find (16th) page called 'Function calling conventions'. Looking at the table provided it's easy to spot the differences. In C programming language 'stdcall' is the call that we are interested in, it's a convention in which all the arguments are pushed on to the stack.
So stack after calling a function with following signature
void function(int a, int b, int c);
would look like this:
| ebp | eip | c | b | a | ...
We are going to use this feature in our exploit.
6. Return-to-libc
Knowing that we cannot execute our code from the stack, we can use something called return-to-libc attack, which is simplified version of Return Oriented Programming(ROP).In short, we want to override eip with address of some function from libc(standard C library) and pass arguments to it using stack.
Let's say we want to return to system() function.
( Quick look into gdb and we know what the address of system() is)
So for this to happen, our stack must be overwritten to look like so:
| ebp | eip | c | b | a | ...
| JUNK | 0xf7e28d80 | JUNK(new eip) | argv[1] | ...
What will happen is that after function returns, it will return to system() function which needs one argument from the stack so it will pop that from it. If we make it valid argument, call will be successful.
Because we are not calling our new function with call instruction, we need to supply next eip on the stack by ourselves to keep offsets to the arguments right.
Our goal will be to call:
system("/bin/sh"); or system("sh");
We will use latter option (I leave first one as exercise for the reader (Tip: Put it on the stack or inside env variable in case of self exploitation)).
For that we will use very convenient feature that pwndbg has of finding sequence of bytes inside a binary.
We got everything we need:
system(): 0xf7e28d80
'sh': 0xf7dfc5f7
You should already know how to find out which bytes of overflown buffer are landing inside eip. If you don't remember check out part 1.
7. Another exploit!
import struct def dd(v): return struct.pack("I", v)
buf = "" buf += dd(0xf7e28d80) buf += dd(0xdeadbeef) buf += dd(0xf7dfc5f7)
with open("payload", "wb") as f: f.write("A" * 1036) f.write(buf)
Take note of dq -> dd change, that is caused by the pointer size change from quad word to double word (8 bytes -> 4 bytes).
And we got shell again! :)
Exercise: try to return from shell cleanly, without segmentation fault error. (Tip: what is the return address of system() call, how about calling exit() after all ?).
And there you have it, you have survived another part of this tutorial, good job!