x64 ret2libc
If a binary does not have enough useful gadgets but imports the libc library, we can try to return to libc and call gadgets like system() and execve().
Last updated
If a binary does not have enough useful gadgets but imports the libc library, we can try to return to libc and call gadgets like system() and execve().
Last updated
Take control of the rsp regiter
Leak a libc function address and the libc version
Use that address to calculate libc's base offset
Call any gadgets within libc
Notice that the arguments are provided by the registers in the x64 architecture, not by the stack.
I like to work with examples, so for this explanation I'll use the ropme challenge from hackthebox, which is the most simple ret2libc example there could ever be, the chall is currently retired so you'll need a VIP subscription if you want to use it through this paper.
Let's say our binary has an input field vulnerable to a buffer overflow in which we can overwrite address on the stack until we hit the regiters, our task would be to find the exact offset in which we start writing to rsp, so we can control the flow of the program.
In the example above I created a pattern to see where exactly I started writing to rsp,
Then I fed the value on rsp to know exactly where it is overwrote, and it tells me the offset is 72, which means that if I send 72 A's and 8 B's the rsp will be all B's.
And there it is :) Now let's start developing our exploit, I'll be using the pwntools library for automation and organization.
To leak the libc version we need to get the address of a function used by the program, so we can disassable the binary with the following command:
And we see that a few functions were imported:
We can leak the address of a function if we print it's value in the GOT table, we can call the puts function to print that address, so we need to first put the address we want in the rdi register, which is where our first argument goes, and then call the puts function. So our chain will look like:
Notice that we need to call main so the program flow returns to the beginning, that way we'll be able to get the output of our call to puts() and continue with the attack. This is important to understand, since ASLR is enabled on the remote instance we need to complete the attack in only one execution, because the addresses we leaked will be reset in the next run.
We'll need a pop rdi gadget, we can look for one of those with ropper,
And here is our gadet :) Now, let's jump to our exploit's development.
Pwntools ships already with functions to simplify that process:
The code above connects to the service running the vulnerable binary then forces it to print the address of the puts function by waiting for the input to be requested, then sending the chain I mentioned before.
Running the script we get the address of the puts function, now we need to feed it to a database that will search for that occurrence in multiple versions of libc.
https://libc.nullbyte.cat/?q=puts%3A0x7f5fa2da5690
As we know, our program's architecture is amd64, so we can ignore any other matches, leaving us with libc6_2.23-0ubuntu10_amd64 and libc6_2.23-0ubuntu11_amd64, and those two end up being the exact same, so we already know what version we need to use for the gadgets in our exploit.
As the addresses are reset upon finishing execution, we need to leak the address we want and calculate the offset in every run. So we'll just append the exploit to our previous code. The next steps we need to take are:
Subtract the offset of our function from it's address, so we get libc's base address.
Add the address of any gadgets we want to use from libc.
The libc database we're using already provides that offset,
Let's say we want to call system("/bin/sh"), then we'll need to put the address of a string with the value of "/bin/sh\x00" in the rdi register and then call the system() function, according to the database, those addresses are, respectively, libc's base address + 0x18cd57
(offset of the string), and libc's base address + 0x045390
(offset of system()), so our next chain will look like:
appending that chain to our exploit code,
An this will pop us a shell! Please notice, some times the /bin/sh address is a little bit off and you'll get something like: "%s%s[...]%s%s No such file or directory", this means you're 64 bytes ahead from where you should be, all you need to do is subtract 64 from your bin_sh address.
Also there are other ways of doing this without system(), there is something called one gadget, that is, basically, any address on libc that pops a shell just for being called, for example, one that contains execve("/bin/sh"). Running the one_gadget command against the libc we downloaded from the database we found a few offsets:
So replacing our chain with:
We'll also get a shell, And here is the final exploit code:
Simple enough. Thanks to everyone that followed along to this point, I hope everyone is enjoying the content!