Writer:b1uef0x / Webページ建造途中
チーム参加でcrypto問を解いていた。
The word "decomposition" has multiple meanings.
Can you decompose?
nc crypto.2023.zer0pts.com 10333
平方和問題。サーバーで動作するプログラムが配布されるので確認する。
import os
import signal
from Crypto.Util.number import *
flag = os.environb.get(b"FLAG", b"dummmmy{test_test_test}")
def main():
p = getPrime(128)
q = getPrime(128)
n = p * q
N = pow(p, 2) + pow(q, 2)
print("Let's factoring !")
print("N:", N)
p = int(input("p: "))
q = int(input("q: "))
if isPrime(p) and isPrime(q) and n == p * q:
print("yey!")
print("Here you are")
print(flag)
else:
print("omg")
def timeout(signum, frame):
print("Timed out...")
signal.alarm(0)
exit(0)
if __name__ == "__main__":
signal.signal(signal.SIGALRM, timeout)
signal.alarm(30)
main()
signal.alarm(0)
素数p,qを作成して、N=p2+q2を出力する。ここからpとqを求めることができればflagが表示される。Nを平方和に分解する問題となる。
フェルマーの二平方定理によれば、奇素数pがp≡1(mod 4)であれば、p=X2+Y2を満たす整数XとYが存在する。
方針としては、Nを素因数分解してp1,p2,...pnを作り、各piをpi=Xi2+Yi2で表現する。
すると、(Xi2+Yi2)(Xj2+Yj2) = (XiXj+YiYj)2 + (XiYj-XjYi)2、の計算を繰り返して、最終的なN=p2+q2を得ることができる。
piの分解には、x2≡-1(mod p)となるxを見つける必要がある。xの存在有無はオイラーの規準を用いることで、平方剰余の存在を判定することができる。
xが存在することがわかれば、具体的にxを求める方法としてTonelli-Shanksのアルゴリズムがあり、次のページのコードを参照した。
xが求まればp=X2+Y2を計算できる。次のページのコードを参照した。
手順をまとめると以下のようになる。
tonelli_shanks.py#https://xornet.hatenablog.com/entry/2020/05/11/112508
from Crypto.Util.number import isPrime
def neg(x, n):
return -x % n
def is_quadratic_residue(a, p):
if a % p == 0:
return True
return legendre_symbol(a, p) == 1
def legendre_symbol(a, p):
if not isPrime(p) or p == 2:
raise ValueError("p must be a odd prime number")
return pow(a, (p-1) // 2, p)
def get_q_s(p):
q = p - 1
s = 0
while q % 2 == 0:
q //= 2
s += 1
return (q, s)
def get_nonresidue(p):
ret = 2
while is_quadratic_residue(ret, p):
ret += 1
return ret
def main(a, p):
if not is_quadratic_residue(a, p):
return ()
if a == 0:
return 0
q, s = get_q_s(p)
z = get_nonresidue(p)
m, c, t, r = s, pow(z, q, p), pow(a, q, p), pow(a, (q+1) // 2, p)
while True:
if t == 1:
return (r, neg(r, p))
i = m
for j in range(1, m):
if pow(t, pow(2, j), p) == 1:
i = j
break
b = pow(c, pow(2, m - i - 1), p)
b_pow = pow(b, 2, p)
m, c, t, r = i, b_pow, t * b_pow % p, r * b % p
solve.pyfrom sympy.ntheory import factorint
import tonelli_shanks
#https://tsujimotter.hatenablog.com/entry/fermat-descent
def descent(x, y, k, p):
if k == 1:
return [x, y, 1, p]
x_mod_k = x % k
if x_mod_k > abs(x_mod_k - k):
x_mod_k = x_mod_k - k
y_mod_k = y % k
if y_mod_k > abs(y_mod_k - k):
y_mod_k = y_mod_k - k
next_k = (x_mod_k**2 + y_mod_k**2) // k
next_x = (x * x_mod_k + y * y_mod_k) // k
next_y = (x * y_mod_k - y * x_mod_k) // k
return descent(next_x, next_y, next_k, p)
#https://tex2e.github.io/blog/crypto/legendre-and-jacobi-symbls
def QR(a,p):
return pow(a, (p - 1) // 2, p) == 1
def calc(p):
Xs = tonelli_shanks.main(p-1,p)
x = Xs[0]
y = 1
k = (x*x+1)//p
x,y,k,p = descent(x,1,k,p)
return (abs(x),abs(y))
N = int(input("N= "))
factors = factorint(N)
print(factors)
MLTs = []
XYs = []
for p in factors:
if factors[p]==2:
MLTs.append(p)
elif p==2 :
XYs.append([1,1])
elif p%4 != 1:
print("false...")
break
elif not QR(p-1,p):
print("false...")
break
else:
x,y = calc(p)
XYs.append([x,y])
print(str(p)+"="+str(x)+"^2+"+str(y)+"^2")
# (a1^2+b1^2)(a2^2+b2^2) = (a1a2+b1*b2)^2 + (a1*b2-b1*a2)^2
def proofm(a1,b1,a2,b2):
return a1*a2+b1*b2,a1*b2-a2*b1
X = 0
Y = 0
for xy in XYs:
if X==0 and Y==0:
X = abs(xy[0])
Y = abs(xy[1])
else:
X,Y = proofm(X,Y,xy[0],xy[1])
X = abs(X)
Y = abs(Y)
for m in MLTs:
X *= m
Y *= m
print("p= " + str(X))
print("q= " + str(Y))
print(X*X+Y*Y == N)
solve.pyを実行、サーバーから与えられたNを入力するとp,qを出力する。時間がかかってtime outになることもあるが数回以内には成功する。同じ素因数は2個までを想定しており、piが2個ある時は分解する必要はない。
ローカル側:
$ python3 solve.py
N= 226331076098488940388205693533921106326905753746168177126656766414479243947810
{2: 1, 5: 1, 22633107609848894038820569353392110632690575374616817712665676641447924394781: 1}
5=2^2+1^2
22633107609848894038820569353392110632690575374616817712665676641447924394781=65871231347943334317349051483801057334^2+135255641252239906040866650072552760085^2
p= 332869335296069908992913804523955932087
q= 339895692408776383805250898733857222921
サーバー側:
$ nc crypto.2023.zer0pts.com 10333
Let's factoring !
N: 226331076098488940388205693533921106326905753746168177126656766414479243947810
p: 332869335296069908992913804523955932087
q: 339895692408776383805250898733857222921
yey!
Here you are
b"zer0pts{piyopiyo_Fermat's_Sum_of_Square_meow!!}"
zer0pts{piyopiyo_Fermat's_Sum_of_Square_meow!!}
I wrote a pseudorandom number generator. You can't guess the next bit without knowing each other's secret.
nc crypto.2023.zer0pts.com 10666
平方剰余問題。途中まで進んだので、そこまでのwriteupと復習を兼ねてflagを出すまで記載する。
サーバーで動作するプログラムが配布されるので確認する。
#!/usr/bin/env python3
import os
from Crypto.Util.number import getPrime, getRandomRange
def isSquare(a, p):
return pow(a, (p-1)//2, p) != p-1
class SquareRNG(object):
def __init__(self, p, sa, sb):
assert sa != 0 and sb != 0
(self.p, self.sa, self.sb) = (p, sa, sb)
self.x = 0
def int(self, nbits):
v, s = 0, 1
for _ in range(nbits):
self.x = (self.x + 1) % p
s += pow(self.sa, self.x, self.p) * pow(self.sb, self.x, self.p)
s %= self.p
v = (v << 1) | int(isSquare(s, self.p))
return v
def bool(self):
self.x = (self.x + 1) % self.p
t = (pow(self.sa, self.x, self.p) + pow(self.sb, self.x, self.p))
t %= self.p
return isSquare(t, self.p)
p = getPrime(256)
sb1 = int(input("Bob's seed 1: ")) % p
sb2 = int(input("Bob's seed 2: ")) % p
for _ in range(77):
sa = getRandomRange(1, p)
r1 = SquareRNG(p, sa, sb1)
print("Random 1:", hex(r1.int(32)))
r2 = SquareRNG(p, sa, sb2)
print("Random 2:", hex(r2.int(32)))
guess = int(input("Guess next bool [0 or 1]: "))
if guess == int(r1.bool()):
print("OK!")
else:
print("NG...")
break
else:
print("Congratz!")
print(os.getenv("FLAG", "nek0pts{*** REDACTED ***}"))
プログラムは起動すると素数pを生成して、好きなsb1とsb2を入力することができる。続いて乱数saを生成して、SquareRNGクラスで定義されたint関数を(p,sa,sb1)と(p,sa,sb2)の2つについて実行、この2つの結果から(p,sa,sb1)のbool関数の出力を推測する問題。乱数saを変えて77回連続で成功すればflagが表示される。
int関数とbool関数はともに平方剰余の有無を判定するisSquare関数の結果(True/False)を出力する。
ここでint関数は同一のsbに対してN=1~32までの32個分を出力する。
入力可能なsb1とsb2に1と-1を入れると式を次のように整理できる。
競技中はここまで書いて時間切れとなったが、sb=1とsb=-1のint関数及びbool関数の式を利用すると、次のように書ける。
(sa33 + 1) mod p = (∑[x=0,32](-sa)x)×(1+sa) mod p
すなわち
bool(1) = int(-1,32)×int(1,1)
よって、sb1=1、sb2=-1を入力したとき、回答すべきbool(1)はsb1のint関数出力の最初のビットとsb2のint関数出力の最後のビットから得ることができる。
平方剰余の相互法則(ja.wikipedia.org)より、平方剰余同士の積が計算できる。ここではisSquareの出力はTrue/Falseだが、これを1/-1に読み替えればよい。
以下のsolverを書いた。
from pwn import *
io = remote('crypto.2023.zer0pts.com',10666)
io.recvuntil("Bob's seed 1: ")
io.sendline(b'1')
io.recvuntil("Bob's seed 2: ")
io.sendline(b'-1')
for _ in range(77):
int1 = 1 if int(io.recvline().decode()[10:],16) >> 31 == 0 else -1
int2 = 1 if int(io.recvline().decode()[10:],16) & 0x1 else -1
bool1 = 1 if int1*int2==-1 else 0
io.recvuntil("Guess next bool [0 or 1]: ")
io.sendline(str(bool1).encode())
res = io.recvline().decode()
print(str(int1) + "*" + str(int2) + " > " + str(bool1) + " : " + res[:-1])
if not "OK!" in res:
break
io.interactive()
実行させるとflagに到達する。
$ python3 solve.py
[+] Opening connection to crypto.2023.zer0pts.com on port 10666: Done
-1+-1 > 0 : OK!
-1+1 > 1 : OK!
1+1 > 0 : OK!
-1+-1 > 0 : OK!
-1+-1 > 0 : OK!
-1+-1 > 0 : OK!
1+-1 > 1 : OK!
-1+1 > 1 : OK!
1+1 > 0 : OK!
1+-1 > 1 : OK!
-1+1 > 1 : OK!
-1+1 > 1 : OK!
-1+1 > 1 : OK!
1+1 > 0 : OK!
-1+-1 > 0 : OK!
-1+-1 > 0 : OK!
1+-1 > 1 : OK!
-1+1 > 1 : OK!
1+1 > 0 : OK!
-1+1 > 1 : OK!
1+1 > 0 : OK!
1+1 > 0 : OK!
-1+-1 > 0 : OK!
-1+1 > 1 : OK!
1+1 > 0 : OK!
1+1 > 0 : OK!
-1+-1 > 0 : OK!
-1+-1 > 0 : OK!
1+-1 > 1 : OK!
1+-1 > 1 : OK!
-1+-1 > 0 : OK!
-1+1 > 1 : OK!
1+1 > 0 : OK!
-1+-1 > 0 : OK!
1+-1 > 1 : OK!
1+-1 > 1 : OK!
1+1 > 0 : OK!
1+-1 > 1 : OK!
-1+-1 > 0 : OK!
-1+1 > 1 : OK!
1+-1 > 1 : OK!
-1+1 > 1 : OK!
1+1 > 0 : OK!
-1+1 > 1 : OK!
1+1 > 0 : OK!
-1+-1 > 0 : OK!
-1+1 > 1 : OK!
-1+1 > 1 : OK!
1+-1 > 1 : OK!
1+1 > 0 : OK!
-1+-1 > 0 : OK!
-1+1 > 1 : OK!
1+-1 > 1 : OK!
-1+1 > 1 : OK!
1+1 > 0 : OK!
-1+1 > 1 : OK!
1+-1 > 1 : OK!
1+-1 > 1 : OK!
1+-1 > 1 : OK!
-1+-1 > 0 : OK!
1+-1 > 1 : OK!
-1+-1 > 0 : OK!
-1+-1 > 0 : OK!
1+-1 > 1 : OK!
-1+1 > 1 : OK!
-1+1 > 1 : OK!
1+1 > 0 : OK!
1+1 > 0 : OK!
-1+-1 > 0 : OK!
1+1 > 0 : OK!
-1+-1 > 0 : OK!
1+-1 > 1 : OK!
-1+1 > 1 : OK!
-1+-1 > 0 : OK!
1+-1 > 1 : OK!
-1+-1 > 0 : OK!
1+-1 > 1 : OK!
[*] Switching to interactive mode
Congratz!
zer0pts{L(a)L(b)=L(ab)}
zer0pts{L(a)L(b)=L(ab)}