Midnight Sun CTF Quals 2022 - Pelle's Rotor-Supported Arithmetic
TL;DR
- 入力に対して、におけるRSAの秘密鍵を指定したインデックスで区切って前後入れ替えたもの()を用いてを計算した結果をくれる
- RSAの問題だが、が不明なのでRSAが乗法的であることを利用し、の倍数を複数導出してからGCDをとってを復元する
- の関係を利用すると、総当たりによっての先頭の1桁が求まるのでこれを全てので行うことでを復元する
- フラグを暗号化しているは65537で無いので求めたを利用しての候補を絞り、フラグを暗号化している公開鍵に対する秘密鍵を求めて復号する
Prerequisite
- RSA
Writeup
次のようなスクリプトが動いている
#!/usr/bin/python3
from sys import stdin, stdout, exit
from flag import FLAG
from secrets import randbelow
from gmpy import next_prime
p = int(next_prime(randbelow(2**512)))
q = int(next_prime(randbelow(2**512)))
n = p * q
e = 65537
phi = (p - 1)*(q - 1)
d = int(pow(e, -1, phi))
d_len = len(str(d))
print("encrypted flag", pow(FLAG, 3331646268016923629, n))
stdout.flush()
ctr = 0
def oracle(c, i):
global ctr
if ctr > 10 * d_len // 9:
print("Come on, that was already way too generous...")
return
ctr += 1
rotor = lambda d, i: int(str(d)[i % d_len:] + str(d)[:i % d_len])
return int(pow(c, rotor(d, i), n))
banner = lambda: stdout.write("""
Pelle's Rotor Supported Arithmetic Oracle
1) Query the oracle with a ciphertext and rotation value.
2) Exit.
""")
banner()
stdout.flush()
choices = {
1: oracle,
2: exit
}
while True:
try:
choice = stdin.readline()
print("c:")
stdout.flush()
cipher = stdin.readline()
print("rot:")
stdout.flush()
rotation = stdin.readline()
print(choices.get(int(choice))(int(cipher), int(rotation)))
stdout.flush()
except Exception as e:
stdout.write("%s\n" % e)
stdout.flush()
exit()
最初にを用いてRSAで暗号化したフラグをくれる。以後はの場合の秘密鍵に対して、d_len = len(str(d))
で定義されたd_len
を用いて10 * d_len // 9
回まで次のようなオラクルに問い合わせることが出来る。
- 入力:
- 10進法表記での上位桁を取り出し、元のの下の桁とした結果を返す
- 該当箇所:
rotor = lambda d, i: int(str(d)[i % d_len:] + str(d)[:i % d_len])
- 該当箇所:
また、通常のRSA問題とは違って、公開鍵が与えられていない。というわけで最初にを特定する事を目指す。
これはオラクルの入力に自由な入力をいれることが出来るのでとなるようなを同じで入力するとrotor(d,i)
をとおいて、となるから、のように左辺がの倍数となる。よって、異なるの組み合わせを2つ用意しをそれぞれ計算して最大公約数を計算することでを得ることが期待出来る。
が得られので、次はを得る事を考える。には次のような関係がある。
ここで、は10未満の非負整数とする。また、の桁数はと同じかやや小さいだけなのではの桁数 - 1になるとする(そうでなかったらそうなるまでスクリプトを回せば良い)。
また、は共に未知数だが、が高々10通りしかありえないのに対し、の取りうる範囲は非常に広いことから、を消去してを総当りで特定する事を考える。これによって、次のようになる。
よって両辺をの指数とすると次のようになる。
これを更に変形すると次のようになる。但し、とおいた。
右辺はオラクルの結果から計算することが出来、左辺はを総当りすることで候補を列挙出来る。よって、これを比べて一致した際にを特定することが出来る。これを全てので行うことでを復元出来る。
ここまでくればがの倍数となり、となるはと同程度(がと同程度なので)になるから、を総当りすることでを求めることが出来る。これに対してフラグの復号を行って、フラグフォーマットに従っているものがフラグとなる。
Code
from math import gcd
from pwn import remote
import sys
from Crypto.Util.number import long_to_bytes
def oracle(c, i):
sc.sendline(b"1")
sc.recvuntil(b"c:\n")
sc.sendline(str(c).encode())
sc.recvuntil(b"rot:\n")
sc.sendline(str(i).encode())
res = int(sc.recvline())
return res
args = sys.argv
DEBUG = len(args) > 1 and args[1] == "-d"
sc = remote("localhost", 13337)
sc.recvuntil(b"encrypted flag ")
ct = int(sc.recvline())
# for debug
if DEBUG:
sc.recvuntil(b"n=")
true_n = int(sc.recvline())
sc.recvuntil(b"d=")
true_d = int(sc.recvline())
sc.recvuntil(b"2) Exit.\n")
cs = [
[2,3,6],
[7,13,91]
]
kN = []
for i in range(2):
r1 = oracle(cs[i][0], 0)
r2 = oracle(cs[i][1], 0)
r3 = oracle(cs[i][2], 0)
kN.append(r1*r2 - r3)
N = gcd(kN[0], kN[1])
N_length = len(str(N))
coef = 10**N_length - 1
c = 2
d_digits = []
r_previous = None
r = None
for i in range(len(str(N))+1):
r = oracle(c, i)
if i == 0:
r_previous = r
continue
found = False
rhs = pow(r_previous, 10, N) * pow(r, -1, N) % N
for x in range(10):
lhs = pow(c, x * coef, N)
if rhs == lhs:
found = True
print(f"[{i}]: {x=}")
break
if not found:
print("ha?")
exit()
d_digits.append(x)
r_previous = r
e = 65537
d = int("".join(map(str, d_digits)))
if DEBUG:
assert d == true_d
k_phi = e*d - 1
phi_cands = []
for k in range(1, e):
if k_phi % k == 0:
phi = k_phi // k
if phi.bit_length() == N.bit_length():
phi_cands.append(phi)
print(len(phi_cands))
_e = 3331646268016923629
for phi in phi_cands:
_d = pow(_e, -1, phi)
pt = pow(ct, _d, N)
print(long_to_bytes(pt))
Flag
(ローカルでやっただけなので無し)