Daily AlpacaHack - Fully Padded RSA
問題ソースコードは以下の通り。
問題ソースコード
import os
from Crypto.Util.number import *
from math import gcd
flag = os.environ.get("FLAG", "Alpaca{dummy}")
assert len(flag) <= 40
e1 = 65517
e2 = 65577
while True:
p = getPrime(512)
q = getPrime(512)
if gcd((p-1)*(q-1), e1) == gcd((p-1)*(q-1), e2) == 1:
break
n = p * q
padded_flag = long_to_bytes(n)[:-len(flag)] + flag.encode()
m = bytes_to_long(padded_flag)
assert m < n
c1 = pow(m, e1, n)
c2 = pow(m, e2, n)
print(f"{n = }")
print(f"{c1 = }")
print(f"{c2 = }")以下の記事に、RSAの問題においてよく出題される攻撃手法が列挙されている。
https://zenn.dev/anko/articles/ctf-crypto-rsa
今回の問題に適用できる攻撃がないか調べてみると、以下が該当する。
Common Modulus Attack
同一の平文を同一の 異なる で暗号化した暗号文を与えてはいけない
この攻撃を適用すると、より小さな の暗号文を作ることができる。実際に適用してみよう。
今回の問題では、e1 = 65517, e2 = 65577となっている。これらについて、拡張ユークリッドの互除法を適用する。
実際に計算すると、 という式が得られる。
これを使って、 という形で の値が計算できる。
e1とe2が互いに素だった場合はこれだけで、つまりフラグが得られるが、今回は最小公倍数 が3なため、が得られた。
再度攻撃可能なケースのリストを見てみると、の値が計算できたことから、以下の状況に合致する。
Low Public Exponent Attack
公開鍵 が小さすぎてはいけない
これは、 という条件を満たす場合に直接 の 乗根を計算することによって の値が計算出来てしまうという攻撃だ。しかし、これは今回の問題には直接は適用できない。の値がパディングによって大きな値となっているためだ。
パディングはflagの前にnと同じビット列を結合することによって計算されている。これだととは上位bitが完全に一致し、がと非常に近い値になることとなる。実際に手元でとの値を計算して出力してみると、がより少し小さい値になっていることがわかる。
n = 0x5080c9faff634e8018c4a6c367b257fad5cb95797a00c663b277af51926b72b41f0aebf8b8d7760842c3b3be9d28da7b50c173deb2017f7c733fa0442ee298a4721311168af7fca9f21d1673d2a64c8948c3e57befad0fff4cb991dedd05f7bf2d9eb3d7803344b6adb5a768bd47b825395c7630f4c3ce665b2dc315665de735
m = 0x5080c9faff634e8018c4a6c367b257fad5cb95797a00c663b277af51926b72b41f0aebf8b8d7760842c3b3be9d28da7b50c173deb2017f7c733fa0442ee298a4721311168af7fca9f21d1673d2a64c8948c3e57befad0fff4cb991dedd05f7bf2d9eb3d7803344b6adb5a768bd47b825395c76416c706163617b64756d6d797dこのとの差をとすると、 という式が立つ。すると、となることから、、つまりがの暗号文となることがわかる。
はフラグが40byte以下という制約から320bit程度のかなり小さい値となる。は1024bitなので、Low Public Exponent Attackが成立する条件であるを満たす。よって、の3乗根を計算すれば、の値が求まり、そこからの値も求めることができる。
ソルバの全体は以下の通り。
拡張ユークリッドの互除法は自分で実装してもいいが、SageMathのxgcd関数や、gmpy2のgcdext関数を利用すると簡単に計算できる。
同様に、3乗根を求めるのにはgmpy2のiroot関数を使うと簡単。int(x**(1/3))のような方法だと精度の問題で正しく計算できないので注意。
from Crypto.Util.number import long_to_bytes
from gmpy2 import gcdext, iroot
n = ...
c1 = ...
c2 = ...
g, x, y = gcdext(65517, 65577)
c = pow(c1, x, n) * pow(c2, y, n) % n
m_dash, _ = iroot(n-c, 3)
m = n-m_dash
print(long_to_bytes(m))