这是一个基于椭圆曲线的LWE(Learning With Errors)变种问题,通常称为 EC-LWE。
核心分析
- 问题结构:
- 题目给出了矩阵 AAA 和结果向量 b⃗\vec{b}b。
- 方程形式为: b⃗=A⋅s⃗+e⃗\vec{b} = A \cdot \vec{s} + \vec{e}b=A⋅s+e。
- 运算是在椭圆曲线 EEE 上进行的(点加法和标量乘法)。
- 漏洞点:
- 弱误差 (Weak Error): 这里的误差 e⃗\vec{e}e 不是通常LWE中的高斯分布小误差,而是从仅包含两个随机点 {E0,E1}\{E_0, E_1\}{E0,E1} 的集合中选取的。这意味着误差只有 1 bit 的熵。
- 超奇异曲线 (Supersingular Curve):
- 给定的 p=1096126227998177188652856107362412783873814431647p = 1096126227998177188652856107362412783873814431647p=1096126227998177188652856107362412783873814431647。
- 计算 p(mod3)p \pmod 3p(mod3),结果为 222。
- 对于 y2=x3+5y^2 = x^3 + 5y2=x3+5,当 p≡2(mod3)p \equiv 2 \pmod 3p≡2(mod3) 时,曲线是超奇异的,且阶 (Order) 刚好为 p+1p+1p+1。
- 这不仅意味着我们可以利用 MOV 攻击将点映射到有限域,而且在这个特定大小下(约160位),离散对数(DLog)通常是可解的(如果 p+1p+1p+1 是光滑的,或者通过 Pollard's Rho/Index Calculus)。
攻击策略
第一步:将 EC-LWE 转换为 标量 LWE
由于是在椭圆曲线上,直接处理点比较麻烦。我们需要计算所有点 PPP 相对于某个生成元 GGG 的离散对数 xxx,使得 P=xGP = xGP=xG。
由于曲线是超奇异的且 ppp 只有 160 bit,我们可以尝试计算 AAA、b⃗\vec{b}b 中所有元素的离散对数。这将问题转化为模 NNN (曲线的阶) 上的线性方程组:
b⃗′=A⋅s⃗′+e⃗′(modN)\vec{b}' = A \cdot \vec{s}' + \vec{e}' \pmod N
b′=A⋅s′+e′(modN)
其中 e⃗′\vec{e}'e′ 的每个元素只能取两个标量值之一 {e0,e1}\{e_0, e_1\}{e0,e1}。
第二步:利用 Kernel (核) 消除 s⃗\vec{s}s
由于 AAA 是 137×73137 \times 73137×73 的矩阵,这是一个超定方程组。我们可以找到矩阵 AAA 的左零空间(Kernel),即找到向量 v⃗\vec{v}v 使得 v⃗⋅A≡0⃗(modN)\vec{v} \cdot A \equiv \vec{0} \pmod Nv⋅A≡0(modN)。
将 v⃗\vec{v}v 作用于方程两边:
v⃗⋅b⃗′=v⃗⋅(As⃗′)+v⃗⋅e⃗′=0+v⃗⋅e⃗′\vec{v} \cdot \vec{b}' = \vec{v} \cdot (A \vec{s}') + \vec{v} \cdot \vec{e}' = 0 + \vec{v} \cdot \vec{e}'
v⋅b′=v⋅(As′)+v⋅e′=0+v⋅e′
y=∑viϵiy = \sum v_i \epsilon_i
y=∑viϵi
其中 ϵi∈{e0,e1}\epsilon_i \in \{e_0, e_1\}ϵi∈{e0,e1}。这实际上是一个子集和问题(或者说是 Hidden Subset Sum),我们可以设 ϵi=e0+ui(e1−e0)\epsilon_i = e_0 + u_i(e_1 - e_0)ϵi=e0+ui(e1−e0),其中 ui∈{0,1}u_i \in \{0, 1\}ui∈{0,1}。
第三步:构造格 (Lattice) 使用 LLL 求解
我们会有多个 Kernel 向量,从而得到多个关于 uiu_iui 的线性方程。
Yk=(∑vk,i)e0+(∑vk,iui)δY_k = (\sum v_{k,i}) e_0 + (\sum v_{k,i} u_i) \delta
Yk=(∑vk,i)e0+(∑vk,iui)δ
通过代数变换消除未知的 e0e_0e0 和 δ=e1−e0\delta = e_1 - e_0δ=e1−e0,我们可以得到仅关于 uiu_iui 的模线性方程。
利用 LLL 算法(解决 Knapsack/Lattice 问题)恢复二进制向量 u⃗\vec{u}u。
第四步:恢复 s⃗\vec{s}s 并解密
一旦知道了 u⃗\vec{u}u,我们就知道了每一行使用了哪一个误差项。
修正 b⃗\vec{b}b 减去误差后,解线性方程组恢复 s⃗\vec{s}s。
SageMath 求解脚本
你需要将题目输出的 A 和 b 填入脚本中。
Python
from sage.all import *
from Crypto.Cipher import AES
from hashlib import md5
A_list = [ ... ]
b_list = [ ... ]
enc_flag = b'...'
p = 1096126227998177188652856107362412783873814431647
E = EllipticCurve(GF(p), [0, 5])
N = E.order()
print(f"[*] Curve Order N = {N}")
print(f"[*] Is N smooth? {factor(N)}")
try:
A_matrix = matrix(GF(p), 137, 73, A_list)
except:
A_matrix = matrix(GF(p), 137, 73, A_list)
B_points = [E(x, y) for x, y in b_list]
print("[*] Computing Discrete Logs (This might take a moment)...")
G = E.gens()[0]
try:
b_scalars = vector(GF(N), [G.discrete_log(pt) for pt in B_points])
print("[+] DLogs for b computed.")
except Exception as e:
print(f"[-] DLog failed: {e}")
exit()
A_mod_N = matrix(GF(N), 137, 73, [list(row) for row in A_matrix])
print("[*] Computing Kernel to eliminate s...")
K = A_mod_N.left_kernel()
V = K.basis_matrix()
num_kernel_rows = V.nrows()
print(f"[*] Found {num_kernel_rows} kernel vectors.")
y_vec = V * b_scalars
print("[*] Constructing LLL Matrix...")
sigma = [sum(row) for row in V]
Z = [sigma[0] * y_vec[i] - sigma[i] * y_vec[0] for i in range(num_kernel_rows)]
lattice_rows = []
num_eqs = num_kernel_rows - 2
dim = 137
M = []
for i in range(2, num_kernel_rows):
row_coeffs = []
for j in range(137):
term1 = sigma[0] * V[i, j] - sigma[i] * V[0, j]
term2 = sigma[0] * V[1, j] - sigma[1] * V[0, j]
coeff = Z[1] * term1 - Z[i] * term2
row_coeffs.append(Integer(coeff % N))
M.append(row_coeffs)
weight = Integer(2**100)
L_mat = Matrix(ZZ, 137 + num_eqs, 137 + num_eqs)
for i in range(137):
L_mat[i, i] = 1
for k in range(num_eqs):
L_mat[i, 137 + k] = M[k][i] * weight
for k in range(num_eqs):
L_mat[137 + k, 137 + k] = Integer(N) * weight
print("[*] Running LLL...")
L_reduced = L_mat.LLL()
u_recovered = None
for row in L_reduced:
is_solution = True
for k in range(num_eqs):
if row[137+k] != 0:
is_solution = False
break
if is_solution:
potential_u = row[:137]
if all(x in [0, 1, -1] for x in potential_u):
if sum(potential_u) < 0:
potential_u = [-x for x in potential_u]
if all(x in [0, 1] for x in potential_u):
print("[+] Found valid binary vector u!")
u_recovered = potential_u
break
if u_recovered is None:
print("[-] Failed to recover u.")
exit()
print("[*] Recovering s...")
diff_A = []
diff_b = []
indices_0 = [i for i, x in enumerate(u_recovered) if x == 0]
for k in range(len(indices_0) - 1):
idx1 = indices_0[k]
idx2 = indices_0[k+1]
row_A = A_mod_N[idx1] - A_mod_N[idx2]
val_b = b_scalars[idx1] - b_scalars[idx2]
diff_A.append(list(row_A))
diff_b.append(val_b)
indices_1 = [i for i, x in enumerate(u_recovered) if x == 1]
for k in range(len(indices_1) - 1):
idx1 = indices_1[k]
idx2 = indices_1[k+1]
row_A = A_mod_N[idx1] - A_mod_N[idx2]
val_b = b_scalars[idx1] - b_scalars[idx2]
diff_A.append(list(row_A))
diff_b.append(val_b)
Solver_Mat = matrix(GF(N), diff_A)
Solver_Vec = vector(GF(N), diff_b)
try:
s_scalars = Solver_Mat.solve_right(Solver_Vec)
print("[+] s recovered (scalars).")
except:
s_scalars = Solver_Mat[:73].solve_right(Solver_Vec[:73])
s_points = [s_val * G for s_val in s_scalars]
key = md5(str(s_points).encode()).digest()
cipher = AES.new(key=key, nonce=b"LWECC", mode=AES.MODE_CTR)
flag = cipher.decrypt(enc_flag)
print(f"[!] Flag: {flag}")