How do I even pwn anything? Part 7— Return-Oriented Programming with 64-bit

Daryl 🥖
3 min readMay 15, 2024

--

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

Click here for part 6!

Course Link

You can find all the files required for the exercises on GitHub.

ROP With 64-bit Binaries

If you recall, 64-bit binaries store their parameters in registers instead of the stack.

Passing arguments in 32-bit vs 64-bit

To convert your script from 32-bit to 64-bit, all you have to do is find the gadget that pops the specific register. For example, if you wish to pass one argument, you would need to find the POP RDI; RET; gadget.

Once again, you can do it with ropper in Pwndbg with ropper -- --search "pop rdi; ret;"

Notice that the layout between 32-bit and 64-bits differs. The gadget is placed first, followed by the argument and the function address.

ROP stack layout for 64-bit binaries

Exercise

Return To Function — Chaining a bunch of gadgets to chain multiple functions together

You can find the binary and source code in at ret2func64. If you deployed the services locally, this exercise can be accessed using nc localhost 30005

#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.

Similar to the previous part, all we have to do is just get the addresses of each function and the string of “magicman”. Remember to use the p64 function instead of the p32 (because it’s 64-bit instead of 32-bit).

However, Pwntools provides a shortcut to doing all these. We can use ROP to generate our ROP chains automatically.

We can use rop.raw to add in raw bytes (for example, our padding). Then use rop.call to call the function name, and an array of arguments. The rop.dump() function dumps the ROP chain in an easy to read manner. The rop.chain() builds the actual ROP chain in bytes to send to the program.

As you can see, Pwntools does a lot of the magic for us.

Final ROP chain script

Solution can be found here.

As you might have noticed, this doesn’t seem very useful. Could we call a function that does not exist in the binary, such as system() and pass /bin/sh as our argument?

Click here for part 8!

--

--