帮我非常详细的讲解这道ctf题目中的每一个知识点,具体是怎样证明怎样利用,把每一个知识点与解决的代码...

Creato il: 23 marzo 2026

Risposto usando GPT-5.4 Thinking da Chat01

Domanda

ecc.py
offical.py
坐标.txt

帮我非常详细的讲解这道ctf题目中的每一个知识点,具体是怎样证明怎样利用,把每一个知识点与解决的代码对应起来,并给出学习的相关路径,方法

Python

python
x = [] # read from /mnt/data/坐标.txt maybe easier eval after "x = " import ast, re text=open('/mnt/data/坐标.txt','r',encoding='utf-8').read() data=ast.literal_eval(text.split('=',1)[1].strip()) len(data), data[0] (73, (4713513545399586887294281187501009141689080934674926984853052046637141607331993392136186920709675152470824454822335527736604585312216293390500894812356575, 10152198551269550712132843544953877513974069303791371586626547671550636936369607844350995807520993189830394615282586805640124173078292476320861958178531199))

Python

python
from math import gcd from functools import reduce x=data def find_gcd(numbers): r=numbers[0] for n in numbers[1:]: r=gcd(r,n) return r c=[] for i in range(0,68,4): x1,y1=x[i] x2,y2=x[i+1] x3,y3=x[i+2] x4,y4=x[i+3] t1=(y1*y1-x1*x1*x1) t2=(y2*y2-x2*x2*x2) t3=(y3*y3-x3*x3*x3) t4=(y4*y4-x4*x4*x4) x_1=(x1-x3)*(t1-t2)-(x1-x2)*(t1-t3) x_2=(x1-x4)*(t1-t2)-(x1-x2)*(t1-t4) c.append(gcd(abs(x_1),abs(x_2))) p=find_gcd(c) p, p.bit_length() (11093300438765357787693823122068501933326829181518693650897090781749379503427651954028543076247583697669597230934286751428880673539155279232304301123931419, 512)

Pensiero

Clarifying naming and recovery approach

The points C are random, not part of the curve, and could be confusing if not clarified. I might mention deriving p recovery using a determinant approach where points satisfy a linear relation. However, this may be overly complex. Alternatively, I could discuss Gröbner bases and solving for a and b, using mod p arithmetic while explaining why E.gens()[0] is a generator. Also, I could mention checking the order of a point when p is prime.

Thought for 7m 28s

Risposta

下面我按“题目本身在干什么 → 每一步为什么能破 → 官方解每段代码到底对应什么数学 → 你该怎么学这条攻击链”来讲。先给结论:

这题的根本漏洞不是单一的一个点,而是三层问题叠在一起:

  1. 把椭圆曲线参数藏起来,但直接给了很多曲线点坐标,于是可以先从坐标反推出模数 pp 和曲线参数 a,ba,b
  2. 这条曲线是 anomalous curve(异常曲线),满足 #E(Fp)=p\#E(\mathbb F_p)=p,因此可以用 Smart Attack 把椭圆曲线离散对数直接降成普通整数比值。
  3. 题目把每个输出都写成 Qi=miR+niE+hiCQ_i = m_iR+n_iE+h_iC 其中 R,E,CR,E,C全局固定的 3 个点。也就是说,所有输出本质上都落在一个“秩最多 3”的线性结构里。离散对数一旦被降掉,再用 LLL + 正交格/右核 就能把隐藏向量 m,n,hm,n,h 从关系中抠出来。 (ecc.py) (offical.py)

1. 先把题目脚本翻译成人话

题目脚本 ecc.py 做的事是:生成一个 512-bit 素数 pp,随机取椭圆曲线

E:y2=x3+ax+b(modp)E: y^2=x^3+ax+b \pmod p

再随机取 3 个点 R,E,CR,E,C(注意这里变量名 E 是一个,不是曲线,很容易看混),然后对 flag 的每个字节单独生成一个点

Qi=b2l(mi)R+b2l(noncei)E+b2l(sha256(nonceimi))C.Q_i = \text{b2l}(m_i)R+\text{b2l}(\text{nonce}_i)E+\text{b2l}(\text{sha256}(\text{nonce}_i\|m_i))C.

