How do I even pwn anything? Part 3— 32-bit vs 64-bit

Daryl 🥖
4 min readApr 15, 2024

--

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

Click here for part 2!

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.

Difference between 32-bit and 64-bit

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.

Generating sequence and crashing the binary

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.

Data that has been written into the return pointer

Using Pwndbg’s cyclic command, we can calculate that 72 bytes of padding is required before we overwrite the return pointer.

Calculating offset of padding

We can use readelf -s ret2win64 | grep win to find the address of the win function. In this case, it’s 0x40067d

Getting address of win function

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 .

Overwriting address with the address of the win function

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.

Breakpoint at return at 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.

Piping exploit into Pwndbg and hitting the breakpoint
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.

Inserting the RET gadget

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?

Click here for part 4!

--

--