How do I even pwn anything? Part 6— Return-Oriented Programming

Daryl 🥖
6 min readMay 15, 2024

--

Explore how to perform Linux Binary Exploitation from Capture-the-Flag (CTF) competitions.

Click here for part 5!

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);
}
Stack (32-bit) and Register (64-bit) layout for passing parameters

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);
}
Stack layout when returning for func()

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!

Chaining functions with pop-ret gadget

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.

Passing 2 arguments into func1()

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.

Finding POP-RET Gadgets

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.

Finding address of string in binary
Passing string as argument

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
Final ROP chain script

Solution can be found here.

In the next part, we will learn how to do the same thing but with 64-bit systems.

Click here for part 7!

--

--