手撕平方根-Hard
第一节:题目
跟 Easy 是同一个服务升级版:128 关全过才给 flag,交上去的 x要求小于等于10000、服务器又只回“最小的那个根”——现成的平方交上去换不回任何信息。做的时候卡了三天还是四天一直没有想出来....最后通过询问AI了解到了Jacobi 符号,才破的局。
第二节:过程
Jacobi 符号即只靠 n 和 x 就能免费算出来的一个 ±1 值,提前告诉你 x 在模 n 下有没有平方根。
这题解题关键在于:我们要算 Jacobi 符号——而 n 是公开的,Jacobi 是免费能算的。而J(x) = -1 的 x 必无根,直接跳过;J(x) = +1 的有根率约 50%,所以每关挑一组三角探针 [a, b, c, ab, ac, bc](都挑 J=+1 的)
其实之前掉过坑了以为 a b c都是固定的,但是发现如果固定了abc那最终通关的概率会非常低,于是后面想到了,根据题目动态变化abc可以让正确率提高
如何选择abc?
good = []
for p in (2,3,5,7,11,13,17,19,23,29,31,37,41,43):
if jacobi(p, n) == 1: # 只看 n 和 p,不需要分解 n
good.append(p)
if len(good) == 3: break
a, b, c = good
jacobi(p, n) 是脚本里自己实现的 Jacobi 符号:只用公开的 n 和 p 就能算,直接获取
然后打128次就行了,虽然仍然会有结构性死亡的概率,但是概率很小,128次方之后成功的概率还有18%左右,那多做几次就行了
最后脚本
# py -3.12 sqhard_simple.py <host> <port> [maxlv]
import re
import sys
from math import gcd
from pwn import context, remote
from sympy import jacobi_symbol, sqrt_mod
from sympy.ntheory.modular import crt
context.log_level = "error"
HOST, PORT = sys.argv[1], int(sys.argv[2])
MAXLV = int(sys.argv[3]) if len(sys.argv) > 3 else 128
def pick(n):
"""前三个 J=+1 的小素数:乘积不超 9999,每个探针约一半概率有根"""
return [p for p in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31) if jacobi_symbol(p, n) == 1][:3]
def ask(io, x):
io.sendline(str(x).encode())
io.recvuntil(b"y = ")
s = io.recvline().strip()
return None if s == b"None" else int(s)
def split(xs, ys, n):
"""在已问到的 (x, y) 里找同一个数的两个不同根:差值/和与 n 求 gcd"""
y_of = dict(zip(xs, ys))
live = [(x, y) for x, y in y_of.items() if y is not None]
for xa, ya in live:
for k in range(2, 300):
xk = k * k * xa
if xk > 9999:
break
yk = y_of.get(xk)
if yk is None:
continue
for d in (yk - k * ya, yk + k * ya):
g = gcd(d % n, n)
if 1 < g < n:
return g
for xa, ya in live: # 乘积网:ya·yb 应是 xa·xb 的根
for xb, yb in live:
yk = y_of.get(xa * xb)
if yk is None:
continue
for d in (yk - ya * yb, yk + ya * yb):
g = gcd(d % n, n)
if 1 < g < n:
return g
return None
def main():
while True:
try:
io = remote(HOST, PORT, timeout=20)
lv = 1
while lv <= MAXLV:
n = int(re.findall(rb"n = (\d+)", io.recvuntil(b"x = ", timeout=30))[-1])
trip = pick(n)
a, b, c = trip if len(trip) == 3 else (15, 10, 6) # 兜底三角
xs, ys = [], []
for x in (a, b, c, a * b, a * c, b * c):
xs.append(x); ys.append(ask(io, x))
if ys[-1] is not None:
break
xb, yb = xs[-1], ys[-1]
if yb is not None: # 按 k·yb 离 n/2 的远近降序折叠
ks = [k for k in range(2, 300) if k * k * xb <= 9999]
ks.sort(key=lambda k: -min(k * yb % n, n - k * yb % n))
for k in ks:
if len(xs) == 7:
break
if k * k * xb not in xs:
xs.append(k * k * xb); ys.append(ask(io, k * k * xb))
while len(xs) < 7: # 协议固定 7 问,补满
xs.append(30); ys.append(ask(io, 30))
X = int(re.findall(rb"x = (\d+)", io.recvuntil(b"y = ", timeout=20))[-1])
p = split(xs, ys, n)
ans = None
if p:
q = n // p
rp, rq = sqrt_mod(X % p, p), sqrt_mod(X % q, q)
if rp is not None and rq is not None:
ans = int(crt([p, q], [int(rp), int(rq)])[0])
io.sendline(str(ans if ans is not None else 1).encode())
print(f"L{lv}: {'OK' if ans is not None else 'DEATH'}")
lv = lv + 1 if ans is not None else 1
print(io.recvall(timeout=10).decode("utf-8", "replace"))
return
except Exception as e:
print("reconnect:", str(e)[:80])
if __name__ == "__main__":
main()
最后 flag:flag{4eeae838-d967-4f1d-8bdc-869fb8067c08}