DiceCTF 2022 - pow-pow
TL;DR
- ある関係式を満たすような3つ組の数を提出する問題
- 愚直にやる方法はあるが、非常に計算時間が掛かるため、別の方法でなんとかする必要がある
- 関係式を適当な根における指数で考えることで、(実質的に)一様ランダムに選んだ数がsmoothであるまで試行を繰り返すという問題になる
- この確率はそこまで小さくないので頑張って回す
Prerequisite
(※特になし、使われている暗号スキームはゼロ知識証明っぽいが特に問題に必要ではない)
Writeup
次のようなスクリプトが与えられる。
#!/usr/local/bin/python
from hashlib import shake_128
# from Crypto.Util.number import getPrime
# p = getPrime(1024)
# q = getPrime(1024)
# n = p*q
n = 20074101780713298951367849314432888633773623313581383958340657712957528608477224442447399304097982275265964617977606201420081032385652568115725040380313222774171370125703969133604447919703501504195888334206768326954381888791131225892711285554500110819805341162853758749175453772245517325336595415720377917329666450107985559621304660076416581922028713790707525012913070125689846995284918584915707916379799155552809425539923382805068274756229445925422423454529793137902298882217687068140134176878260114155151600296131482555007946797335161587991634886136340126626884686247248183040026945030563390945544619566286476584591
T = 2**64
def is_valid(x):
return type(x) == int and 0 < x < n
def encode(x):
return x.to_bytes(256, 'big')
def H(g, h):
return int.from_bytes(shake_128(encode(g) + encode(h)).digest(16), 'big')
def prove(g):
h = g
for _ in range(T):
h = pow(h, 2, n)
m = H(g, h)
r = 1
pi = 1
for _ in range(T):
b, r = divmod(2*r, m)
pi = pow(pi, 2, n) * pow(g, b, n) % n
return h, pi
def verify(g, h, pi):
assert is_valid(g)
assert is_valid(h)
assert is_valid(pi)
assert g != 1 and g != n - 1
m = H(g, h)
r = pow(2, T, m)
assert h == pow(pi, m, n) * pow(g, r, n) % n
if __name__ == '__main__':
g = int(input('g: '))
h = int(input('h: '))
pi = int(input('pi: '))
verify(g, h, pi)
with open('flag.txt') as f:
print(f.read().strip())
2つの1024bit素数の積と、が定義され、それに対して次を満たすようなを提出することが目標である。
計算してみると分かるが、様々な変数が他の変数に依存したりしているせいでこの関係を満たすようなものは簡単に見つからないように見える。
また、prove()
という使われていない関数があるが、変数名を見る限りを決めたら、verify(g,h,pi)
のassertionが成功するを計算するような関数だと思われる(確かめてないです)。ということはこれを走らせば良いんじゃないかと思うが、2箇所のfor _ in range(T): ...
があり、であることを考えるとこれは相当長い時間がかかる1。最初はここの最適化を試みたがあえなく撃沈した。
というわけで、prove()
を使わずにを求める方法を考える。目標の式の変形を試みようとしても、が素因数分解出来ないせいで指数を上手く外すことが出来ない2。そこで適当な根を用意し、左辺と右辺との指数が同じになるような状態を考える。
に対して、とおく。これを利用すると目標とする形は次のようになる。
この両辺での指数が一致するなら、当然この関係を満たすので次のようなを探すことが目標になる。
ところでの依存関係は次のようになっている(a <- b
で、a
はb
に依存している、つまり「b
を決めたらa
も決まる」とする)。
r <- m <- g <- e_g
m <- h <- e_h
これを見るとを決定すると自動的にが決まり、その結果からも決まることが分かる。というわけで適切なを探す事を考える。
について解くと次のようになる。
は当然整数なのでがの倍数になっているようなを用意すれば良い。が多項式で面倒なのでとしてがの倍数となることを考える3。もっと言えばの部分を考えるのも面倒なのでがの倍数となる事を考える4。
がハッシュ関数を使って求められるせいで、が確定するまで予測出来ないことから、この関係を満たすのも難しいように思えるが、もしがsmoothかつ、が大量の素因数の積からなる場合ならこの関係を満たすように思える。はハッシュ関数によって生成されるので実質一様ランダムと考えると、一様ランダムな数を適当に取ってきた時にそれがある数未満の素因数の積で構成されるぐらいsmoothである確率を考える。
手元で軽い実験を行ってみると、ランダムに選んだ数が未満の素因数に分解される確率は少なくともよりは大きそうな結果が得られた(補遺を参照のこと)5。ということはを未満の素数の積(加えてある程度小さい素数は複数回掛ける)とすれば、現実的なの選択回数()で条件を満たすが得られる予感がする。
とは言ってもは非常に大きな数になるため、の選択の度にを計算するのは現実的では無い。ある選択のに対して、2以上の適当な整数を掛けたもとして適切なのでと更新し、であるから、とすることで計算量を抑える。
こうしてを選択し、からを求めてがの倍数であるかを判定し、そうであれば探索を終了してを求めてそこからを求めるという方法で目標を満たすが得られる。だいたい17万回ぐらいの選択で終わる。
Code
from hashlib import shake_128
from xcrypto.prime import create_sieve
def encode(x):
return x.to_bytes(256, 'big')
def H(g, h):
return int.from_bytes(shake_128(encode(g) + encode(h)).digest(16), 'big')
def is_valid(x):
return type(x) == int and 0 < x < n
def verify(g, h, pi):
assert is_valid(g)
assert is_valid(h)
assert is_valid(pi)
assert g != 1 and g != n - 1
m = H(g, h)
r = pow(2, T, m)
assert h == pow(pi, m, n) * pow(g, r, n) % n
n = 20074101780713298951367849314432888633773623313581383958340657712957528608477224442447399304097982275265964617977606201420081032385652568115725040380313222774171370125703969133604447919703501504195888334206768326954381888791131225892711285554500110819805341162853758749175453772245517325336595415720377917329666450107985559621304660076416581922028713790707525012913070125689846995284918584915707916379799155552809425539923382805068274756229445925422423454529793137902298882217687068140134176878260114155151600296131482555007946797335161587991634886136340126626884686247248183040026945030563390945544619566286476584591
T = 2**64
g0 = 2
sieve = create_sieve(10**6)
e_g = 1
for i, res in enumerate(sieve):
if res:
exponent = 1 if i > 100 else 10
e_g *= i**exponent
e_h = 0
g = pow(g0, e_g, n)
h = pow(g0, e_h, n) # obviously 1
C = 2
cnt = 0
print("[+] start searching...")
while True:
cnt += 1
if cnt % 1000 == 0:
print(f"current: {cnt}")
m = H(g, h)
if e_g % m == 0:
print("[+] Found!!")
r = pow(2, T, m)
e_pi = - e_g // m * r
break
e_g *= C
g = pow(g, C, n)
pi = pow(g0, e_pi, n)
print(f"{g=}")
print(f"{h=}")
print(f"{pi=}")
verify(g, h, pi)
Flag
ローカルで解いただけだが、問題リポジトリにあった
dice{the_m1n1gun_4nd_f1shb0nes_the_r0ck3t_launch3r}
Resources
- dicectf-2022-challenges/crypto/pow-pow at master · dicegang/dicectf-2022-challenges: 問題ファイル
- priv.pub: 公式Writeup
- CTFtime.org / DiceCTF 2022 / pow-pow / Writeup: 十分smoothなを得るまで回すという方針を示したWriteup
Appendix: smoothな数を得られる確率について
未満の素因数の積で表される数が得られる確率の大まかなオーダーを検証するため、次のコードで軽い実験を行った。
a = 1
b = 2^128
lim = 10^6
cnt = 0
while True:
found = True
x = randint(a, b)
f = factor(x, limit=lim)
for p, e in f:
if int(p) > lim:
found = False
break
cnt += 1
if cnt % 10000 == 0:
print(f"current: {cnt}")
if found:
print(f"[+] Found at {cnt}")
print(f)
break
これを5回回してみたが、最高で65万回ぐらいのループが回ったぐらいで残り4回は30万回より小さかった(1回に関しては1万回にも満たなかったので外れ値扱いした)。
そういうわけで、一様ランダムに選んだ数が未満の素数の積となるぐらいsmoothな確率はより大きく、の選択回数の期待値は十分現実的であると判断した。
少なくともCTFの開催期間である48時間は余裕で超えるだろう
RSA暗号において、となるをの素因数分解無しで求めるのが難しいのと同じ理屈
現時点の自由度はの2つなので片方(ここではを決定してもまだは定まらない
となるので、右辺第二項はで割り切ることが出来、結局がの倍数であるかを考えれば良いのでそこまで面倒では無い
この結果は個人的にはかなり非自明で、当初はこの辺で解けないと思ってWriteupを見たら「がsmoothになるまでを試す」と書いており、ビビって検証を始めた。として大量の素因数の積を用いることを思いつかなったのが敗因