软件安全 --- CTF加密 之 RSA
RSA 加密解密最小示例
第一步:选择两个小素数 素1,素2
假设
素1 = 3
素2 = 11
素数是大于 1 的自然数,除了 1 和它本身之外,没有其他正因数。
实际应用中素1,素2都是 1024 位以上的大素数,这里仅为演示原理而取极小值。
第二步:计算 模数 和 欧拉函数值
模数 = 素1 × 素2 = 3 × 11 = 33 欧拉函数值 = (素1-1) × (素2-1) = 2 × 10 = 20
公开的"模数"本质就是要被模的一个数字,模就是找余数的动作
欧拉函数值本质就是在 1 到 n 中,与 n 互素(最大公约数为 1)的整数有多少个。必须保密,这是 RSA 安全性的核心
第三步:选择公钥指数 e
e 需要满足:1 < e < φ(n) 且 最大公约数 gcd(e, φ(n)) = 1(互素)
选 e = 3
验证:gcd(3, 20) = 1 ✓ 满足条件
公钥 = (e, n) = (3, 33)
第四步:计算私钥指数 d
d 是 e 关于 φ(n) 的模逆元,满足:
e × d ≡ 1 (mod φ(n))
3 × d ≡ 1 (mod 20) 翻译成人话:我想找一个数 d,让它乘以 3 之后,除以 20 的余数恰好是 1。
3 × d = 20 × (某个整数) + 1
尝试 d = 7:3 × 7 = 21 = 20 + 1,满足条件 ✓
私钥 = (d, n) = (7, 33)
第五步:把字符编码成数字
假设我们用最简单的映射:A=1, B=2, C=3, D=4, ...
要加密的字符是 'D',对应数字 m = 4
⚠️ 这里
m必须小于n=33,所以这个玩具级 RSA 只能加密很小的数字(真实场景中 n 有上千位,可以装下任意长度的消息分组)。
第六步:加密
加密公式:
密文 c = m^e mod n
代入数字:
c = 4^3 mod 33
= 64 mod 33
= 31
密文 c = 31
发送方只需要公开的 (e, n) = (3, 33) 就能完成加密,不需要知道私钥。
第七步:解密
解密公式:
明文 m = c^d mod n
代入数字:
m = 31^7 mod 33
手算过程(用快速幂简化计算):
第一步: 31^1 mod 33 = 31
第二步: 31^2 mod 33 = 961 mod 33 = 4 (因为 33×29=957, 961-957=4)
第三步: 31^4 mod 33 = 4^2 mod 33 = 16
第四步: 31^7 = 31^4 × 31^2 × 31^1
= 16 × 4 × 31 mod 33
= 64 × 31 mod 33
= 31 × 31 mod 33
= 961 mod 33
= 4
解密得到 m = 4
反查编码表:4 → 'D'
成功还原明文字符!
完整流程图
明文字符 'D'
↓ (编码)
m = 4
↓ (用公钥加密: c = m^e mod n = 4^3 mod 33)
c = 31
↓ (网络传输,公开可见)
c = 31
↓ (用私钥解密: m = c^d mod n = 31^7 mod 33)
m = 4
↓ (解码)
明文字符 'D'
核心原理小结
| 要素 | 数值 | 是否公开 |
|---|---|---|
| p, q(两个素数) | 3, 11 | ❌ 保密 |
| n = p×q | 33 | ✅ 公开 |
| φ(n) | 20 | ❌ 保密 |
| e(公钥指数) | 3 | ✅ 公开 |
| d(私钥指数) | 7 | ❌ 保密 |
为什么安全?
攻击者即使知道公钥 (e, n) = (3, 33),想求出私钥 d,必须先算出 φ(n),而这需要先分解 n = p × q。
- 这个玩具例子里
n=33太小,手算就能分解(3×11) - 但当
n是 2048 位(617 位十进制数)的大数时,没有已知的高效算法能在合理时间内分解它,这就是 RSA 的安全基础。
怎么找到 d = 7?(不是乱猜的,有方法)
方法一:暴力枚举(适合小数字,直观理解用)
挨个试 d = 1, 2, 3, ...,看哪个满足条件:
| d | 3×d | 3×d mod 20 | 是否等于1? |
|---|---|---|---|
| 1 | 3 | 3 | ❌ |
| 2 | 6 | 6 | ❌ |
| 3 | 9 | 9 | ❌ |
| 4 | 12 | 12 | ❌ |
| 5 | 15 | 15 | ❌ |
| 6 | 18 | 18 | ❌ |
| 7 | 21 | 1 | ✅ 找到了! |
验证:3 × 7 = 21,21 ÷ 20 = 1余1,所以 21 mod 20 = 1 ✓ 正确!
方法二:扩展欧几里得算法(真实场景中用的方法)
实际 RSA 中 φ(n) 非常大(几百位数字),不可能一个个去试,而是用扩展欧几里得算法直接计算出来。
这个算法的核心思想是:既然 gcd(e, φ(n)) = 1,那么一定存在两个整数 d 和 k,使得:
e × d + φ(n) × k = 1
展开到我们的例子:
3 × d + 20 × k = 1
用扩展欧几里得算法(这里简化演示过程):
20 = 6×3 + 2 → 2 = 20 - 6×3
3 = 1×2 + 1 → 1 = 3 - 1×2
= 3 - 1×(20 - 6×3)
= 3 - 20 + 6×3
= 7×3 - 20
所以:3 × 7 + 20 × (-1) = 1
对照:3 × d + 20 × k = 1
得出:d = 7, k = -1
验证一下:3 × 7 - 20 = 21 - 20 = 1 ✓ 正确!
💻 实际编程中直接调用库函数即可,比如 Python 里是
pow(e, -1, phi_n),或者sympy.mod_inverse(e, phi_n),电脑瞬间就能算出来,不需要人工推导。
扩展欧几里得算法原理详解
第一部分:普通欧几里得算法(先搞懂怎么求 gcd)
核心思想:辗转相除
欧几里得算法的原理很简单:两个数的最大公约数,等于较小数和两数相除余数的最大公约数。
用数学语言表达:
gcd(a, b) = gcd(b, a mod b)
不断重复这个过程,直到余数为 0,最后一个非零余数就是 gcd。
实际计算过程(求 gcd(20, 3))
第1步:20 = 6×3 + 2 (20 除以 3,商6余2)
第2步:3 = 1×2 + 1 (3 除以 2,商1余1)
第3步:2 = 2×1 + 0 (2 除以 1,商2余0,余数为0,停止)
最后一个非零余数是 1,所以 gcd(20, 3) = 1(验证了它们互素)。
第二部分:为什么要"扩展"?
普通欧几里得算法只能告诉我们 gcd 是多少,但 RSA 需要的是找到满足:
3 × d + 20 × k = 1
的具体数字 d 和 k。这个式子叫做 裴蜀等式(Bézout's Identity),数学上已经证明:只要 gcd(a,b)=1,就一定存在整数 d 和 k 使得 a×d + b×k = 1。
扩展欧几里得算法就是用来反向推导出这两个具体的系数 d 和 k。
第三部分:反向代入的原理(核心部分)
思路:把每一步的余数都表示成"原始两个数的组合"
正向计算(辗转相除):
20 = 6×3 + 2 → 变形:2 = 20 - 6×3 ...(式A)
3 = 1×2 + 1 → 变形:1 = 3 - 1×2 ...(式B)
反向代入(从后往前,把余数替换掉):
目标:把式B中的"2",替换成用 20 和 3 表示的形式(也就是用式A替换)
式B: 1 = 3 - 1×2
把式A 2 = 20 - 6×3 代入进去,替换掉式B里的 "2":
1 = 3 - 1×(20 - 6×3)
展开括号:
1 = 3 - 1×20 + 1×6×3
= 3 - 20 + 6×3
合并同类项(把两个 "3" 的项加在一起):
1 = (1+6)×3 - 20
1 = 7×3 - 20
对照目标式:
3 × d + 20 × k = 1
7 × 3 + (-1) × 20 = 1
结论:d = 7,k = -1
第四部分:用一个更直观的"搬箱子"比喻理解
想象你在玩一个数字搬运游戏:
-
正向过程(辗转相除):你不断用大数减小数的倍数,把大问题拆解成小问题,就像不断把大箱子拆分成小箱子,直到剩下的箱子里东西最少(余数最小,也就是gcd)。
-
反向过程(回代):你再把这些拆解过程倒着走一遍,把每一步用过的"小箱子"重新换算回最初的两个"大箱子"(也就是原始的 3 和 20),每次替换都在给 3 和 20 前面积累系数。
走完这个来回的过程,你就能精确知道:要搭配多少个 3,多少个 20(正的或负的),才能刚好凑出 1 这个数字。
第五部分:验证结果
把 d=7, k=-1 代入原式验证:
3 × 7 + 20 × (-1)
= 21 - 20
= 1 ✓ 正确!
第六部分:为什么只要 d,不要 k?
回到 RSA 的场景,我们真正需要的只是:
3 × d ≡ 1 (mod 20)
而完整的裴蜀等式是:
3 × d + 20 × k = 1
移项:
3 × d = 1 - 20 × k = 1 + 20 × (-k)
两边同时 mod 20:
3 × d mod 20 = 1 (因为 20×(-k) 这一项正好是 20 的倍数,mod 20 后变成 0)
所以 k 只是计算过程中的一个"副产品",RSA 真正要用的只有 d = 7。
总结:扩展欧几里得算法的本质
| 普通欧几里得算法 | 扩展欧几里得算法 |
|---|---|
| 只求出 gcd(a,b) 的数值 | 还能求出满足 a×d+b×k=gcd(a,b) 的具体系数 d,k |
| 正向计算(不断取余) | 正向计算后,再反向回代 |
| 目的:判断两数是否互素 | 目的:求模逆元(RSA私钥计算的核心工具) |
一句话总结:扩展欧几里得算法 = 普通欧几里得算法(正向拆解)+ 反向代入还原系数,这个系数 d 恰好就是我们在 RSA 中要找的私钥指数。
裴蜀定理(Bézout's Identity)通俗解释
这个结论有一个正式的数学名字,叫 裴蜀定理(也叫贝祖定理),是数论中一个非常基础但极其重要的定理。
定理的完整表述
裴蜀定理:对于任意两个整数
a和b,一定存在整数x和y,使得:a × x + b × y = gcd(a, b)
特殊情况:当 a 和 b 互素时(即 gcd(a,b)=1),这个式子就变成:
a × x + b × y = 1
这正是我们之前用到的那个等式(把 a 换成 e,b 换成 φ(n),x 换成 d,y 换成 k):
e × d + φ(n) × k = 1
为什么这个定理是对的?直观理解
用"能凑出哪些数字"的角度理解
想象你手里有两种"货币":面值为 a 的硬币和面值为 b 的硬币(硬币可以使用负数个,也就是可以"欠款")。
问题是:用这两种硬币(正着用或者倒着欠),你能凑出哪些数字?
答案:你能凑出的所有数字,恰好就是 gcd(a,b) 的所有倍数,一个不多一个不少。
举例验证
a = 3, b = 20
用 3 和 20 的组合(可以是负数倍),能凑出哪些数?
3×1 + 20×0 = 3
3×0 + 20×1 = 20
3×7 + 20×(-1) = 21-20 = 1
3×(-3) + 20×1 = -9+20 = 11
...
因为 gcd(3,20)=1,所以理论上能凑出任何整数(1的倍数就是所有整数)。特别是能凑出 1 这个最小正数,这正是裴蜀定理保证的。
反例:如果不互素会怎样?
a = 4, b = 20, gcd(4,20) = 4
用 4 和 20 的组合:
4×1 + 20×0 = 4
4×(-5) + 20×1 = -20+20 = 0
4×3 + 20×(-0.5) ❌ 不行,y必须是整数
无论怎么组合整数倍的 4 和 20,你永远凑不出 1,只能凑出 4 的倍数(4, 8, 12, -4, 0, ...)。
这就是为什么 RSA 要求 gcd(e, φ(n))=1 ——只有互素,才能保证能凑出 "1",也就是能找到满足 e×d ≡ 1 (mod φ(n)) 的那个 d。
另一个直观比喻:钟表齿轮
想象两个啮合的齿轮,齿数分别是 a 和 b:
-
如果
gcd(a,b)=1(比如齿数为3和20),两个齿轮转动时,会经历"所有可能的相对位置组合"后才会重新对齐回起点。这意味着信息不会丢失,每个状态都唯一对应。 -
如果
gcd(a,b)=d>1(比如齿数为4和20,gcd=4),齿轮转动会提前重复、进入循环,只能覆盖 1/d 的状态,会丢失信息。
RSA 需要的正是前一种情况——齿数互素,保证了"一一对应、不丢信息",这也是为什么解密能够精确还原加密前内容的根本原因。
这个定理和"扩展欧几里得算法"的关系
裴蜀定理只是从理论上证明了这样的 d 和 k 一定存在,但它没有告诉你具体数值是多少。
扩展欧几里得算法,就是用来具体计算出这组 d 和 k 的算法——也就是我们之前手算的那个"辗转相除再反向代入"的过程。
两者的关系可以这样总结:
| 作用 | |
|---|---|
| 裴蜀定理 | 从理论上证明满足 e×d + φ(n)×k = 1 的整数 d、k 一定存在(只要互素) |
| 扩展欧几里得算法 | 提供一个具体可执行的算法,一步步计算出这组 d 和 k 的精确数值 |
一句话总结
裴蜀定理说的是:只要两个数互素,就一定能找到合适的整数系数,把它们组合"凑"出数字 1 —— 这正是 RSA 中私钥 d 必然存在的数学根基;而扩展欧几里得算法则是把这个"理论上存在"变成"实际上能算出来"的具体工具。
1. Crypto-babe-Lifting(Crypto)
入口/附件:附件/output.txt(pk、cipher、hint)与 附件/task.py;纯附件题。
1.1 分析
n 为 1024 bit,e = 0x1001,泄漏了解密指数 d 的低 400 位。由 e*d ≡ 1 + k*φ(N) (mod 2^400) 且 1 ≤ k ≤ e,可以枚举 k 得到 φ(N) mod 2^400。再由 φ(N) = N - (p+q) + 1 得到 (p+q) mod 2^400,解二次方程即可得到 p mod 2^400。已知 p 的低 400 位后,用 Coppersmith 的 small_roots 分解 N。最后求 d 并解密 cipher。
1.2 解题步骤
- 读取参数并确认位数:
python3 - <<'PY'
import re
txt = open('附件/output.txt').read()
n, e = map(int, re.search(r'pk = \((\d+), (\d+)\)', txt).groups())
c = int(re.search(r'cipher = (\d+)', txt).group(1))
d0 = int(re.search(r'hint = (\d+)', txt).group(1))
print('n bits =', n.bit_length(), 'e =', e, 'd0 bits =', d0.bit_length())
PY
- 在
Z/2^400Z上枚举k,解出p的低位候选:
X = var('X')
for k in range(1, int(e) + 1):
rows = solve_mod([k*X^2 + (e*d0 - (1 + k*(n+1)))*X + k*n == 0], 1 << 400)
for row in rows:
p0 = ZZ(row[0])
- 对每个
p0用 Coppersmith 求p:
PR.<x> = PolynomialRing(Zmod(n))
f = (2^398*x + p0).monic()
roots = f.small_roots(X=2^(512-400+2), beta=0.49)
if roots:
p = gcd(2^398*int(roots[0]) + p0, n)
- 分解
N后求私钥并解密:
q = n // p
d = inverse_mod(e, (p-1)*(q-1))
m = pow(c, d, n)
print(int(m).to_bytes((int(m).bit_length() + 7) // 8, 'big').decode())
- 直接运行完整 Sage 脚本:
sage solve/solve.sage
1.3 完整脚本
import re
import os
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
txt = open(os.path.join(BASE, "附件", "output.txt")).read()
pk = re.search(r"pk = \((\d+), (\d+)\)", txt)
n = Integer(pk.group(1))
e = Integer(pk.group(2))
c = Integer(re.search(r"cipher = (\d+)", txt).group(1))
d0 = Integer(re.search(r"hint = (\d+)", txt).group(1))
print(f"[*] n bits = {n.nbits()}, e = {e}, d0 bits = {d0.nbits()}")
def recover_p(p0, n):
"""已知 p 的低 400 位,用 Coppersmith 求 p。"""
PR = PolynomialRing(Zmod(n), 'x')
x = PR.gen()
nbits, p0bits = 1024, 400
f = 2 ** (p0bits - 2) * x + p0
f = f.monic()
roots = f.small_roots(X=2 ** (nbits // 2 - p0bits + 2), beta=0.49)
if roots:
x0 = roots[0]
return ZZ(gcd(2 ** (p0bits - 2) * int(x0) + p0, n))
return None
def find_p(d0, e, n):
"""遍历 k,解出 p 的低位候选并尝试 Coppersmith 分解。"""
X = var('X')
for k in range(1, int(e) + 1):
results = solve_mod([k * X**2 + (e * d0 - (1 + k * (n + 1))) * X + k * n == 0], 1 << 400)
for x in results:
p = recover_p(ZZ(x[0]), n)
if p and p != 1:
return p
return None
p = find_p(d0, e, n)
if p is None:
print("[-] 未能分解 N")
raise SystemExit(1)
q = n // p
d = inverse_mod(e, (p - 1) * (q - 1))
m = pow(c, d, n)
pt = int(m).to_bytes((int(m).bit_length() + 7) // 8, 'big')
match = re.search(rb"moectf\{[^}]+\}", pt)
print(f"[+] 成功获取 Flag: {match.group(0).decode() if match else pt}")
Flag:moectf{7h3_st4rt_0f_c0pp3rsmith!}(附件题静态 Flag
浙公网安备 33010602011771号