SJTU CTF 2026 线上赛
task.py
import os
import signal
import sys
import secrets
import sympy
from flag import flag
N = 512
BASES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
def is_prime(n):
if n < 2:
return False
for p in BASES:
if n == p:
return True
if n % p == 0:
return False
d, s = n - 1, 0
while d % 2 == 0:
d //= 2
s += 1
for a in BASES:
x = pow(a, d, n)
if x == 1 or x == n - 1:
continue
ok = False
for _ in range(s - 1):
x = (x * x) % n
if x == n - 1:
ok = True
break
if not ok:
return False
return True
def ring_mul(a, b, q):
r = [0] * N
for i in range(N):
ai = a[i]
if ai == 0:
continue
for j in range(N):
bj = b[j]
if bj:
k = (i + j) % N
r[k] = (r[k] + ai * bj) % q
return r
def encode(msg):
v = int.from_bytes(msg, "big")
out = [0] * N
for k in range(len(msg) * 8):
out[N - 1 - k] = (v >> k) & 1
return out
def main():
signal.alarm(150)
print("send q (62 <= bit_length(q) <= 64, is_prime(q) must hold, sympy.isprime(q) must NOT):", flush=True)
try:
q = int(sys.stdin.readline().strip())
except Exception:
print("bad input")
return
if not (62 <= q.bit_length() <= 64):
print("bit_length out of range")
return
if not is_prime(q):
print("q failed is_prime")
return
if sympy.isprime(q):
print("q is actually prime, not accepted")
return
pw = secrets.token_bytes(64)
s = encode(pw)
a = [secrets.randbelow(q) for _ in range(N)]
e = [secrets.randbelow(8) - 4 for _ in range(N)]
b = [(x + y) % q for x, y in zip(ring_mul(a, s, q), e)]
print(f"n = {N}")
print(f"A = {a}")
print(f"b = {b}")
sys.stdout.flush()
print("password (hex)? ", end="", flush=True)
guess = sys.stdin.readline().strip().lower()
if guess == pw.hex():
print(flag)
else:
print("wrong")
main()
思路
服务端给出环
上的 Ring-LWE-like 样本:
目标是从公开的 恢复二进制多项式 。环 上的多项式 在文中也用向量 表示。
服务端要求提交一个能通过固定 bases Miller-Rabin、但实际为合数的 , 使用著名强伪素数:
代码中选择因子 ,因为较小的 可以明显加速 LLL,且 ,原方程自动投影为:
我们不直接恢复 512 个 bit,而是递归计算:
注意到 , 因此 也可以被写作一个 维的向量 。且由于 是 0-1 系数,有 .
Bootstrap:
模 :
其中需要满足
因此直接枚举 再检查 是否在范围内,就可以恢复 的值。
Lift:
假设已经知道
目标是恢复下一层的 , 令 .
模 时有 ,所以
模 时有 。我们发现,只要知道
就能求出 , 进而得到 .
这里有 , 因此要搜索的 满足 . 不过这样搜索次数仍然是
不可接受,于是考虑利用其他关系。
那么 满足什么关系呢?根据给出的 LWE 方程,有
其中 . 而模 的乘法是 negacyclic 卷积, 将乘法写成矩阵得
下面我们需要给 系数一个共同的 bound. 我们有 ,第一步 让每个系数 bound 是原来的 倍,因此每个系数都在 ;第二步是系数相减,因此有
Kannan embedding 恢复差分
重写方程
可以看作是一个 CVP 问题,对于给定格点 , 求它在格
下的最近向量,然后恢复出 .
这里加入 是因为我们还想要约束 的范数。我们要求的最近向量就是 , 和目标向量相差 , 因此最好让 . 下面我们仍然保留 .
对上面的 CVP 问题进行 Kannan 嵌入得到新的格,它的基是:
因此 上的 CVP 被转化成了 上的 SVP (尽管范数并不完全对上).
我们希望这个最短向量是
或者
这样求出 之后,还需要检验 的每个分量是否都在 里面,且是整数。
然后直接 LLL 计算 SVP 即可。
剪枝
若某个 分量为 或为 ,则只能拆成:
对应差分(即 的对应分量)必为 ,无需求解。
以及可以根据格的维数删掉一些行,让 LLL 更快结束,实测几乎没有得到过不符合要求的解。
剪枝后运行结果
x^1 - 1 -> x^2 - 1: 1 variables, 3 dimensions
x^2 - 1 -> x^4 - 1: 2 variables, 5 dimensions
x^4 - 1 -> x^8 - 1: 4 variables, 9 dimensions
x^8 - 1 -> x^16 - 1: 8 variables, 17 dimensions
x^16 - 1 -> x^32 - 1: 16 variables, 33 dimensions
x^32 - 1 -> x^64 - 1: 32 variables, 65 dimensions
x^64 - 1 -> x^128 - 1: 64 variables, 129 dimensions
x^128 - 1 -> x^256 - 1: 117 variables, 185 dimensions
x^256 - 1 -> x^512 - 1: 128 variables, 207 dimensions
exploit.py
from __future__ import annotations
from typing import List, Optional, Sequence, Tuple
from fpylll import IntegerMatrix, LLL
Q = 3825123056546413051
N = 512
P = 34233211
SCALE = 5
SAMPLE_SLACK = 16
LARGE_LATTICE_THRESHOLD = 192
LARGE_LATTICE_OFFSET = -50
def center_mod(value: int, modulus: int = Q) -> int:
value %= modulus
return value - modulus if value > modulus // 2 else value
def ring_mul(a: Sequence[int], b: Sequence[int]) -> List[int]:
"""Multiply in Z_Q[x] / (x^N - 1)."""
out = [0] * N
for i, ai in enumerate(a):
for j, bj in enumerate(b):
out[(i + j) % N] += ai * bj
return [value % Q for value in out]
def fold(poly: Sequence[int], size: int, modulus: int) -> List[int]:
"""Reduce a polynomial modulo x^size - 1."""
out = [0] * size
for i, value in enumerate(poly):
out[i % size] += value
return [value % modulus for value in out]
def project_minus(poly: Sequence[int], size: int) -> List[int]:
"""Reduce modulo x^(2*size) - 1 and then map x^size to -1."""
reduced = fold(poly, 2 * size, P)
return [(reduced[i] - reduced[i + size]) % P for i in range(size)]
def negacyclic_matrix(poly: Sequence[int]) -> List[List[int]]:
"""Return the matrix for multiplication by poly modulo x^m + 1."""
size = len(poly)
return [
[
center_mod(
poly[row - column]
if row >= column
else -poly[size + row - column],
P,
)
for column in range(size)
]
for row in range(size)
]
def mat_vec_mul(matrix: Sequence[Sequence[int]], vector: Sequence[int]) -> List[int]:
return [sum(a * b for a, b in zip(row, vector)) for row in matrix]
def build_embedding_basis(
matrix: Sequence[Sequence[int]],
target: Sequence[int],
) -> IntegerMatrix:
"""
Build the Kannan embedding of
{(SCALE*d, A*d + P*k) : d, k in Z}.
"""
samples = len(matrix)
variables = len(matrix[0])
dimension = variables + samples
basis = IntegerMatrix(dimension + 1, dimension + 1)
for variable in range(variables):
basis[variable, variable] = SCALE
for sample in range(samples):
basis[variable, variables + sample] = matrix[sample][variable]
for sample in range(samples):
basis[variables + sample, variables + sample] = P
for column, value in enumerate(target):
basis[dimension, column] = value
basis[dimension, dimension] = 1
return basis
def recover_initial_weight(a: Sequence[int], b: Sequence[int]) -> List[int]:
a_sum = sum(a) % Q
b_sum = sum(b) % Q
candidates = [
weight
for weight in range(N + 1)
if -4 * N <= center_mod(b_sum - a_sum * weight) <= 3 * N
]
if len(candidates) != 1:
raise RuntimeError(f"expected one initial weight, got {candidates}")
return candidates
def choose_sample_count(size: int, variables: int) -> int:
samples = min(size, variables + SAMPLE_SLACK)
if variables + samples <= LARGE_LATTICE_THRESHOLD:
return samples
return min(size, max(1, variables + LARGE_LATTICE_OFFSET))
def decode_split(
parent: Sequence[int],
active_columns: Sequence[int],
active_difference: Sequence[int],
child_bound: int,
) -> Optional[Tuple[List[int], List[int], List[int]]]:
"""Recover children u, v from parent = u + v and difference = u - v."""
difference = [0] * len(parent)
for column, value in zip(active_columns, active_difference):
difference[column] = value
first = []
second = []
for total, delta in zip(parent, difference):
if (total + delta) % 2:
return None
left = (total + delta) // 2
right = (total - delta) // 2
if not (0 <= left <= child_bound and 0 <= right <= child_bound):
return None
first.append(left)
second.append(right)
return first, second, difference
def recover_difference(
a: Sequence[int],
b: Sequence[int],
parent: Sequence[int],
size: int,
) -> List[int]:
"""
Lift s modulo x^size - 1 to s modulo x^(2*size) - 1.
Projecting modulo x^size + 1 gives a small-secret equation for
difference = first_half - second_half.
"""
child_bound = N // (2 * size)
a_minus = project_minus(a, size)
b_minus = project_minus(b, size)
matrix = negacyclic_matrix(a_minus)
active_columns = [
column
for column, total in enumerate(parent)
if total not in (0, 2 * child_bound)
]
if not active_columns:
return [value // 2 for value in parent] * 2
samples = choose_sample_count(size, len(active_columns))
reduced_matrix = [
[matrix[row][column] for column in active_columns]
for row in range(samples)
]
target = (
[0] * len(active_columns)
+ [center_mod(value, P) for value in b_minus[:samples]]
)
basis = build_embedding_basis(reduced_matrix, target)
LLL.reduction(basis, delta=0.99)
for row in basis:
if abs(row[-1]) != 1:
continue
sign = -1 if row[-1] > 0 else 1
scaled_difference = [
sign * row[column]
for column in range(len(active_columns))
]
if any(value % SCALE for value in scaled_difference):
continue
split = decode_split(
parent,
active_columns,
[value // SCALE for value in scaled_difference],
child_bound,
)
if split is None:
continue
first, second, difference = split
residual = [
center_mod(rhs - lhs, P)
for rhs, lhs in zip(b_minus, mat_vec_mul(matrix, difference))
]
if all(abs(value) <= 7 * child_bound for value in residual):
print(
f"x^{size} - 1 -> x^{2 * size} - 1: "
f"{len(active_columns)} variables, {basis.nrows} dimensions"
)
return first + second
raise RuntimeError("Kannan embedding did not produce a valid split")
def solve(a: Sequence[int], b: Sequence[int]) -> Tuple[List[int], List[int]]:
"""Recover the binary secret s and the original error e."""
if len(a) != N or len(b) != N:
raise ValueError(f"a and b must each contain {N} coefficients")
secret = recover_initial_weight(a, b)
size = 1
while size < N:
secret = recover_difference(a, b, secret, size)
size *= 2
error = [
center_mod(rhs - lhs)
for rhs, lhs in zip(b, ring_mul(a, secret))
]
if any(value < -4 or value > 3 for value in error):
raise RuntimeError("recovered secret does not reproduce a valid error")
return secret, error