zer0pts CTF writeup(3)
はじめに
これが最終回です。
janken vs yoshiking (crypto, 143pts, 44solves)
Yoshiking knows the flag. He will give the flag to who has gold luck. Let's play the janken with Yoshiking and prove your luck!
import random import signal from flag import flag from Crypto.Util.number import getStrongPrime, inverse HANDNAMES = { 1: "Rock", 2: "Scissors", 3: "Paper" } def commit(m, key): (g, p), (x, _) = key r = random.randint(2, p-1) c1 = pow(g, r, p) c2 = m * pow(g, r*x, p) % p return (c1, c2) def decrypt(c, key): c1, c2 = c _, (x, p)= key m = c2 * inverse(pow(c1, x, p), p) % p return m def keygen(size): p = getStrongPrime(size) g = random.randint(2, p-1) x = random.randint(2, p-1) return (g, p), (x, p) signal.alarm(3600) key = keygen(1024) (g, p), _ = key print("[yoshiking]: Hello! Let's play Janken(RPS)") print("[yoshiking]: Here is g: {}, and p: {}".format(g, p)) round = 0 wins = 0 while True: round += 1 print("[system]: ROUND {}".format(round)) yoshiking_hand = random.randint(1, 3) c = commit(yoshiking_hand, key) print("[yoshiking]: my commitment is={}".format(c)) hand = input("[system]: your hand(1-3): ") print("") try: hand = int(hand) if not (1 <= hand <= 3): raise ValueError() except ValueError: print("[yoshiking]: Ohhhhhhhhhhhhhhhh no! :(") exit() yoshiking_hand = decrypt(c, key) print("[yoshiking]: My hand is ... {}".format(HANDNAMES[yoshiking_hand])) print("[yoshiking]: Your hand is ... {}".format(HANDNAMES[hand])) result = (yoshiking_hand - hand + 3) % 3 if result == 0: print("[yoshiking]: Draw, draw, draw!!!") elif result == 1: print("[yoshiking]: Yo! You win!!! Ho!") wins += 1 print("[system]: wins: {}".format(wins)) if wins >= 100: break elif result == 2: print("[yoshiking]: Ahahahaha! I'm the winnnnnnner!!!!") print("[yoshiking]: You, good loser!") print("[system]: you can check that yoshiking doesn't cheat") print("[system]: here's the private key: {}".format(key[1][0])) exit() print("[yoshiking]: Wow! You are the king of roshambo!") print("[yoshiking]: suge- flag ageru") print(flag)
OT or NOT OTに続いてnc
で接続するタイプの問題です。ローカル再現は例によってfrom flag import flag
を書き換えてsocat
でどうぞ。
コードが長めですが、要約すると
は公開鍵、は秘密鍵
以下の流れでユーザ(我々) vs Yoshikingのじゃんけんが行われる
Yoshikingがランダム値を設定。これはじゃんけんの度に毎回変化する
Yoshikingは mod , mod を公開
の値は、「Yoshikingがグーなら1、チョキなら2、パーなら3」
を見た後で、ユーザ側は出す手を指定する
1回でも負ければ終了。引き分けは特に影響なし。通算100勝でflag入手
いつものように大文字は既知で小文字は未知です。
この段階で先に告白しておきますが、CTFに久しく参加していないせいでセンスが鈍った決定的な1問です。
以下で色々試行錯誤していますが、cryptoのプロフェッショナルの方々は温かい目で見守ってください。
アプローチ1
くどいですが、乗法準同型性があります。グーに対する暗号文が分かれば、2倍や3倍することでチョキ、パーに対する暗号文の計算は容易です。
もちろん、グーに限った話ではなく他の手にも言えます。なので、なんとか1回目は引き分けか勝利して
「の情報、および2回目以降の情報から
がに一致するかを求める(一致しなければで検証する)」
ことになります。いわゆるDDH仮定(Decisional Diffie-Hellman)を突破するのが目標です。
さて、ここで突然ですが楕円曲線の場合を考えます。つまり、楕円曲線上のPが与えられた場合に
「の情報、および2回目以降の情報から
がに一致するかを求める」
という問題を設定してみます。実は、これはペアリングという技術で解決可能です。
ペアリングとは楕円曲線上の2点に対する演算のことで、任意の整数係数に対し、
が成立します。これを使えば、
となるので、が成立すればとは一致すると言えます。
これを何とか上記に適用できないでしょうか…?
結論から言うと、無理です。
上の方でDDH仮定という言葉を使いましたが、 mod のような乗法巡回群でこのDDH仮定は存在していて、のような加法巡回群ではペアリングが存在している以上、DDH仮定は存在しません。
言い換えると、乗法巡回群でのペアリングが成立したら、DDH仮定(およびそれを安全性の根拠にしている暗号スキーム)が崩壊します。
アプローチ2
楕円曲線は忘れましょう。
改めてこのコードを見るとどうやらElGamal暗号に似た形をしています。
Wikipediaで何か有力な情報がないか見てみると、以下の文章がありました。
を素数とするとき、としてはいけない。このような群上ではDDH仮定が破れる。
あの、どう破れるかを見せてくださいWikipedia先生…
英語版Wikipediaも参照しましたが、めぼしい情報が得られません。こういう時は英語でググるとたいていヒットします。
"elgamal break ddh"で検索をかけて一番上に出てきたPDFにアクセスしてみます。
がっつり論文チックなやつですね。セクション名だけ読むくらいのめちゃくちゃ粗い粒度で目を通していくと…
4 BREAKING ELGAMAL
...
Example 4.1 (Breaking the DDH Assumption)
...
Example 4.2 (Breaking the ElGamal Scheme using a QR generator)
...
なる文字が。このセクションだけ熟読しました。どうやらQR、つまり平方剰余を絡めれば特殊なケースではDDHを破れる様子1。
論文はここでおしまいです。問題に戻りましょう。
以下では平方剰余を (Quadratic Residue), 平方非剰余を (Quadratic Non-Residue)で表します。
これらの性質として押さえておくべきポイントは
がともにならも
の一方だけがならは
がともにならは
だけです。に置き換えればわかりやすいかもしれません2。
なので、以下ではをそれぞれで表記します3。
さて、問題設定は
「からがのどれなのかを当てる」
でした。ここで、がのとき、なので
がならは偶数
がならは奇数
です。ここまでくるとほぼ解けたも同然です。ここで、わざと
「がすべてである」
状況を想定します。このとき、
→ | → | Yoshiking | |||||||
---|---|---|---|---|---|---|---|---|---|
奇数 | グー | ||||||||
奇数 | チョキ/パー | ||||||||
奇数 | チョキ/パー | ||||||||
奇数 | グー | ||||||||
偶数 | グー | ||||||||
偶数 | チョキ/パー | ||||||||
偶数 | グー | ||||||||
偶数 | チョキ/パー |
はから決定できます。その右隣の3つはそれぞれグー、チョキ、パーに対応していて、 との偶奇から計算できます。
これとが一致するものがYoshikingの手の候補となります。
ここで、今までずっと完全に特定できなくても、2通りに絞れれば負けはないことを失念していました。
引き分けは影響しないですからね。
さて、ソルバの枠組みは以下のようになります。
与えられた数がのどちらか、つまり平方剰余かどうかを判定する関数を作る
最初にが与えられるので、がになるよう祈る
1回目のじゃんけんの情報がになるよう祈る
1回目のじゃんけんを引き分けか勝ちになるよう祈る
ここまでくると、上の表に従ってコーディングすれば2回目以降で負けはない
…運の要素多くないですか?平方剰余と平方非剰余の割合はともに1/2なので、2.と3.の突破率は合計で約1/8です。
4.の突破率は言うまでもなく2/3で、トータルで1/12といったところでしょうか。
ですが、実際やってみるとわかりますが、なぜかほとんど突破しません4。
たぶん上の概算が甘いのでもう少し偏りがあるんだと思います。
なお、1.についてはLegendre記号を実装すればOKです。つまり、
mod
ならは平方剰余(ここでいうに相当)、なら平方非剰余()です。
アプローチ3
こちらは例によってrkm0959氏のwriteupです。
平方剰余に注目する点は一緒ですが、
「がで、がである」
状況を想定します。この時、は常にになり、とは常になので
→ | Yoshiking | |
---|---|---|
チョキ/パー | ||
グー |
確率もざっくり1/8で、場合分けも少ない。それに1回目から運ゲーに頼らなくていいのもエレガントすぎる。こういうセンスの良さは脱帽ですね。
自分が粉骨砕身して考えたシナリオが悲しくなってくるので、こちらをコーディングして終わろうと思います。
from pwn import * import sys # Legendre記号の実装 def legendre(a, p): assert(p % 2 == 1) val = pow(a, (p-1)//2, p) if val == 1: return 1 elif val == p-1: return -1 else: print('QR error') sys.exit() return 0 # 祈りパート。突破率約1/8 while True: # ローカル環境を想定 io = remote('127.0.0.1', 4000) io.recvline() msg = io.recvline()[:-1].decode().split(': ') # G, Pの取得 g, p = int(msg[2][:-7]), int(msg[3]) # 2, 3が平方非剰余になるかどうか if legendre(2, p) == -1 and legendre(3, p) == -1: print('2 and 3 is QNR! Good!') # Gが平方剰余になるかどうか if legendre(g, p) == 1: print('G is QR! Attack start!') break io.close() win = 0 while win != 100: io.recvline() msg = io.recvline()[:-1].decode().split('=')[1].split(', ') # C_1, C_2の取得(C_1は使わない) c1, c2 = int(msg[0][1:]), int(msg[1][:-1]) io.recv(100) # 上の表の部分を実装 # C_2が平方非剰余ならチョキで引き分け以上確定 if legendre(c2, p) == -1: io.sendline('2') # C_2が平方剰余ならパーで勝ち確定 elif legendre(c2, p) == 1: io.sendline('3') else: print('coding error') sys.exit() io.recvline() io.recvline() io.recvline() msg = io.recvline() if b'win' in msg: # デバッグ用。勝利数表示 print(io.recvline()) win += 1 io.recvline() io.recvline() print(io.recvline()) io.close()