Explore how to perform Linux Binary Exploitation from Capture-the-Flag (CTF) competitions.
Course Link
You can find all the files required for the exercises on GitHub.
32-bit vs 64-bit
There are a few notable differences between 32-bit and 64-bit machines.
Because the default De Brujin sequence generates a sequence for 4 byte substrings, we need to generate a sequence for 8 byte substrings for 64-bit machines (64 / 8 = 8).
from pwn import *
cyclic(50, n=8) # Generate sequence for 64-bit
cyclic_find(0x6167616161616161, n=8) # Find offset
16-byte stack alignment states that RSP has to be divisible by 16-bytes. If RSP is not divisible, you can use a RET gadget to align it.
Exercise
Return To Win — Hijacking the return pointer to control code execution
You can find the binary and source code in at ret2win64. If you deployed the services locally, this exercise can be accessed using nc localhost 30001
#include <stdio.h>
#include <stdlib.h>
void win() {
system("/bin/sh");
}
void vuln() {
char buffer[64];
gets(buffer);
}
int main() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
puts("Guess my name");
vuln();
puts("Wrong!");
return 0;
}
Solution
If you didn’t manage to pwn it, do not worry, look at the solution and see if you can replicate it for yourself.
Let’s use Pwndbg’s cyclic function to generate a De Brujin sequence. Since this is a 64-bit system, we use -n
to set the size of the unique subsequence to 8.
Looking into the value of the return value in Pwndbg’s context, we can see we have successfully overwritten the return pointer with part of our input.
Using Pwndbg’s cyclic command, we can calculate that 72 bytes of padding is required before we overwrite the return pointer.
We can use readelf -s ret2win64 | grep win
to find the address of the win function. In this case, it’s 0x40067d
We start by putting 72 bytes of padding, and then submitting the address of the win function in little endian format \x7d\x06\x40\x00\x00\x00\x00\x00
.
In usual fashion, exploitation is never smooth. We have calculated the correct padding and inserted the address of the win
function. We even piped cat
to keep STDIN open. So, what happened?
Remember, that 64-bit machines have their stack 16-byte aligned? This means that RSP has to be divisible 16 bytes! Let’s go to GDB and see what is the value of the stack.
Let’s pipe the exploit into a file so we can use it as user input in Pwndbg.
echo -en \
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x7d\x06\x40\x00\x00\x00\x00\x00" \
> exploit
Set a breakpoint at the return instruction of the vuln
function.
Now we let’s pipe the exploit into Pwndbg and run it. We hit the breakpoint. Please focus on the value of RSP.
Since 0x7fffffffc4e8
is not divisible by 16, if we continue execution by using continue
in Pwndbg, it will crash.
How can we solve this? Introducing, the RET gadget (more on gadgets later).
Remember in part 1, we showed that a return function pops a value off the stack. Since a 64-bit item on the stack is 8 bytes, popping an item off adds 8 to RSP and thus makes it 16-byte aligned.
0x7fffffffc138 + 8 = 0x7fffffffc140
Great! Now where can we find this RET gadget? Why don’t we just reuse the return address of the vuln
function. Therefore, 0x4006a2
is valid!
This works because when the return address returns, instead of directly returning to the win
function, it returns to its own return address, thus popping an item off the stack, aligning the stack to 16-bytes, and returns to the address of win
.
Do not worry if you are unable to visualise gadgets. In later parts, there will be a huge focus on gadgets. For now, understand that the RET gadget allows you to heed to the 16-byte stack alignment.
Now we can interact with the shell! Solution can be found here.
Of course, this assumes there exists a win function to jump to. But what if there is no win function? Is it possible to create one ourselves?