DiceCTF @ HOPE - small-fortune
TL;DR
- Goldwasser-Micali暗号
- 暗号化に使うビットごとの乱数の差が小さい
- 最初に使われた乱数の2乗は求まることから、それを利用して差分を変数とした合同方程式を
small_roots()
で解き、解が存在するビットを平文とする
Prerequisite
Writeup
次のようなスクリプトとその実行結果が与えられる
from Crypto.Util.number import *
from gmpy2 import legendre
flag = bytes_to_long(open("flag.txt", "rb").read())
p, q = getPrime(256), getPrime(256)
n = p * q
x = getRandomRange(0, n)
while legendre(x, p) != -1 or legendre(x, q) != -1:
x = getRandomRange(0, n)
def gm_encrypt(msg, n, x):
y = getRandomRange(0, n)
enc = []
while msg:
bit = msg & 1
msg >>= 1
enc.append((pow(y, 2) * pow(x, bit)) % n)
y += getRandomRange(1, 2**48)
return enc
print("n =", n)
print("x =", x)
print("enc =", gm_encrypt(flag, n, x))
明らかにGoldwasser-Micali暗号が使われているが、各ビットに使う乱数y
の差がn
に比べて小さい。
公開鍵に対して、ビットの暗号文をとおくと、暗号化に用いる乱数を用いて次が成り立つ。
ここで、がインクリメントされる度には最大で程度加算されるが、これは比べて非常に小さい。よって、との差もに比べて小さくなることから次のように書き直す。
ここで、は0か1かの2択であり、フラグがフォーマットに沿っていて改行も存在していないとすれば、ord("}") = 0x7d
より奇数であるから、としてよい。よってが成り立つ。
上記のに関する式を見直すと、が現れ、が既知であることから、後はを消去出来れば良い事になる。これは明らかにについて解いて2乗すれば良く、最終的に次のような式が得られる。
ここで、もしを正しくGuess出来たとしたら、がに比べて小さく、更には左辺の根の1つなのでCoppersmith's Attackで求められることが期待出来る。そしては0,1の高々2通りなので総当たり出来ることを利用すれば、どちらもsmall_roots()
を試して解が存在したら、のGuessが成功したことになる。
Code
from output import n, x, enc
from Crypto.Util.number import long_to_bytes
from tqdm import tqdm
R = Zmod(n)
PR.<delta> = PolynomialRing(R)
# assumption: lsb of the flag is 1
y2 = inverse_mod(x, n) * enc[0] % n
flag = "1"
for c in tqdm(enc[1:]):
found = False
for b in [0,1]:
lhs = (c * power_mod(x, -b, n) - y2) % n
f = (lhs - delta**2)**2 - 4*delta**2*y2
rs = f.small_roots(epislon=1/20)
if len(rs) != 0:
if found:
print("[+] ha? (1)")
flag = str(b) + flag
found = True
if not found:
print("[+] ha? (2)")
print(flag)
flag = int(flag, 2)
print(long_to_bytes(flag))
Flag
hope{r4nd0m_sh0uld_b3_truly_r4nd0m_3v3ry_t1m3_sh0uld_1t_n0t?}
Thanks for reading! Read other posts?