大阪大学のCTF サークル Wani Hackaseさんが開催してくださりましたWaniCTFのWriteupです!初心者向けのCTFということもあり、かなり面白かったです。
今回はCrypto問について書いていきます。他の分野も少しだけ解けたのですが、もう少し出来るようになりたいですね。
Crypto問のOUCSは解き方に加え証明も書いているので、是非とも読んでいただければと思います。分かりやすく書いたつもりですが、不明瞭な点がありましたら、コメントしていただけますと嬉しいです。
- Simple conversion [Begineer]
- Easy [Easy]
- Extra [Normal]
- Can't restore the flag? [Hard]
- OUCS [Very hard]
- Problems & Solver
Simple conversion [Begineer]
戻し方を忘れました…
ファイル:cry-simple-conversion.zip
convert.pyを見るとバイト列を整数にしているようですね。とりあえずlong_to_bytesしてみたらflagが出てきました。little endianならto_bytesを使えば良さそうですね。
from Crypto.Util.number import * x = 709088550902439876921359662969011490817828244100611994507393920171782905026859712405088781429996152122943882490614543229 print(long_to_bytes(x))
FLAG{7h1s_i5_h0w_we_c0nvert_m3ss@ges_1nt0_num63rs}
Easy [Easy]
手始めに
ファイル:cry-easy.zip
encrypt.pyを見ると、[A-Z]を[0-25]に対応させてそれをxに入れ、a*x+b mod 26の式で暗号化しています。それの結果がoutput.txtに入っています。
outputの先頭4文字は"FLAG"が暗号化された文字列なので、そこからa, bを求めていきます。
- "H" = a×"F" + b
- "L" = a×"L" + b
- "I" = a×"A" + b
- "M" = a×"G" + b
となるa, bを求めていきますが、複雑なことはしません。mod 26の世界なのでa, b共に0~25でbrute forceすればOKです。
enc = "HLIM{OCLSAQCZASPYFZASRILLCVMC}" test = "FLAG" ans = [] for i in range(len(test)): temp = [] for a in range(26): for b in range(26): t = encrypt(test[i],a,b) if t == enc[i]: temp.append([a,b]) ans.append(temp) for i in range(len(ans[0])): #ans[0][i]が他にもあるか探索 check = True for j in range(1,len(ans)): ch = False for k in range(len(ans[j])): if ans[0][i] == ans[j][k]: ch = True break if not ch: check = False break if check: print(ans[0][i])
上記の4つの式の内、一番上の式に当てはまるa, bはans[0]に記憶し、その下の式はans[1], ans[2], ans[3]に入っています。ans[0][i]がans[1~3]にも含まれているかをcheckします。もっといいコードありそう。
コードを走らせると、a=5, b=8と一意に決まりました。あとはencrypt関数の逆を作ればOKです。
def decrypt(ct,a,b): pt = "" for x in ct: if ord("A") <= ord(x) <= ord("Z"): x = ord(x) - ord("A") for i in range(26): if x == (a*i+b)%26: x = chr(i + ord("A")) pt += x break else: pt += x return pt
FLAG{WELCOMETOCRYPTOCHALLENGE}
Extra [Normal]
いつものRSA?
ファイル:cry-extra.zip
RSAの公開鍵(e,N)に加えて、M = 2×p+qが公開されています。これは解と係数の関係を使って解けそうです。Mがp+qならx2-N×x+Mの解がp, qになりますが、M = 2×p+qなので2×p, qが解になる二次方程式を立てます。二つの解の積2×p×qはNを2倍すれば手に入るので、解と係数の関係を利用してp, qを求めます。
from Crypto.Util.number import * import gmpy2 import math N = (省略) M = (省略) e = 65537 c = (省略) n = 2*N ro, check = gmpy2.iroot(M*M-4*n,2) assert check q = (M-ro)//2 assert math.gcd(N,q) != 1 p = N//q phi = (p-1)*(q-1) d = inverse(e,phi) m = pow(c,d,N) print(long_to_bytes(m))
FLAG{@n_ex7ra_param3ter_ru1n5_3very7h1n9}
Can't restore the flag? [Hard]
ちりつもですよ
nc crt.cry.wanictf.org 50000
ファイル:cry-cant-restore-the-flag.zip
server.pyを見るとbytes_to_longしたflagに対して、300以下の数値を送るとその数で割った余りを教えてくれます。これはもう中国剰余定理ですね。2~300を全て送ってその余りを記憶してライブラリで解きます。300! > 10103 なので確実にflagを復元できます。
from sympy.ntheory.modular import crt mod = [] val = [] for i in range(2,301): print(i) read_until(f,"Mod > ") s.send(str(i).encode()+b"\n") recv = int(read_until(f).strip()) mod.append(i) val.append(recv) m,_ = crt(mod,val) print(long_to_bytes(m))
FLAG{Ch1n3s3_r3m@1nd3r_7h30r3m__so_u5eful}
OUCS [Very hard]
OUによるHomomorphicなCryptoSystemです
nc oucs.cry.wanictf.org 50010
ファイル:cry-oucs.zip
この問題解けたの嬉しかったですね。
ではまず暗号化について。n = p×p×qとしgp-1 mod p2 != 1であるgを選び、h=gn mod nとした(n,g,h)が公開鍵となります。plaintextをmとすると暗号文cは、c=(gm mod n × hr mod n) mod nとなります(r : 乱数)。hの条件より上記の式は
c = (gm mod n × (gn)r mod n) = gm+n×r mod n ・・・①
となります。
次に復号について。なぜserver.pyのコードで復号できるのでしょうか。
a = ( (cp-1 mod p2) - 1) // p ・・・②
b = ( (gp-1 mod p2) - 1) // p ・・・③
m = a × b-1 mod p ・・・④
まず、フェルマーの小定理 ( xp-1 mod p = 1) を使います。cp-1 = k'×p+1 (k' : 整数)となるので、k' != pである限り、②より(cp-1 mod p2) = k'×p+1となり、a=k'となります。同様に、gp-1 = k×p+1 (k : 整数)となるので、b=kとなります。ここで④より、k'/k = mとなります。故に、k'=k×mと置くことが出来ます。
k×m = ( (cp-1 mod p2) - 1) // p
k×m×p+1 = cp-1 mod p2 ・・・⑤
⑤を証明していきます。式①と式⑤に代入しています。n = p×p×qであるので、mod nの式をmod p2 の式に落とし込むことが出来ます。
k×m×p+1 = (gm+n×r mod n)p-1 mod p2 = (gm+n×r )p-1 mod p2
= g (m+n×r)×(p-1) mod p2
となります。さらに展開していきます。
k×m×p+1 = g (m+n×r)×(p-1) mod p2 = gm×(p-1) × gn×r×(p-1) mod p2
ここで、オイラーの公式( x φ(n) = 1 mod n )を使います。φ(p2) = p×(p-1)となります(p2と互いに素ではないp2以下の自然数は、p, 2p, 3p, ... , (p-1)p, pp のp個)。
n×r×(p-1) = p×p×q×r×(p-1) = A×p×(p-1)
であるので、gn×r×(p-1) mod p2 = 1です。故に、k×m×p+1 = gm×(p-1) となります、これをgp-1 = k×p+1 (k : 整数) が出てくるようにします。
k×m×p+1 = gm×(p-1) mod p2 = gp-1m mod p2 = (k×p+1)m mod p2
となります。ここで二項係数とパスカルの三角形を考えると、
(k×p+1)m = (k×p)m + m×(k×p)m-1 + mC2 × (k×p)m-2 + ... + mC(m-2) × (k×p)2 + m×(k×p) + 1
となります。これをmod p2すると、後ろ2項以外はp2が含まれるので0となるのでm×(k×p) + 1となり、式⑤が証明されました。
では、これを使ってどのようにmを求めればいいのでしょうか。ここまで長い証明を読んで下さった方には申し訳ないですが、答えはあっさりです。
a = ( (cp-1 mod p2) - 1) // p = k×mとなるのに対しa' = 2×k×mとなるa'があれば、
a'×b-1 mod p2= 2×m mod p2となり、復号結果がflagと異なるので復号後その値をサーバが教えてくれるはずです。2×m < p となるのは希望的観測です。このくらいは耐えるだろう。
c = gm+n×r mod n より、c2 mod n = (gm+n×r mod n) × (gm+n×r mod n) = g2×(m+n×r) mod n
c' = c2 mod nとして復号してもらうと、
a = ( (c'^(p-1) mod p2) - 1) // p = ( (g2×(m+n×r) )p-1 mod p2 - 1) // p = (g2×m×(p-1) mod p2 - 1) // p
b = ( (gp-1 mod p2) - 1) // p
gp-1 = k×p+1 (k : 整数) より、
b = ( ( k×p+1 mod p2) - 1) // p = (k×p) // p = k
a = (g2×m×(p-1) mod p2 - 1) // p = ( (k×p+1)2×m mod p2 - 1) // p = (先程のパスカルの三角形より) ( ( 2×m×k×p+1 ) - 1) // p = 2×m×k
よって、a×b-1 mod p = 2×m×k×k-1 = 2×m となり、a'×b-1 mod p2= 2×m mod p2が成り立つa'をサーバに計算させることが出来ました。
長ったらしい証明をしましたが、公開鍵とcを受け取ってc2 mod nを計算して復号させ、得られた平文の値を2で割ってlong_to_bytes()するだけです。solverは結構短めです。
HOST, PORT = "oucs.cry.wanictf.org", 50010 s, f = sock(HOST, PORT) for _ in range(10): read_until(f) for _ in range(7): read_until(f) read_until(f,"> ") s.send(b"1\n") recv_m = read_until(f).split() c = int(recv_m[-1][2:],16) for _ in range(8): read_until(f) read_until(f,"> ") s.send(b"4\n") recv_m = read_until(f).split() n = int(recv_m[-1][2:],16) recv_m = read_until(f).split() g = int(recv_m[-1][2:],16) for _ in range(9): read_until(f) c2 = pow(c,2,n) read_until(f,"> ") s.send(b"3\n") read_until(f) read_until(f,"> ") s.send(str(c2).encode()+b"\n") recv_m = read_until(f).split() m2 = int(recv_m[-1][2:],16) print(long_to_bytes(m2//2))
FLAG{OU_d0es_n0t_repre53nt_Osaka_University_but_Okamoto-Uchiyama}
Problems & Solver
GitHub - ksbowler/waniCTF_2021spring
おわりに
今回はcrypto問について書いていきました。
OUCSにおいて、証明までしっかりして解いたのは久しぶりでしたしflagが出たときの爽快感が半端なかったです!解いた人の中で証明までした人と、たまたま見つけた人との割合が気になります。この問題ならたまたまの人はそれなりにいるんじゃないかな。
他の分野はまだまだ分からないことだらけで、今年から少しずつ勉強していきたいと思います。もう5月だけど…
m1z0r3として出場するコンテストならともかく、個人戦の初心者向けのCTFならある程度解けるようになりたいですね。