###### tags: `InterKosenCTF2020` `crypto` # [InterKosenCTF 2020] bitcrypto ## 問題 サーバーで動いてるスクリプトだけもらえる 以下、スクリプトのmain部分 ```python= def main(): pubkey, privkey = key_gen(256) keyword = "yoshiking, give me ur flag" m = input("your query: ") if any([c in keyword for c in m]): print("yoshiking: forbidden!") exit() if len(m) > 8: print("yoshiking: too long!") exit() c = enc(pubkey, bytes_to_long(m.encode())) print("token to order yoshiking: ", c) c = [int(x) for x in input("your token: ")[1:-1].split(",")] if len(c) != len(set(c)): print("yoshiking: invalid!") exit() if any([x < 0 for x in c]): print("yoshiking: wow good unintended-solution!") exit() m = long_to_bytes(dec(privkey, c)) if m == keyword.encode(): print("yoshiking: hi!!!! flag!!!! here!!! wowowowowo~~~~~~") print(flag) else: print(m) print("yoshiking: ...?") ``` `m`の入力時に弾かれる条件は、 - keywordに含まれている文字を使用している - `m`が9文字以上だとダメ これを通ると、`m`を暗号化したもの(`token`)がもらえる. その後、`token`を入力し、復号したものが`keyword`と同じであれば、flagゲット 入力のバイパスはどうやってもできないので、keygen, enc, decを見てみる. まず、keygenで気になるのは`pubkey`に`z`が含まれるところ. この`z`は $(\frac{z}{p})=(\frac{z}{q})=-1$を満たすものである. encでは、`b`を`m`の1ビット分にして、$z^b \cdot rand(2,n)^2$ を計算していった結果を返す. decは、与えられた数字`b`が $(\frac{b}{p}) = (\frac{b}{q}) = 1$ だったら`0`を違ったら`1`とし、`m`を復元する. ## 解法 legendre symbolがみそっぽい. encの式をlegendre symbolのみに注目すると、 $$ z^b = \begin{cases} -1 & (b=1)\\ 1 & (b=0) \end{cases}\\ rand(2,n)^2 = 1 $$ となる. つまり、入力した`m`のビットが`1`のところは`token`のlegendre symbolが`-1`になり.`0`のところはlegendre symbolが`1`となる.decの方法がうまく行くことがわかる. 入力した`m`のビットが`1`となる`token`と`m`のビットが`0`の`token`を使えば任意のビット列が生成できる. しかし、mainに ```python if len(c) != len(set(c)): print("yoshiking: invalid!") exit() ``` とあるように、同じ値を入れるとexitしてしまう. これをパスするためには、legendre symbolの性質を使って異なる値となるようにする.legendre symbolは$a^2 \bmod{n}$ の解が存在すれば`1`となるので、適当な値$a$を2乗したものはlegendre symbolが`1`となる. legendre symbol同士の乗算は普通に成り立つ?(証明はしらん)ので、legendre symbolが`1`となるようなものをかけても全体の値のlegendre symbolは変わらない. よくわからない文を生成してしまったけど、とりあえず適当な値を2乗したものをかけとけば条件をパスできる. ```python= from ptrlib import * from Crypto.Util.number import bytes_to_long from random import randrange, choice keyword = b"yoshiking, give me ur flag" sock = Socket("localhost", 13003) sock.recvuntil("query: ") sock.sendline(b"@") sock.recvuntil("yoshiking: ") token = eval(sock.recvline().decode()) payload = [] for bit in bin(bytes_to_long(keyword))[2:]: if bit == '1': payload.append(randrange(10000)**2 * token[0]) else: payload.append(randrange(10000)**2 * choice(token[1:])) print(len(payload)) print(len(set(payload))) sock.recvuntil("your token: ") sock.sendline(str(payload)) sock.interactive() ``` 何回か試すと、flagゲット ``` $ python solver.py [+] __init__: Successfully connected to localhost:13003 207 207 [ptrlib]$ yoshiking: hi!!!! flag!!!! here!!! wowowowowo~~~~~~ KosenCTF{yoshiking_is_clever_and_wild_god_of_crypt} ^C[+] close: Connection to localhost:13003 closed ``` ## 感想 interkosenだとmediumかhardぐらい? legendre symbolの勉強になった