最后把所有 QiQ_i 输出。 (ecc.py)

这里几个知识点一定要看懂:

1.1 椭圆曲线群

在有限域 Fp\mathbb F_p 上,曲线点和无穷远点 O\mathcal O 组成阿贝尔群。
kPkP” 表示点 PP 自加 kk 次。
所以题里

mR+nE+hCmR+nE+hC

就是 3 个点的线性组合。

1.2 bytes_to_long

题目想把消息字节、nonce、hash 都转成整数,作为标量去乘点。ecc.py 里导入了 bytes_to_long,但后面写的是 b2l(...),这是一个命名不一致的小坑;实际意图显然是“把字节串按大端转整数”。 (ecc.py)

1.3 pad 的结构

题目里的 pad 是:

python
pad = lambda m: urandom(8) + m + b'\x00' * (ZZ(p).nbits() // 8 - len(m) - 8 - 1)

如果 m 是一个 flag 字节,那么 pad 后的结构就是:

text
8字节随机前缀 || 1字节flag字符 || 一大串 0x00

这个结构极其关键。因为它意味着每个 mim_i 都带有一个巨大的公共因子 25654=2432256^{54}=2^{432}(这里 pp 是 512 bit,所以后面补了 54 个 0 字节)。也就是说:

mi=2432ti,m_i = 2^{432}\cdot t_i,

其中 tit_i 只剩 9 字节,而且它的最低字节就是 flag 字符。这正是最后官方解里 long_to_bytes(...)[-1] 能直接打印字符的原因。 (ecc.py)


2. 第一步:为什么能从点坐标反推出 pp

官方解先读入一堆点坐标 x = [(x1,y1), ...]。这些点就是题目输出的 QiQ_i 坐标。坐标.txt 给了 73 个点,offical.py 里也把它们硬编码进去了。 (offical.py) (坐标.txt)

已知每个点都满足同一条曲线方程

yi2xi3+axi+b(modp).y_i^2 \equiv x_i^3 + ax_i + b \pmod p.

移项得

ui:=yi2xi3axi+b(modp).u_i:=y_i^2-x_i^3 \equiv ax_i+b \pmod p.

于是对任意两点 i,ji,j

uiuja(xixj)(modp).u_i-u_j \equiv a(x_i-x_j)\pmod p.

再拿第三个点 kk 消去 aa

(xixk)(uiuj)(xixj)(uiuk)(modp).(x_i-x_k)(u_i-u_j)\equiv (x_i-x_j)(u_i-u_k)\pmod p.

移到一边:

(xixk)(uiuj)(xixj)(uiuk)0(modp).(x_i-x_k)(u_i-u_j)-(x_i-x_j)(u_i-u_k)\equiv 0\pmod p.

这说明上面这个整数表达式一定被 pp 整除

这就是官方解里这几行的来源:

python
x_1=(x1-x3)*((y1**2-x1**3)-(y2**2-x2**3))-(x1-x2)*((y1**2-x1**3)-(y3**2-x3**3)) x_2=(x1-x4)*((y1**2-x1**3)-(y2**2-x2**3))-(x1-x2)*((y1**2-x1**3)-(y4**2-x4**3)) c.append(gcd((x_1,x_2))) p=find_gcd(c)

它做的不是魔法,而是:

  • 先用 3 个点构造一个“必被 pp 整除”的量;
  • 再用第 4 个点再构造一个;
  • 先对这一组取 gcd,尽量消掉随机噪声因子;
  • 最后把很多组的 gcd 再取总 gcd,通常就只剩真正的 pp

这是一个非常经典的“从模同余关系中抽出未知模数”技巧。 (offical.py)

我按这个公式算出来的模数是:

p=11093300438765357787693823122068501933326829181518693650897090781749379503427651954028543076247583697669597230934286751428880673539155279232304301123931419.p= 11093300438765357787693823122068501933326829181518693650897090781749379503427651954028543076247583697669597230934286751428880673539155279232304301123931419.

3. 第二步:为什么能反推出曲线参数 a,ba,b

有了 pp 以后,点方程

yi2xi3+axi+b(modp)y_i^2 \equiv x_i^3 + ax_i + b \pmod p

就变成了关于 a,ba,b 的线性方程。

任取两点:

a(x1x2)(y12x13)(y22x23)(modp),a(x_1-x_2)\equiv (y_1^2-x_1^3)-(y_2^2-x_2^3)\pmod p,

所以

a(y12x13)(y22x23)x1x2(modp).a \equiv \frac{(y_1^2-x_1^3)-(y_2^2-x_2^3)}{x_1-x_2}\pmod p.

再代回去求 bb

官方解用了 Gröbner basis:

python
P.<a, b> =PolynomialRing(Zmod(p)) f1 =x[0][0]**3+a*x[0][0] +b -x[0][1]**2 f2 =x[1][0]**3+a*x[1][0] +b -x[1][1]**2 f3 =x[2][0]**3+a*x[2][0] +b -x[2][1]**2 Ideal = P.ideal(F) I = Ideal.groebner_basis() res =[x.constant_coefficient() for x in I] a = -res[0]%p b = -res[1]%p

这本质上是在求解一个模 pp 的二元线性方程组。
说白了:这一步用 Gröbner basis 是“大炮打蚊子”,直接线性代数也能做,但在 Sage 里这样写很顺手。 (offical.py)

我按坐标反推出:

a=490963434153515882934487973185142842357175523008183292296815140698999054658777820556076794490414610737654365807063916602037816955706321036900113929329671a= 490963434153515882934487973185142842357175523008183292296815140698999054658777820556076794490414610737654365807063916602037816955706321036900113929329671 b=7668542654793784988436499086739239442915170287346121645884096222948338279165302213440060079141960679678526016348025029558335977042712382611197995002316466b= 7668542654793784988436499086739239442915170287346121645884096222948338279165302213440060079141960679678526016348025029558335977042712382611197995002316466

而且判别式 Δ=16(4a3+27b2)≢0(modp)\Delta=-16(4a^3+27b^2)\not\equiv 0\pmod p,说明这确实是一条非奇异椭圆曲线。


4. 第三步:为什么这里可以用 Smart Attack

这是整题最核心的密码学知识点。

4.1 Smart Attack 的适用条件

Smart Attack 不是“通用 ECC DLP 算法”,它只在异常曲线上成立,即:

#E(Fp)=p.\#E(\mathbb F_p)=p.

这种曲线的 trace 为 1,因为

#E(Fp)=p+1t=pt=1.\#E(\mathbb F_p)=p+1-t=p \quad\Longrightarrow\quad t=1.

这类曲线叫 anomalous curve
一旦曲线异常,离散对数问题会通过 pp-adic 提升和形式群线性化,被降到一个几乎线性的对象上。

官方解直接上了:

python
G = E.gens()[0] dec.append(SmartAttack(G, E(i), p))

这说明解题人已经知道这题是 intended 的 anomalous-curve 路线。
严格做题时,应该先检查 #E(Fp)=p\#E(\mathbb F_p)=p;官方脚本把这一步省略了。 (offical.py)

4.2 Smart Attack 的数学直觉

设群里有

Q=kP.Q = kP.

在普通椭圆曲线里,求 kk 很难;但在 anomalous curve 上,可以把曲线从 Fp\mathbb F_p 提升到 Qp\mathbb Q_p 上,然后看点的 pp 倍:

pP~,pQ~.p\widetilde P,\quad p\widetilde Q.

它们会掉进形式群(formal group)里,而形式群在局部参数

t=xyt = -\frac{x}{y}

下近似就是“加法群”。于是:

  • Q=kPQ=kP,则在形式群里也有对应的线性关系;
  • 因而 ϕ(Q)=kϕ(P)\phi(Q)=k\phi(P) 成立;
  • 最后 k=ϕ(Q)ϕ(P).k=\frac{\phi(Q)}{\phi(P)}.

官方解中的:

python
p_times_P = p*P_Qp p_times_Q = p*Q_Qp x_P,y_P = p_times_P.xy() x_Q,y_Q = p_times_Q.xy() phi_P = -(x_P/y_P) phi_Q = -(x_Q/y_Q) k = phi_Q/phi_P

就是在实现这个比值。 (offical.py)

4.3 lift_xQpQ_p 提升在干什么

SmartAttack 里还有两段你必须读懂:

python
Eqp = EllipticCurve(Qp(p, 2), [ ZZ(t) + randint(0,p)*p for t in E.a_invariants() ]) P_Qps = Eqp.lift_x(ZZ(P.xy()[0]), all=True) Q_Qps = Eqp.lift_x(ZZ(Q.xy()[0]), all=True)

它们的作用是:

  1. 把曲线从 Fp\mathbb F_p 提升到 QpQ_p(只要精度 p2p^2 就够了,所以 Qp(p,2))。
  2. 已知模 ppxx 坐标,向上找所有可能的 pp-adic lift。
  3. 因为一个 xx 往往对应两个 yy(正负号),所以代码再通过
python
if GF(p)(P_Qp.xy()[1]) == P.xy()[1]:

筛掉不匹配的那个 lift,只保留和原来点同余的那个。 (offical.py)


5. 第四步:为什么离散对数出来以后会变成“秩 3”问题

现在设 GG 是曲线群生成元。因为 anomalous curve 的群阶是 pp,而 pp 是素数,所以任意非零点都可写成某个标量乘 GG

R=rG,E=eG,C=cG.R=rG,\quad E=eG,\quad C=cG.

那么题目输出的每个点就是

Qi=miR+niE+hiC=(mir+nie+hic)G.Q_i = m_iR+n_iE+h_iC = (m_ir+n_ie+h_ic)G.

QiQ_i 做离散对数(官方解里 SmartAttack(G, E(i), p))以后,得到一个整数

di=logG(Qi)mir+nie+hic(modp).d_i = \log_G(Q_i) \equiv m_ir+n_ie+h_ic \pmod p.

于是 73 个样本满足:

d=rm+en+ch(modp),d = r\,m + e\,n + c\,h \pmod p,

这里 d,m,n,hd,m,n,h 都是长度 73 的向量。
这就是“所有数据都只落在 3 个隐藏向量张成的子空间里”。

这一点非常关键:
题目虽然额外加了 noncesha256(nonce || m),看起来很复杂,但在群论上它们只是又加了两个“系数向量”。固定的基点只有 3 个,所以本质维度最多就是 3。
sha256 没把结构打散,只是把隐藏子空间从 2 维加到 3 维而已。 (ecc.py) (offical.py)


6. 第五步:官方的 LLL 到底在找什么

官方解构造了这个格基:

python
M =Matrix(ZZ,74,74) for i in range(73): M[i,i]=1 M[i,-1]=dec[i] M[-1,-1]=p R=M.LLL()

这个矩阵的每一行是:

  • ii 行:ei+die74e_i + d_i e_{74}
  • 最后一行:pe74p e_{74}

所以它张成的格里任意向量都形如

(u1,,u73,uidi+tp).(u_1,\dots,u_{73}, \sum u_i d_i + tp).

如果一个格向量的最后一维是 0,那么就说明

i=173uidi0(modp).\sum_{i=1}^{73} u_i d_i \equiv 0 \pmod p.

也就是找到了一个关于 did_i 的模 pp 线性关系。

所以这段代码:

python
LK=[] for i in R: if i[-1] != 0: break else: LK.append(i[:-1]) LK = Matrix(ZZ, LK)

就是把那些“最后一维为 0”的短关系全收集起来,形成关系矩阵 LKLK。 (offical.py)


7. 为什么 right_kernel() 能把隐藏的 m,n,hm,n,h 找回来

这是这题最值得学的格技巧。

因为

dirmi+eni+chi(modp),d_i \equiv r m_i + e n_i + c h_i \pmod p,

对任意关系向量 u=(u1,,u73)u=(u_1,\dots,u_{73}),如果

uidi0(modp),\sum u_i d_i \equiv 0 \pmod p,

那么代入得

ru,m+eu,n+cu,h0(modp).r\langle u,m\rangle + e\langle u,n\rangle + c\langle u,h\rangle \equiv 0 \pmod p.

如果 uu 是 LLL 找到的短关系,而 r,e,cr,e,c 又是随机数,那么最自然、最稳定的情况就是:

u,m=u,n=u,h=0.\langle u,m\rangle = \langle u,n\rangle = \langle u,h\rangle = 0.

也就是说,短关系向量 uu 落在

span(m,n,h)\mathrm{span}(m,n,h)^\perp

里。

因此,把很多这样的 uu 组成矩阵 LKLK 后,LK.right_kernel() 就会把它们的正交补求出来,于是恢复出:

span(m,n,h).\mathrm{span}(m,n,h).

这就是:

python
LK1 =LK.right_kernel().basis() key=Matrix(ZZ,LK1).LLL()[0]

的数学意义。
严格说,这一步带一点启发式:它依赖“短模关系大概率来自真正的正交关系而不是偶然模消掉”。但在 CTF 场景下,512-bit 模数 + 73 个样本 + 维数只有 3,这个启发式非常稳。 (offical.py)


8. 为什么最后最短向量会直接泄露 flag

最妙的是最后这行:

python
for i in key: print(chr(long_to_bytes(ZZ(i))[-1]),end='')

乍一看很怪,因为 mim_i 不是“单字节”,而是 pad 过的大整数。

但前面说过:

mi=2432ti,m_i = 2^{432}\cdot t_i,

其中 tit_i 只有 9 字节,结构是:

text
8字节随机前缀 || 1字节flag字符

所以:

  • mm 向量本身很大;
  • 但把公共因子 24322^{432} 除掉后,得到的 primitive 向量 tt 很短;
  • span(m,n,h)\mathrm{span}(m,n,h) 里:
    • nonce 向量是 16 字节量级;
    • hash 向量是 32 字节量级;
    • tt 只有 9 字节量级;
  • 所以对这个 3 维空间再做一次 LLL,最短向量很可能就是 tt(或它的相反数)。

于是每个坐标转回字节串后,最后一个字节刚好就是那个 flag 字符。
这就解释了为什么官方解最后可以直接 [-1] 取字符。 (ecc.py) (offical.py)


9. 把官方解按模块和知识点一一对上

你可以这样记:

模块 A:从点坐标恢复曲线

对应代码:

  • find_gcd
  • x_1, x_2
  • c.append(...)
  • p=find_gcd(c)

对应知识点:

  • 有限域椭圆曲线方程
  • 消元
  • 从同余式构造“必被未知模数整除”的整数
  • gcd 抽模数

模块 B:恢复 a,ba,b

对应代码:

  • PolynomialRing(Zmod(p))
  • f1,f2,f3
  • groebner_basis
  • a=-res[0], b=-res[1]

对应知识点:

  • pp 多项式环
  • 理想/Gröbner basis
  • 本质是线性方程组求解

模块 C:Smart Attack

对应代码:

  • Eqp = EllipticCurve(Qp(p, 2), ...)
  • lift_x
  • p_times_P = p*P_Qp
  • phi_P = -(x_P/y_P)
  • k = phi_Q/phi_P

对应知识点:

  • anomalous curve
  • pp-adic 提升
  • formal group
  • 用局部参数把 DLP 线性化

模块 D:关系格 + 右核

对应代码:

  • M[i,i]=1; M[i,-1]=dec[i]
  • M[-1,-1]=p
  • R=M.LLL()
  • LK.right_kernel().basis()
  • Matrix(ZZ,LK1).LLL()[0]

对应知识点:

  • LLL
  • 模关系格
  • 短向量对应隐藏结构的正交关系
  • 用右核恢复低秩隐藏子空间
  • 再一次 LLL 找最短的“消息向量”

10. 这题最本质的“设计错误”到底是什么

从出题角度看,最致命的不是某一行代码,而是这三个结构同时存在:

10.1 固定基点复用

所有样本都使用同一组 R,E,CR,E,C
这让所有输出都落在同一个低维子空间里。

10.2 曲线参数没公开,但坐标全公开

这类隐藏是没有意义的。只要给足够多点,p,a,bp,a,b 都能反推。

10.3 用了 anomalous curve

一旦是异常曲线,ECC DLP 本身就已经不安全了。

所以这题不是“hash 用错了”,也不是“nonce 长度不够”,而是群结构本身已经被选崩了,而且还叠加了固定 3 基点带来的低秩泄漏。


11. 你学习这题应该怎么走

我建议按下面这条路学,不要一上来就看 Smart Attack 论文。

第一阶段:把椭圆曲线基础打牢

先会这些:

  1. 有限域 Fp\mathbb F_p
  2. 曲线 y2=x3+ax+by^2=x^3+ax+b
  3. 判别式为什么不能为 0
  4. 点加法、倍点、标量乘法
  5. 群阶、生成元、离散对数

练习方法:

  • 自己选一个小素数 p<1000p<1000,手写一条曲线;
  • 枚举点数;
  • 手算几次点加法;
  • 写 Python 实现点加法和标量乘。

第二阶段:学“从点恢复曲线”

目标:

  1. 已知若干点,推回 pp
  2. 已知 pp,推回 a,ba,b

练习方法:

  • 自己随机生成一条小曲线;
  • 随机打几个点;
  • 只保留坐标;
  • 不看原参数,自己写恢复脚本。

这一步练熟以后,你会对“隐藏参数但公开样本”有天然警觉。


第三阶段:学 anomalous curve 和 Smart Attack

目标:

  1. 知道 anomalous curve 是什么
  2. 知道为什么 #E(Fp)=p\#E(\mathbb F_p)=p 会导致异常脆弱
  3. 会用 Sage 跑一个 toy 版 Smart Attack

学习方法:

  • 先看椭圆曲线 formal group 的直觉,不要先追完整证明;
  • 先接受结论:异常曲线能把 DLP 线性化;
  • 再看 lift_xQpQ_px/y-x/y 这个参数为什么起作用。

你只要能读懂“为什么代码里要先 lift,再乘 p,再取 x/y-x/y 比值”,就已经超过大多数只会抄脚本的人了。


第四阶段:学格攻击和“右核恢复隐藏子空间”

目标:

  1. 理解 LLL 在找什么
  2. 理解“短关系”为什么逼近真正正交关系
  3. 理解 right_kernel() 为什么能把原始隐藏向量张成的空间找回来

学习方法:

  • 先在普通整数线性代数里理解“行空间/右核/正交补”;
  • 再把它迁移到“模 pp 关系 + 格”上;
  • 自己造一个小实验: d=rm+en+ch(modp)d = r m + e n + c h \pmod p m,n,hm,n,h 比较短,看看能不能用同样方法把它们找回来。

12. 我给你的实战学习顺序

最推荐这个顺序:

有限域 → 椭圆曲线群 → 从点恢复曲线 → anomalous curve → Smart Attack → LLL → 正交格/右核恢复

不要反过来。
反过来你会变成“会跑 Sage,但不知道自己在干什么”。


13. 这题里几个很容易忽略的细节

最后把一些容易漏掉的点列出来:

  1. ecc.py 里的 b2l 命名和导入不一致,这是个脚本细节坑。 (ecc.py)
  2. M=Matrix(ZZ,len(flag),3) 在题目脚本里没有被使用,基本可以视为无关项。 (ecc.py)
  3. 坐标.txt 里一共 73 个点,所以真实 flag 长度对应 73,而不是 ecc.py 里那个占位字符串的长度。 (坐标.txt)
  4. pad 不是“防护”,反而制造了一个巨大的公共 24322^{432} 因子,最后等于把消息向量进一步“短化”了。 (ecc.py)
  5. sha256(nonce+m) 不是安全来源,它只是在固定 3 个基点的框架下多增加了一个隐藏向量维度。 (ecc.py)
  6. 官方解里先 LLL 找关系,再 right_kernel(),本质是在做“先恢复正交补,再取正交补的正交补”。这是非常典型的线性/格思路。 (offical.py)

如果你愿意,我下一条可以直接给你一份**“带逐行注释的官方解重写版”**,把 offical.py 改写成更适合学习的 Sage 脚本,并把每一行旁边都标上“这一行在数学上等价于什么”。

Condividi questa Q&A