帮我非常详细的讲解这道ctf题目中的每一个知识点,具体是怎样证明怎样利用,把每一个知识点与解决的代码...
作成日: 2026年4月18日
使用モデル GPT-5.4 Thinking by Chat01
作成日: 2026年4月18日
使用モデル GPT-5.4 Thinking by Chat01
帮我非常详细的讲解这道ctf题目中的每一个知识点,具体是怎样证明怎样利用,把每一个知识点与解决的代码对应起来,并给出学习的相关路径,方法
下面我按“出题脚本 → 漏洞来源 → 论文定理 → 利用代码 → 学习路径”这条线,把这道 CTF 题从头到尾非常细地讲清楚。你给的三个材料分别是:
2025-1396.pdf,核心是针对 的 generalized Wiener-type attack,给出 Theorem 8 和 Theorem 9。(2025-1396.pdf)task.py,说明题目怎样生成 。(task.py)exp.py,说明实际怎么把论文攻击落地。(exp.py)这题本质不是普通 RSA,而是一个 RSA-like cryptosystem,使用的“欧拉函数”不再是
而是
论文明确说明,这一族系统的密钥方程是
当 就退化成普通 RSA;当 时就是这道题对应的情况。(2025-1396.pdf)
而 task.py 里正是这样生成参数的:
pythonphi = (p ** 6 - 1) * (q ** 6 - 1) d = getPrime(int(nbits * 2)) e = pow(d, -1, phi)
也就是:
这些都直接写在题目脚本里。(task.py)
task.py 内容非常短,但信息量很大:(task.py)
pythonnbits = 512 p = getPrime(nbits) q = getPrime(nbits) N = p * q phi = (p ** 6 - 1) * (q ** 6 - 1) d = getPrime(int(nbits * 2)) e = pow(d, -1, phi) m = bytes_to_long(flag) c = pow(m, e, N)
从这里可以立刻得到几个结论:
因为 都是 512-bit,所以 约 1024-bit。(task.py)
而
所以 的量级大约是 6144-bit。
但出题人取的 只有 1024-bit。也就是说,相对于模 ,这个 非常小。
这就是漏洞的根。
普通 RSA 中,Wiener 攻击告诉你:如果
里的 太小,那么 会非常逼近 或相关近似量,于是可以用连分数把 找出来。
这篇论文做的是它的 推广版。论文最后给出结论:
如果
那么给定 可以在多项式时间内分解 。(2025-1396.pdf)
而 exp.py 第一行注释就已经把这个界写出来了:(exp.py)
python# bound = sqrt((N ^ 6 - 162 * N ^ 3 + 1) / (2 * N * sqrt(N) + 1100 * N ^ 3))
现在看量级:
所以整个根号大约是
而题目里 约是 1024-bit,即 。 显然
所以它完全落在攻击范围里。
这题的核心不是加密写错了,而是密钥生成时把 取得太小。
这题的完整利用流程是:
利用密钥方程
把它改写成论文里的统一形式
在本题中其实就是
论文证明:如果 足够小,则
会出现在
的连分数收敛分数里。(2025-1396.pdf)
所以我们枚举这个连分数的每个 convergent,把它看成 的候选。
对每个候选 ,论文 Theorem 8 给出一个非常精妙的公式,可以构造出一个对 的近似值 ,误差小于 。(2025-1396.pdf)
一旦我们有了“误差不超过 的 近似值”,就能用 Coppersmith 小根技术把真正的 找出来。论文也明确调用了这个结论。(2025-1396.pdf)
有了 ,就得到 ,接着重建
求出 ,最后解密拿到 flag。(exp.py)
题目本质满足
也就是存在整数 ,使得
论文统一研究更一般的形式:
对我们这题,直接代入:
所以你在读 exp.py 时,一定要记住:
代码里的
a,b不是随便起的变量,它们就是论文里连分数阶段要恢复出来的那两个整数,在这题里最终对应 。(2025-1396.pdf)
这是论文 Theorem 9 里最核心的近似中心。论文先把
展开并整理,构造出一个“主项 + 误差项”的形式,最终得到:
其中 是一个可以被界住的误差项,而且满足
(2025-1396.pdf)
于是就能推出
再结合连分数经典定理:
若
则 一定是 的某个 convergent。(2025-1396.pdf)
所以 会出现在
的收敛分数序列里。(2025-1396.pdf)
这就是 exp.py 为什么从这里开头:(exp.py)
pythondenominator_approx = N^6 - 162 * N^3 + 1 cf = continued_fraction(e / denominator_approx) convergents = cf.convergents()
对应关系非常直接:
denominator_approxcf.convergents()这是整题第一个知识点,也是最容易被“会用不会证”的地方。
论文第 2.1 节给了经典定理:
若
且 ,那么 一定是 的一个收敛分数。(2025-1396.pdf)
这就是 Wiener 攻击以及各种 generalized Wiener attack 的根基。
论文第 3.2 节最终证明:
若
则
(2025-1396.pdf)
因此 会出现在这个连分数展开的收敛分数中。论文 Theorem 9 明确就是这么说的。(2025-1396.pdf)
exp.py 里:
pythoncf = continued_fraction(e / denominator_approx) convergents = cf.convergents() for conv in convergents: b = conv.numerator() a = conv.denominator()
这里不是随便试分数,而是在严格利用“正确的 必然是某个 convergent”这件事。(exp.py)
要特别注意变量方向:
conv = b/aconv.numerator() 对应论文里的 conv.denominator() 对应论文里的 这也正好解释了为什么代码写成:
pythonb = conv.numerator() a = conv.denominator()
这是整题最关键、最有技术含量的一步。
论文定义:
然后把 重写成关于 或 的形式。对于 ,论文给出:(2025-1396.pdf)
并继续整理成
然后由
得到
于是
这就是一个三次方程。论文调用 Cardano 公式解这个三次方程,从而构造出 。(2025-1396.pdf)
同理,利用 也能构造出 。(2025-1396.pdf)
论文的 Lemma 2 和 Lemma 3 证明:
(2025-1396.pdf)
于是
就有近似
并且误差仍然小于 。论文 Theorem 8 明确写出:(2025-1396.pdf)
注意这里代码里用的是 且近似公式里省去了那个很小的修正项,本质上就是论文 Theorem 8 里的 公式。
exp.py 里:(exp.py)
pythontmp1 = (N**3 - 1)**2 - (a * e / b) tmp2 = (N**3 + 1)**2 - (a * e / b) hat_p = round((0.5 * (sqrt(tmp1) + sqrt(tmp2)))^(1 / 3))
对应论文公式:
tmp1 对应
tmp2 对应
0.5 * (sqrt(tmp1) + sqrt(tmp2)) 对应
^(1/3) 就是立方根round(...) 是因为真实的 应该非常靠近整数 这一步就是把 Theorem 8 直接翻译成代码。(2025-1396.pdf)
这是第二个核心知识点:Coppersmith 小根。
论文第 2.3 节给出一个关键结论:
若给出了 的近似值,且加法误差不超过 ,那么可以在多项式时间内分解 。(2025-1396.pdf)
这就是题目攻击里“从 到真实 ”的桥梁。
设
其中 。
因为 ,所以
更常见的建模方法是在模 下构造单变量多项式
或等价变体,然后把“真实偏差”当作一个很小的模根,再用 Coppersmith 求出这个小根 。
本质上:
exp.py 里分两步:(exp.py)
pythonif hat_p > 1 and N % hat_p == 0: print("[!] Attack Success!") actual_p = hat_p break
这表示:有时 已经精确到就是 ,那直接除就行。
pythonR.<x> = PolynomialRing(Zmod(N)) f = hat_p - x f = f.monic() roots = f.small_roots(x=2**(int(nbits * 0.6)), beta=0.5, epsilon=0.1)
这段就是 Sage 里的 Coppersmith/Howgrave-Graham 风格小根求解。
它的含义是:
然后代码继续:
pythonfor r in roots: actual_p = int(hat_p + r) if actual_p > 1 and N % actual_p == 0: print("[!] Attack Success!") print(f"Found p: {actual_p}") break
这里变量号写法有点“工程化”,但本质就是把 small root 给出的偏差补回去,得到真正的质因子 。(exp.py)
一旦找到 ,剩下就是纯还原。
exp.py 后半段:(exp.py)
pythonp = actual_p q = N // p phi6 = (p ** 6 - 1) * (q ** 6 - 1) d = pow(e, -1, phi6) m = pow(c, d, N) flag = long_to_bytes(m) print(flag)
每一步都很标准:
q = N // p
phi6 = (p ** 6 - 1) * (q ** 6 - 1)
d = pow(e, -1, phi6)
因为题目定义了
所以重新求逆元即可恢复 。
m = pow(c, d, N)
解密。
long_to_bytes(m)
转回字节串,得到 flag。
最后利用脚本打印出的结果是:(exp.py)
python# nctf{98b1e5c-9a3c-4d8e-9f1a-2b3c4d5e6f7g}
下面这一段最适合你拿来复盘。
普通 RSA:
本题:
pythonphi = (p ** 6 - 1) * (q ** 6 - 1) e = pow(d, -1, phi)
(task.py)
后面所有攻击都在恢复这条方程里的 ,只是改写成论文通用符号 。(exp.py)
论文结论: 若 小于某个界,则可分解 。(2025-1396.pdf)
pythond = getPrime(int(nbits * 2))
这里 只有约 1024 bit,而 很大。(task.py)
因为它落入 generalized Wiener attack 适用区间。
若
则 是 convergent。(2025-1396.pdf)
论文证明正确的 是
的某个 convergent。(2025-1396.pdf)
pythondenominator_approx = N^6 - 162 * N^3 + 1 cf = continued_fraction(e / denominator_approx) convergents = cf.convergents() for conv in convergents: b = conv.numerator() a = conv.denominator()
(exp.py)
论文 Theorem 8 给出
并证明
(2025-1396.pdf)
pythontmp1 = (N**3 - 1)**2 - (a * e / b) tmp2 = (N**3 + 1)**2 - (a * e / b) hat_p = round((0.5 * (sqrt(tmp1) + sqrt(tmp2)))^(1 / 3))
(exp.py)
若知道 的近似值,误差 ,则可多项式时间分解 。(2025-1396.pdf)
pythonR.<x> = PolynomialRing(Zmod(N)) f = hat_p - x f = f.monic() roots = f.small_roots(x=2**(int(nbits * 0.6)), beta=0.5, epsilon=0.1)
(exp.py)
pythonactual_p = int(hat_p + r) if actual_p > 1 and N % actual_p == 0:
(exp.py)
pythonq = N // p phi6 = (p ** 6 - 1) * (q ** 6 - 1) d = pow(e, -1, phi6) m = pow(c, d, N) flag = long_to_bytes(m)
(exp.py)
你问的是“具体是怎样证明怎样利用”,这里我把证明思想抽成模板。
普通 Wiener 攻击里,近似中心是 或 的某种近似。 这题里,论文构造出的近似中心是:
这一步的本质是:
这是 generalized Wiener attack 的标准思路。
只要你能证明某个未知分数 满足
那么你就不用暴力枚举 ,而是直接枚举 的 convergents。
这就是“从指数级猜测”变成“多项式级遍历”的关键。
这题很漂亮的一点是:
这是“数论近似 + 代数求根”结合的经典套路。
这几乎是很多 RSA 变种题的通用终局:
所以你要把 Coppersmith 理解成“最后一公里技术”。
try/exceptpythontry: ... except Exception: continue
因为很多错误候选的 会让:
所以工程上直接跳过错误候选。
pythonif hat_p.bit_length() > nbits or hat_p.bit_length() < nbits: continue
因为本题 是 512-bit。错误 convergent 往往导出离谱的 ,位数先不对。这个剪枝很实用。(exp.py)
pythonif hat_p > 1 and N % hat_p == 0:
这是因为理论上 只是近似值,但有时已经四舍五入刚好命中真实 。先试一下几乎不花代价。
small_roots 的界写成 2**(int(nbits * 0.6))pythonroots = f.small_roots(x=2**(int(nbits * 0.6)), beta=0.5, epsilon=0.1)
这不是论文里精确推导出来的常数,而是利用者根据实际经验给的参数。因为只要误差明显小于 本身,往往就能搜出来。工程实现里常会有这种“理论保证 + 经验参数”的写法。
你可以把它理解为普通 Wiener 攻击的升级版。
针对:
当 很小时, 是 的良好逼近,于是连分数能恢复 。
针对:
因为模数结构更复杂,不能直接逼近 ,而要逼近
恢复出 的候选后,还要进一步通过一个更复杂的公式去近似 ,最后再用 Coppersmith 完成分解。(2025-1396.pdf)
所以这题比普通 Wiener 多了两层:
我给你一个非常实用的学习路径,按顺序来。
先确保这些你完全熟:
练习建议:
pow(e,-1,phi) 的扩展欧几里得版本这是这题的根。
重点掌握:
学习方法:
continued_fraction / convergents这是很多高阶 RSA 题的通解核心。
要理解的不是“会调 Sage 参数”,而是:
学习方法:
当你已经熟 Wiener + Coppersmith 后,再来看这篇论文就会很顺。
你会发现它本质上就是:
这时论文就不再显得神秘。
给你一个真正能提升的练法。
每看一篇论文,都先写:
对这题,表就是:
hat_p 对应 你可以把 exp.py 重构成:
gen_convergents(e, N)recover_hat_p(a, b, e, N)recover_p_from_hat_p(hat_p, N)decrypt_with_pq(p, q, e, c)这样你会更容易看懂“每一段数学到底在干什么”。
不要只会跑 exp。
你可以在 Sage 里自己算:
这会让你真正理解“为什么这个题必然能被打”。
建议顺序:
这样爬最稳。
pow(c, d, N) 不该对 RSA-like 系统成立这题虽然论文背景来自更一般的 RSA-like 系统,但 task.py 实际上还是直接做了:
pythonc = pow(m, e, N)
也就是普通整数模 的幂运算。(task.py)
它只是把“私钥逆元的模”换成了 。这正是题目故意设计的地方:把 RSA 的密钥关系换成更大群阶下的逆元关系,制造小私钥漏洞。
不是。
连分数阶段恢复的是满足
的那个有理比 。在本题里就是 ,不是 。
hat_p 是拍脑袋公式不是。
它来自论文 Theorem 8,背后是:
是严格推出来的。(2025-1396.pdf)
不是。
真正的 Coppersmith 是基于 LLL 的格攻击方法,利用的是“小根在模方程中可恢复”的理论。论文也在 preliminaries 部分明确回顾了 Howgrave-Graham、LLL 和 Coppersmith。(2025-1396.pdf)
如果你要在比赛中快速复盘,这题一句话概括是:
题目把 RSA 的逆元模数改成了 ,但私钥 仍然取得很小,落入 generalized Wiener attack 范围。先通过连分数从 的 convergents 中恢复候选 ,再用论文给出的公式构造 的近似值 ,最后用 Coppersmith 小根恢复真实 ,从而分解 、重建 、求出 并解密。(2025-1396.pdf) (exp.py)
建议你下一步就做这三件事:
exp.py 改写成带注释的函数版。你上传的材料我都已经对应好了:论文在 (2025-1396.pdf),利用脚本在 (exp.py),出题脚本在 (task.py)。
如果你愿意,我下一条可以继续帮你做一份“逐行注释版 exp.py”,把每一行代码旁边都写上对应数学公式和作用。