Midnight Sun CTF Quals 2022 - BabyZK
TL;DR
- を与えるとをくれるクエリに17回問い合わせられる
- そこから任意のに対してを100回正確に求めるチャレンジがある
- 線形代数と基底簡約を利用しての倍数を複数用意し、そこから最大公約数を求めてを導出する
- 任意のに対して、が既知のの線形結合で表現出来るので、を求めてチャレンジに提出する
Prerequisite
- 線形代数
- 基底簡約
Writeup
次のようなスクリプトが動いている
#!/usr/bin/python3
from sys import stdin, stdout, exit
from secrets import randbelow
from gmpy2 import next_prime
from flag import FLAG
class BabyZK:
def __init__(self, degree, nbits):
self.p = self.__safeprime(nbits)
self.degree = degree
self.m = [randbelow(self.p-1) for i in range(self.degree)]
self.g = 2 + randbelow(self.p-3)
self.ctr = 0
def __safeprime(self, nbits):
stdout.write("Generating safeprime...")
p = -1
while True:
q = next_prime(randbelow(2 * 2**nbits))
p = 2*q + 1
if p.is_prime():
break
return p
def __eval(self, x: int) -> int:
y = 0
for a in self.m:
y += y * x + a
return y % (self.p-1)
def prover(self, x: int) -> int:
if self.ctr > self.degree + 1:
raise Exception("Sorry, you are out of queries...")
self.ctr += 1
return int(pow(self.g, self.__eval(x), self.p))
def verify(self, x: int, u: int):
if not u < self.p or u < 0:
raise Exception("Oof, this is not mod p...")
if int(pow(self.g, self.__eval(x), self.p)) != u:
raise Exception("No can do...")
bzk = BabyZK(15, 1024)
def prove():
stdout.write("> ")
stdout.flush()
challenge = int(stdin.readline())
stdout.write("%d\n" % bzk.prover(challenge))
stdout.flush()
def verify():
for i in range(100):
challenge = randbelow(bzk.p)
stdout.write("%d\n" % challenge)
stdout.flush()
response = int(stdin.readline())
bzk.verify(challenge, response)
stdout.write("%s\n" % FLAG)
stdout.flush()
banner = lambda: stdout.write("""
1) Query the prover oracle.
2) Prove to verifier that you know the secret.
3) Exit.
""")
choices = {
1: prove,
2: verify,
3: exit
}
banner()
stdout.flush()
while True:
try:
choice = stdin.readline()
choices.get(int(choice))()
except Exception as e:
stdout.write("%s\n" % e)
stdout.flush()
exit()
安全素数と未満の整数に対して上の14次多項式が定義されており、どれも未知である。これに対して次の2つのコマンドを実行出来る。
- 17回までを指定してを得る
- が指定され、を提出するチャレンジを100回行い、全部成功するとフラグが得られる。1回でも失敗すると終了する
が安全素数であることから、指数を直接求めることは難しく、このことから多項式を求めることが難しい。そもそもすら分かっていないし求める方法も思い浮かばない。そこで、を求めるのは諦めてを求めた上で2のチャレンジに成功する事を考える。
とおく。すると、1のクエリで指定したに対しては次のような行列積で表される。17回クエリを発行出来るのでとする。
わかりやすさの為に左辺の17x15行列を、(を、右辺をとおいて、とする。ここである整数に対してを考えると、の次元が15次元なのでとなるが存在する。
よって、上の式に両辺からを左から掛けると次が成り立つ。
の指数にこれらを用いて次が成り立つ。
17回のクエリによって、はの範囲なら既知であり、簡単な行列の計算によっても既知である。したがって後はを求める事さえできれば、2のチャレンジは突破出来る。
肝心のを求める方法だが、を法として合同な2つの数を用意するとその差がの倍数となるから、それを複数集めてGCDを求めれば(と比較的小さい数の積)が手に入ることが期待出来る。
手元にあるのはが17個だけであるから、となるような、を求めたい。これは結局指数法則によって次のようなを求めることになる。による合同ではなく、イコールなのはが判明していないからである。
とおいて、とすれば、上のの式より、となるを求めれば良く、これはの核を求めることに同じである。このに対してとなるので、となって、左辺がで法を取らずに計算出来れば両辺から1を引くことでの倍数を求められそうである。
ただし、ここで求めたにはが判明していない事に由来する次の2つの問題がある。
- が判明していないのに、負の数がに含まれており、逆数が計算できない
- が判明していないため、がの大きさ次第で非常に大きくなり、時間空間計算量が爆発する
1については簡単に解決出来て、負であるようなは両辺にを掛けることで次のような式に変形出来る事を利用する。
これで左辺も右辺も計算出来るので、この差をとればの倍数が手に入る。
面倒なのは2の方でSageMathの.kernel()
で求めた核は弱の大きな値が含まれており、各が1024bit程度と考えると、はで法をとらないため1億bitぐらいの巨大な数になってしまう。
(※ここでかなり詰まって恒例のWriteupカンニングタイムになった)
そこで登場するのが基底簡約である。の核(2次元)を基底とする格子を考え、これを簡約した基底もまた核となるので、簡約するとが大きくても未満の値に落ち着いてくれる。これで無事に計算量的に問題無いが求まった事になるのでを2つ計算して最大公約数を取り、2や3といった小さい余分な数が含まれているからこれを除き、1つの大きな素数を得てが求められる。
後は前半で述べたようにチャレンジで用いるに対してちなるようなを求めて、を計算すれば、が求められる。
Code
from pwn import process, remote
def choice(c):
sc.sendline(str(c).encode())
def prove(x):
choice(1)
sc.recvuntil(b"> ")
sc.sendline(str(x).encode())
m_x = int(sc.recvline())
return m_x
def verify(p, xs, l):
chall = int(sc.recvline())
rhs = [chall**i for i in range(15)]
rhs = vector(rhs)
exps = l.solve_left(rhs)
ans = 1
for x, e in zip(xs, exps):
e %= (p-1)
ans *= (F_p(x) ^ e)
sc.sendline(str(int(ans)).encode())
l = [
[x**i for i in range(15)] for x in range(17)
]
l = matrix(l)
ker_l = matrix(l.kernel().basis())
ker_l = ker_l.LLL()
print("[+] Init")
sc = process(["python3", "chall.py"])
sc.recvuntil(b"3) Exit.\n")
print("[+] Done")
g_pow_mx = []
for x in range(17):
g_pow_mx.append(prove(x))
kps = []
for j in range(2):
lhs = 1
rhs = 1
for x in range(17):
e = ker_l[j][x]
if e > 0:
lhs *= (g_pow_mx[x] ^ e)
else:
rhs *= (g_pow_mx[x] ^ (-e))
kps.append(lhs - rhs)
print("[+] two multiples of p are prepared")
p = gcd(kps)
factors = list(factor(p))
for _p, e in factors:
if e == 1:
p = _p
assert is_prime(p)
q = (p-1)//2
assert is_prime(q)
print("[+] Prime is calculated")
print(f"{p=}")
F_p = GF(p)
choice(2)
for i in range(100):
print(i)
verify(p, g_pow_mx, l)
sc.interactive()
Flag
ローカルでやっただけなので無し
Resources
- Midnight-Sun/BabyZK.ipynb at main · Neobeo/Midnight-Sun: 格子基底簡約を使っているWriteup