RSA密码学题目完全解析:从部分私钥片段到完整密钥恢复

admin 2026-02-08 00:48:34 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细解析了一道RSA密码学CTF题目,通过分析泄露的部分私钥片段(包含CRT参数dp、dq、qinv),利用数学关系dp×e-1=k×(p-1)成功恢复出质数p和q,进而计算出完整私钥d并解密获取flag。文章涵盖了ASN.1DER编码解析、扩展欧几里得算法、素性测试等关键技术,并提供了完整的Python解题脚本,展示了从部分密钥泄露到完整密钥恢复的完整攻击链,强调了私钥保护的重要性。 综合评分: 92 文章分类: CTF,密码学,漏洞分析,安全开发,渗透测试


第一步:观察与分析题目

1.1 查看加密文件

首先,让我们看看加密文件的基本信息:

$ file flag.enc
flag.enc: data

$ ls -lh flag.enc
-rw-r--r-- 1 user user 128 Mar 11 2016 flag.enc

这是一个128字节的二进制文件,大小正好是1024位(128字节 × 8位/字节 = 1024位),这暗示使用的是1024位RSA密钥。

1.2 查看mask.png图片

打开mask.png,我们看到一个终端界面,显示了cat key.pem命令的输出:

-----BEGIN RSA PRIVATE KEY-----
[前面部分被遮挡/截断]
Os9mhOQRdqW2cwVrnNI72DLcAXpXUJ1HGwJBANWiJcDUGxZpnERxVw7s0913WXNt
V4GqdxCzG0pG5EHThtoTRbyX0aqRP4U/hQ9tRoSoDmBn+3HPITsnbCy67VkCQBM4
xZPTtUKM6Xi+16VTUnFVs9E4rqwIQCDAxn9UuVMBXlX2Cl0xOGUF4C5hItrX2woF
7LVS5EizR63CyRcPovMCQQDVyNbcWD7N88MhZjujKuSrHJot7WcCaRmTGEIJ6TkU
8NWt9BVjR4jVkZ2EqNd0KZWdQPukeynPcLlDEkIXyaQx
-----END RSA PRIVATE KEY-----

关键观察:

  • 这是PEM格式的RSA私钥
  • 私钥的前面部分被遮挡,但后面部分可见
  • 可见部分约300个字符的Base64编码数据

1.3 初步思路

RSA私钥包含多个参数,如果我们能从可见的部分提取出关键参数,或许可以利用RSA参数之间的数学关系来恢复完整的私钥。

第二步:理解RSA私钥结构

在尝试提取参数之前,我们需要理解RSA私钥的存储格式。

2.1 RSA基础知识

RSA加密算法涉及以下关键参数:

公钥参数:

  • n:模数,n = p × q
  • e:公钥指数,通常是65537

私钥参数:

  • d:私钥指数,满足 d × e ≡ 1 (mod φ(n))
  • p:第一个质数
  • q:第二个质数

CRT优化参数(中国剩余定理):

  • dp:d mod (p-1),也称为exponent1
  • dq:d mod (q-1),也称为exponent2
  • qinv:q⁻¹ mod p,也称为coefficient

2.2 PKCS#1 私钥格式

根据PKCS#1标准,RSA私钥的ASN.1结构定义如下:

RSAPrivateKey ::= SEQUENCE {
  version           INTEGER,  -- 版本号,通常是0
  modulus           INTEGER,  -- n
  publicExponent    INTEGER,  -- e
  privateExponent   INTEGER,  -- d
  prime1            INTEGER,  -- p
  prime2            INTEGER,  -- q
  exponent1         INTEGER,  -- dp = d mod (p-1)
  exponent2         INTEGER,  -- dq = d mod (q-1)
  coefficient       INTEGER,  -- qinv = q^-1 mod p
}

这些参数按照上述顺序存储在私钥中。注意:后三个参数(exponent1、exponent2、coefficient)是为了加速RSA运算而存储的冗余信息。

2.3 ASN.1 DER编码

ASN.1使用DER(Distinguished Encoding Rules)编码规则将数据结构编码为二进制:

TLV结构:

  • T (Type):标识数据类型,1个字节

  • 0x02 = INTEGER(整数)

  • 0x30 = SEQUENCE(序列)

  • L (Length):数据长度

  • 如果长度 < 128:直接用1个字节表示

  • 如果长度 ≥ 128:第一字节最高位为1,剩余7位表示后续长度字节数

  • V (Value):实际数据

例如:02 41 00 D5 A2 25 ... 表示:

  • 02:INTEGER类型
  • 41:长度为65字节(十进制)
  • 00 D5 A2 25 ...:65字节的整数值

第三步:提取私钥参数

3.1 Base64解码

首先将可见的Base64字符串解码为二进制数据:

import&nbsp;base64

base64_data =&nbsp;"Os9mhOQRdqW2cwVrnNI72DLcAXpXUJ1HGwJBANWiJcDUGxZpnERxVw7s0913WXNtV4GqdxCzG0pG5EHThtoTRbyX0aqRP4U/hQ9tRoSoDmBn+3HPITsnbCy67VkCQBM4xZPTtUKM6Xi+16VTUnFVs9E4rqwIQCDAxn9UuVMBXlX2Cl0xOGUF4C5hItrX2woF7LVS5EizR63CyRcPovMCQQDVyNbcWD7N88MhZjujKuSrHJot7WcCaRmTGEIJ6TkU8NWt9BVjR4jVkZ2EqNd0KZWdQPukeynPcLlDEkIXyaQx"

binary_data = base64.b64decode(base64_data)
print(f"解码后字节数:&nbsp;{len(binary_data)}") &nbsp;# 输出: 225

解码后得到225字节的二进制数据。

3.2 解析DER结构

查看二进制数据的十六进制表示:

3acf6684e41176a5b673056b9cd23bd832dc017a57509d471b024100d5a225c0
d41b16699c4471570eecd3dd7759736d5781aa7710b31b4a46e441d386da1345
bc97d1aa913f853f850f6d4684a80e6067fb71cf213b276c2cbaed5902401338
c593d3b5428ce978bed7a553527155b3d138aeac084020c0c67f54b953015e55
f60a5d31386505e02e6122dad7db0a05ecb552e448b347adc2c9170fa2f30241
00d5c8d6dc583ecdf3c321663ba32ae4ab1c9a2ded6702691993184209e93914
f0d5adf415634788d5919d84a8d77429959d40fba47b29cf70b943124217c9a4
31

现在我们需要找到所有的INTEGER类型标记(0x02)并提取其值:

def&nbsp;parse_der_integers(data):
&nbsp; &nbsp;&nbsp;"""解析DER编码中的所有INTEGER"""
&nbsp; &nbsp; integers = []
&nbsp; &nbsp; i =&nbsp;0

&nbsp; &nbsp;&nbsp;while&nbsp;i < len(data):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;data[i] ==&nbsp;0x02: &nbsp;# INTEGER类型
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; length = data[i +&nbsp;1]

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;length <=&nbsp;127: &nbsp;# 短格式长度
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; value_bytes = data[i +&nbsp;2&nbsp;: i +&nbsp;2&nbsp;+ length]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; value = int.from_bytes(value_bytes, byteorder='big')
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; integers.append({
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'offset': i,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'length': length,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'value': value
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; })
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; i +=&nbsp;2&nbsp;+ length
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; i +=&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; i +=&nbsp;1

&nbsp; &nbsp;&nbsp;return&nbsp;integers

integers = parse_der_integers(binary_data)
print(f"找到&nbsp;{len(integers)}&nbsp;个INTEGER")

3.3 提取的参数

运行解析代码后,我们找到了3个大整数:

第一个INTEGER: 偏移25,长度65字节

dp = 11188888442779478492506783674852186314949555636014740182307607993518479864690065244102864238986781155531033697982611187514703037389481147794554444962262361

第二个INTEGER: 偏移92,长度64字节

dq = 1006725509429627901220283238134032802363853505667837273574181077068133214344166038422298631614477333564791953596600001816371928482096290600710984197710579

第三个INTEGER: 偏移158,长度65字节

qinv = 11196804284042107547423407831525890933636414684075355664222816007929037065463409676450144484947842399975707117057331864113464711778199061912128258484839473

3.4 参数识别

根据RSA私钥的结构顺序,由于前面部分被截断,我们看到的应该是私钥末尾的三个参数。这三个参数很可能是:

  • exponent1 (dp) = d mod (p-1)
  • exponent2 (dq) = d mod (q-1)
  • coefficient (qinv) = q⁻¹ mod p

这是个好消息!虽然我们没有直接得到p、q、d,但这三个CRT参数之间存在数学关系,理论上可以恢复出完整的私钥。

第四步:数学原理分析

现在的关键问题是:如何从dp、dq、qinv恢复出质数p和q?

4.1 寻找数学突破口

让我们回顾一下这些参数的定义:

  1. dp = d mod (p-1)
  2. dq = d mod (q-1)
  3. qinv = q⁻¹ mod p

同时,RSA的基本性质告诉我们:

  • d × e ≡ 1 (mod φ(n))
  • φ(n) = (p-1) × (q-1)

4.2 推导关键等式

由于 d × e ≡ 1 (mod φ(n)),而 (p-1) 是 φ(n) 的因子,我们有:

d × e ≡ 1 (mod (p-1))

因为 dp = d mod (p-1),所以:

dp × e ≡ d × e ≡ 1 (mod (p-1))

这意味着:

dp × e = 1 + k × (p-1) &nbsp; &nbsp;(对于某个整数k)

移项得到:

dp × e - 1 = k × (p-1)

关键洞察:(p-1) 必定是 (dp × e - 1) 的因子!

4.3 恢复策略

基于上述推导,我们可以设计如下算法:

  1. 计算 d1p = dp × e - 1
  2. 遍历可能的k值(从3到e)
  3. 对每个k,计算 p = (d1p / k) + 1
  4. 检验p是否为质数
  5. 如果p是质数,用相同方法恢复q
  6. 验证 qinv × q ≡ 1 (mod p) 是否成立

4.4 为什么这个方法有效?

理论基础:

  • 质数p的分布相对稀疏,根据质数定理,1024位RSA中的质数p约为512位
  • 虽然k的取值范围很大(最大65537),但满足条件的k值非常少
  • 一旦找到质数p,可以通过qinv关系验证其正确性

计算复杂度:

  • 遍历k值:O(e) ≈ O(65537)
  • 素性测试:O(log³ p)(Miller-Rabin算法)
  • 总体上是可行的

第五步:实现参数恢复算法

5.1 完整实现

from&nbsp;Crypto.Util.number&nbsp;import&nbsp;isPrime

def&nbsp;recover_primes_from_crt(dp, dq, qinv, e=65537):
&nbsp; &nbsp;&nbsp;"""
&nbsp; &nbsp; 从CRT参数恢复RSA质数p和q

&nbsp; &nbsp; 参数:
&nbsp; &nbsp; &nbsp; &nbsp; dp: d mod (p-1)
&nbsp; &nbsp; &nbsp; &nbsp; dq: d mod (q-1)
&nbsp; &nbsp; &nbsp; &nbsp; qinv: q^-1 mod p
&nbsp; &nbsp; &nbsp; &nbsp; e: 公钥指数,默认65537

&nbsp; &nbsp; 返回:
&nbsp; &nbsp; &nbsp; &nbsp; (p, q) 或 (None, None)
&nbsp; &nbsp; """
&nbsp; &nbsp; print("开始恢复质数p...")
&nbsp; &nbsp; d1p = dp * e -&nbsp;1

&nbsp; &nbsp;&nbsp;for&nbsp;k&nbsp;in&nbsp;range(3, e):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 检查k是否是d1p的因子
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;d1p % k ==&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; hp = d1p // k
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; p = hp +&nbsp;1

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 验证p是否为质数
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;isPrime(p):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"找到候选质数p (k={k})")

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 同样方法恢复q
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; d1q = dq * e -&nbsp;1

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;m&nbsp;in&nbsp;range(3, e):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;d1q % m ==&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; hq = d1q // m
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; q = hq +&nbsp;1

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;isPrime(q):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 验证qinv关系
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(qinv * q) % p ==&nbsp;1:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"验证成功! 找到正确的p和q")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;p, q
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;elif&nbsp;(qinv * p) % q ==&nbsp;1:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"验证成功! 找到正确的p和q")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;p, q

&nbsp; &nbsp;&nbsp;return&nbsp;None,&nbsp;None

5.2 执行恢复

# 使用提取的参数
dp =&nbsp;11188888442779478492506783674852186314949555636014740182307607993518479864690065244102864238986781155531033697982611187514703037389481147794554444962262361
dq =&nbsp;1006725509429627901220283238134032802363853505667837273574181077068133214344166038422298631614477333564791953596600001816371928482096290600710984197710579
qinv =&nbsp;11196804284042107547423407831525890933636414684075355664222816007929037065463409676450144484947842399975707117057331864113464711778199061912128258484839473
e =&nbsp;65537

# 恢复质数
p, q = recover_primes_from_crt(dp, dq, qinv, e)

5.3 恢复结果

程序成功找到:

k = 56917 时:
p = 12883429939639100479003058518523248493821688207697138417834631218638027564562306620214863988447681300666538212918572472128732943784711527013224777474072569

m = 5277 时:
q = 12502893634923161599824465146407069882228513776947707295476805997311776855879024002289593598657949783937041929668443115224477369136089557911464046118127387

验证: (qinv × q) mod p = 1 ✓

验证通过!我们成功恢复了RSA的两个质数。

5.4 结果验证

让我们验证恢复的参数是否正确:

# 验证1: 检查p和q是否为质数
print(f"p是质数:&nbsp;{isPrime(p)}") &nbsp;# True
print(f"q是质数:&nbsp;{isPrime(q)}") &nbsp;# True

# 验证2: 检查qinv关系
print(f"qinv验证:&nbsp;{(qinv * q) % p ==&nbsp;1}") &nbsp;# True

# 验证3: 重新计算dp和dq(需要先计算d)
n = p * q
phi = (p -&nbsp;1) * (q -&nbsp;1)

所有验证都通过,说明我们的恢复是正确的。

第六步:计算完整私钥

现在我们有了p、q和e,可以计算完整的RSA私钥。

6.1 计算私钥指数d

RSA私钥d满足:d × e ≡ 1 (mod φ(n))

这意味着d是e在模φ(n)下的乘法逆元。我们使用扩展欧几里得算法(Extended Euclidean Algorithm)来计算。

扩展欧几里得算法原理:

对于整数a和b,EGCD算法不仅能计算gcd(a,b),还能找到系数u和v使得:

a × u + b × v = gcd(a, b)

当 gcd(e, φ(n)) = 1 时(这是RSA的必要条件),我们有:

e × u + φ(n) × v = 1

两边对φ(n)取模:

e × u ≡ 1 (mod φ(n))

因此u就是我们要找的d。

6.2 实现EGCD算法

def&nbsp;extended_gcd(a, b):
&nbsp; &nbsp;&nbsp;"""
&nbsp; &nbsp; 扩展欧几里得算法
&nbsp; &nbsp; 返回u,使得 a*u ≡ 1 (mod b)
&nbsp; &nbsp; """
&nbsp; &nbsp; u, u1 =&nbsp;1,&nbsp;0
&nbsp; &nbsp; v, v1 =&nbsp;0,&nbsp;1

&nbsp; &nbsp;&nbsp;while&nbsp;b:
&nbsp; &nbsp; &nbsp; &nbsp; q = a // b
&nbsp; &nbsp; &nbsp; &nbsp; u, u1 = u1, u - q * u1
&nbsp; &nbsp; &nbsp; &nbsp; v, v1 = v1, v - q * v1
&nbsp; &nbsp; &nbsp; &nbsp; a, b = b, a - q * b

&nbsp; &nbsp;&nbsp;return&nbsp;u

# 计算n和φ(n)
n = p * q
phi = (p -&nbsp;1) * (q -&nbsp;1)

print(f"n =&nbsp;{n}")
print(f"φ(n) =&nbsp;{phi}")

# 计算d
d = extended_gcd(e, phi)
if&nbsp;d <&nbsp;0:
&nbsp; &nbsp; d += phi

print(f"d =&nbsp;{d}")

6.3 计算结果

n = 161080154188292201430717335450301702574211890587423028785946588452513709903864566907797711402814280216429284407010865117658741411399738837015270166197792615276511302372234182990420185803542388458087342116253675425489502589540709488892694405415013333511961708962693793627275736479090319881934245022826824347203

φ(n) = 161080154188292201430717335450301702574211890587423028785946588452513709903864566907797711402814280216429284407010865117658741411399738837015270166197792589890187727809972104162896520873224012407885357471407962114052286639736289047562071900957426227880877105382551206611688383268777399080849320334003232147248

d = 12441639692099655517376308833932392670257420848582256919212988552216677594845086557017745931627109670194928630671056032860651983223301005431608062335676428430110171020554477490159485308455680772826276447201841772149055876380443034602731403064627486237285806604612267999273183028007861118868108999965277036321

6.4 验证私钥

验证私钥d是否正确:

# 验证: d × e ≡ 1 (mod φ(n))
verify = (d * e) % phi
print(f"验证结果:&nbsp;{verify}") &nbsp;# 应该等于1

输出:1 ✓

完美!我们现在拥有了完整的RSA私钥参数:p、q、n、e、d。

第七步:RSA解密

7.1 读取加密文件

from&nbsp;Crypto.Util.number&nbsp;import&nbsp;bytes_to_long, long_to_bytes

# 读取密文
with&nbsp;open("flag.enc",&nbsp;"rb")&nbsp;as&nbsp;f:
&nbsp; &nbsp; ciphertext_bytes = f.read()

print(f"密文长度:&nbsp;{len(ciphertext_bytes)}&nbsp;字节")
print(f"密文(hex):&nbsp;{ciphertext_bytes.hex()}")

# 转换为整数
ciphertext = bytes_to_long(ciphertext_bytes)
print(f"密文(int):&nbsp;{ciphertext}")

输出:

密文长度: 128 字节
密文(hex): 20aede3c132c55736c4b564de173f9b6d1f12a9878f567baf3563a711ced56f3...
密文(int): 22950838243355051507735476344130216859576434035671340145571506267533...

7.2 执行RSA解密

RSA解密公式:

明文 = 密文^d mod n
# RSA解密
plaintext = pow(ciphertext, d, n)
plaintext_bytes = long_to_bytes(plaintext)

print(f"明文(hex):&nbsp;{plaintext_bytes.hex()}")
print(f"明文(bytes):&nbsp;{plaintext_bytes}")

输出:

明文(hex): 02f6d04bd49ea1f49066c07ab9a4645e35049e2adaebecf5a6685c88d1198ddf13c89f9e33ea7d9076...
明文(bytes): b'\x02\xf6\xd0K\xd4\x9e\xa1\xf4\x90f\xc0z\xb9\xa4d^5\x04\x9e*...\x000ctf{Keep_ca1m_and_s01ve_the_RSA_Eeeequati0n!!!}\n'

7.3 PKCS#1 v1.5 填充分析

仔细观察解密结果,我们发现:

  • 开头是 \x02
  • 中间有很多随机字节
  • 后面有 \x00
  • 最后是可读的文本

这是PKCS#1 v1.5填充格式!

PKCS#1 v1.5 填充格式:

0x00 || 0x02 || PS || 0x00 || M

其中:

  • 第1字节:0x00(通常在转换为整数时会丢失)
  • 第2字节:0x02(表示加密操作,而不是签名)
  • PS:填充字符串,由至少8个随机非零字节组成
  • 0x00:分隔符
  • M:实际的消息

7.4 提取实际消息

# 寻找分隔符0x00(从第3个字节开始)
separator_index = plaintext_bytes.find(b'\x00',&nbsp;2)

if&nbsp;separator_index !=&nbsp;-1:
&nbsp; &nbsp;&nbsp;# 提取分隔符之后的内容
&nbsp; &nbsp; actual_message = plaintext_bytes[separator_index +&nbsp;1:]
&nbsp; &nbsp; flag = actual_message.decode('utf-8').strip()

&nbsp; &nbsp; print(f"找到分隔符位置:&nbsp;{separator_index}")
&nbsp; &nbsp; print(f"实际消息:&nbsp;{actual_message}")
&nbsp; &nbsp; print(f"Flag:&nbsp;{flag}")
else:
&nbsp; &nbsp; print("未找到PKCS#1填充分隔符")

输出:

找到分隔符位置: 77
实际消息: b'0ctf{Keep_ca1m_and_s01ve_the_RSA_Eeeequati0n!!!}\n'
Flag: 0ctf{Keep_ca1m_and_s01ve_the_RSA_Eeeequati0n!!!}

成功获取flag!

第八步:技术总结与深度分析

8.1 完整攻击链

让我们回顾整个攻击过程:

部分私钥泄露 (Base64片段)
&nbsp; &nbsp; ↓
ASN.1 DER解析
&nbsp; &nbsp; ↓
提取CRT参数 (dp, dq, qinv)
&nbsp; &nbsp; ↓
数学关系推导 (dp×e-1 = k×(p-1))
&nbsp; &nbsp; ↓
恢复质数 (p, q)
&nbsp; &nbsp; ↓
计算私钥指数 (d)
&nbsp; &nbsp; ↓
RSA解密
&nbsp; &nbsp; ↓
去除PKCS#1填充
&nbsp; &nbsp; ↓
获取flag

8.2 关键技术点深度解析

1. ASN.1 DER编码

ASN.1是密码学中广泛使用的数据描述语言。DER编码保证了相同数据结构只有唯一的编码结果,这对于数字签名等应用至关重要。

理解DER编码的重要性:

  • 能够手工解析密码学文件格式
  • 理解密钥的实际存储结构
  • 为密码分析提供基础

2. CRT参数的安全隐患

RSA实现中使用CRT参数来加速解密运算。解密速度可以提升约4倍:

标准解密: m = c^d mod n
CRT解密:
&nbsp; &nbsp; m1 = c^dp mod p
&nbsp; &nbsp; m2 = c^dq mod q
&nbsp; &nbsp; h = qinv × (m1 - m2) mod p
&nbsp; &nbsp; m = m2 + h × q

然而,这些冗余参数带来了安全风险:

  • 任何一个CRT参数的泄露都可能导致完整密钥恢复
  • 本题展示了即使只有dp、dq、qinv,也能恢复p和q

3. 数学关系的威力

本题的核心在于利用了以下数学关系:

dp × e - 1 = k × (p-1)

这个关系来自于:

  • RSA的基本性质:d × e ≡ 1 (mod φ(n))
  • CRT参数的定义:dp = d mod (p-1)
  • 模运算的性质:如果 a ≡ b (mod m),则 a×c ≡ b×c (mod m)

这种数学关系在密码分析中经常出现,理解这些关系是密码分析的基础。

4. 素性测试

在恢复算法中,我们需要快速判断一个大数是否为质数。常用算法:

  • Miller-Rabin算法:概率性算法,速度快
  • AKS算法:确定性算法,但速度较慢

对于密码学应用,Miller-Rabin算法已经足够,误判率可以降到2^-100以下。

5. 扩展欧几里得算法

EGCD是数论中的基础算法,应用广泛:

  • 计算模逆元(本题使用)
  • 求解线性丢番图方程
  • 实现中国剩余定理

算法时间复杂度为O(log n),非常高效。

8.3 攻击的前提条件

本题攻击成功依赖于以下条件:

  1. 部分私钥泄露:泄露了CRT参数
  2. 使用标准参数:公钥指数e = 65537
  3. 参数完整性:泄露的参数是完整的,没有损坏
  4. 数学关系有效:RSA的数学结构允许从部分参数恢复全部

8.4 防御措施

从这道题我们可以学到以下安全教训:

1. 私钥保护

  • 私钥的任何部分都不应该泄露
  • 即使是”辅助参数”也可能导致完整密钥恢复
  • 使用硬件安全模块(HSM)保护私钥

2. 密钥存储

  • 私钥应该加密存储
  • 使用强密码和密钥派生函数
  • 定期轮换密钥

3. 访问控制

  • 限制对私钥文件的访问
  • 使用文件权限和SELinux等机制
  • 审计私钥访问日志

4. 侧信道防护

  • 防止时序攻击
  • 防止功耗分析攻击
  • 使用常量时间实现

5. 参数选择

  • 使用足够长的密钥(推荐2048位或以上)
  • 考虑使用更现代的密码算法(如ECC)
  • 遵循密码学最佳实践

第九步:实战技巧与经验总结

9.1 CTF密码题解题思路

1. 信息收集

  • 查看所有给定文件
  • 分析文件类型和大小
  • 寻找可能的提示信息

2. 识别密码体制

  • 根据文件格式判断加密算法
  • RSA、AES、ECC等各有特征
  • 密文长度常常是关键线索

3. 寻找弱点

  • 参数泄露
  • 算法实现错误
  • 密钥生成缺陷
  • 填充oracle

4. 数学分析

  • 参数之间的数学关系
  • 是否存在小指数攻击
  • 是否存在共模攻击
  • 是否可以分解模数

5. 工具使用

  • SageMath:强大的数学计算工具
  • RsaCtfTool:自动化RSA攻击工具
  • openssl:密码学工具集
  • Python + Crypto库:脚本开发

9.2 Python密码学实践

本题涉及的Python密码学技术:

1. 数值转换

from&nbsp;Crypto.Util.number&nbsp;import&nbsp;bytes_to_long, long_to_bytes

# 字节转整数
num = bytes_to_long(b"Hello")

# 整数转字节
data = long_to_bytes(num)

2. 素性测试

from&nbsp;Crypto.Util.number&nbsp;import&nbsp;isPrime

# Miller-Rabin素性测试
is_prime = isPrime(p)

3. 模幂运算

# Python内置pow函数支持模幂运算
result = pow(base, exponent, modulus)

4. Base64操作

import&nbsp;base64

# 编码
encoded = base64.b64encode(data)

# 解码
decoded = base64.b64decode(encoded)

9.3 常见RSA攻击类型

除了本题的CRT参数泄露攻击,RSA还有其他常见攻击:

1. 小指数攻击

  • 公钥指数e很小(如e=3)
  • 明文很小,使得m^e < n
  • 直接对密文开e次方根

2. 共模攻击

  • 同一明文用不同公钥加密
  • 但使用相同的模数n
  • 利用扩展欧几里得算法恢复明文

3. Wiener攻击

  • 私钥指数d很小
  • d < n^0.25
  • 利用连分数攻击

4. 费马分解

  • p和q非常接近
  • |p-q| 很小
  • 可以快速分解n

5. Coppersmith攻击

  • 已知部分明文
  • 使用格基约化技术
  • 可以恢复完整明文

9.4 调试技巧

在解题过程中的调试建议:

1. 逐步验证

# 每一步都打印中间结果
print(f"解码后长度:&nbsp;{len(data)}")
print(f"找到{len(integers)}个整数")
print(f"验证:&nbsp;{(d * e) % phi ==&nbsp;1}")

2. 异常处理

try:
&nbsp; &nbsp; result = some_operation()
except&nbsp;Exception&nbsp;as&nbsp;e:
&nbsp; &nbsp; print(f"错误:&nbsp;{e}")
&nbsp; &nbsp; print(f"当前状态:&nbsp;{current_state}")

3. 中间结果保存

# 保存中间结果,避免重复计算
with&nbsp;open('intermediate.txt',&nbsp;'w')&nbsp;as&nbsp;f:
&nbsp; &nbsp; f.write(f"p =&nbsp;{p}\n")
&nbsp; &nbsp; f.write(f"q =&nbsp;{q}\n")

4. 使用日志

import&nbsp;logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

logger.debug(f"尝试k={k}")
logger.info(f"找到质数p")

第十步:完整解题脚本

为了方便学习和复现,这里提供一个完整的自动化解题脚本:

#!/usr/bin/env python3
"""
RSA CRT参数泄露攻击 - 完整解题脚本
"""
import&nbsp;base64
from&nbsp;Crypto.Util.number&nbsp;import&nbsp;isPrime, bytes_to_long, long_to_bytes

print("="&nbsp;*&nbsp;70)
print("RSA CRT参数泄露攻击 - 自动化解题")
print("="&nbsp;*&nbsp;70)
print()

# 步骤1: Base64解码
print("[步骤1] Base64解码和DER解析")
print("-"&nbsp;*&nbsp;70)

base64_fragment =&nbsp;"Os9mhOQRdqW2cwVrnNI72DLcAXpXUJ1HGwJBANWiJcDUGxZpnERxVw7s0913WXNtV4GqdxCzG0pG5EHThtoTRbyX0aqRP4U/hQ9tRoSoDmBn+3HPITsnbCy67VkCQBM4xZPTtUKM6Xi+16VTUnFVs9E4rqwIQCDAxn9UuVMBXlX2Cl0xOGUF4C5hItrX2woF7LVS5EizR63CyRcPovMCQQDVyNbcWD7N88MhZjujKuSrHJot7WcCaRmTGEIJ6TkU8NWt9BVjR4jVkZ2EqNd0KZWdQPukeynPcLlDEkIXyaQx"

data = base64.b64decode(base64_fragment)
print(f"Base64解码完成:&nbsp;{len(data)}&nbsp;字节")

# 解析DER编码的INTEGER
integers = []
i =&nbsp;0
while&nbsp;i < len(data):
&nbsp; &nbsp;&nbsp;if&nbsp;data[i] ==&nbsp;0x02:
&nbsp; &nbsp; &nbsp; &nbsp; length = data[i +&nbsp;1]
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;length <=&nbsp;127:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; value = int.from_bytes(data[i+2:i+2+length], byteorder='big')
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; integers.append(value)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; i +=&nbsp;2&nbsp;+ length
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; i +=&nbsp;1
&nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp; i +=&nbsp;1

print(f"提取INTEGER参数:&nbsp;{len(integers)}&nbsp;个")

dp, dq, qinv = integers[-3], integers[-2], integers[-1]
print(f"dp &nbsp; =&nbsp;{dp}")
print(f"dq &nbsp; =&nbsp;{dq}")
print(f"qinv =&nbsp;{qinv}")
print()

# 步骤2: 恢复质数p和q
print("[步骤2] 从CRT参数恢复质数p和q")
print("-"&nbsp;*&nbsp;70)

e =&nbsp;65537
d1p = dp * e -&nbsp;1

print(f"公钥指数 e =&nbsp;{e}")
print("开始遍历k值...")

p, q =&nbsp;None,&nbsp;None
for&nbsp;k&nbsp;in&nbsp;range(3, e):
&nbsp; &nbsp;&nbsp;if&nbsp;d1p % k ==&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; candidate_p = d1p // k +&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;isPrime(candidate_p):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"找到质数p (k={k})")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; p = candidate_p

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; d1q = dq * e -&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;m&nbsp;in&nbsp;range(3, e):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;d1q % m ==&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; candidate_q = d1q // m +&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;isPrime(candidate_q):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; q = candidate_q
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(qinv * q) % p ==&nbsp;1&nbsp;or&nbsp;(qinv * p) % q ==&nbsp;1:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"找到质数q (m={m})")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"验证qinv关系: 通过")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;q:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break

if&nbsp;not&nbsp;p&nbsp;or&nbsp;not&nbsp;q:
&nbsp; &nbsp; print("错误: 无法恢复质数")
&nbsp; &nbsp; exit(1)

print(f"p =&nbsp;{p}")
print(f"q =&nbsp;{q}")
print()

# 步骤3: 计算私钥d
print("[步骤3] 计算RSA私钥d")
print("-"&nbsp;*&nbsp;70)

n = p * q
phi = (p -&nbsp;1) * (q -&nbsp;1)

def&nbsp;egcd(a, b):
&nbsp; &nbsp; u, u1 =&nbsp;1,&nbsp;0
&nbsp; &nbsp;&nbsp;while&nbsp;b:
&nbsp; &nbsp; &nbsp; &nbsp; q = a // b
&nbsp; &nbsp; &nbsp; &nbsp; u, u1 = u1, u - q * u1
&nbsp; &nbsp; &nbsp; &nbsp; a, b = b, a - q * b
&nbsp; &nbsp;&nbsp;return&nbsp;u

d = egcd(e, phi)
if&nbsp;d <&nbsp;0:
&nbsp; &nbsp; d += phi

print(f"n &nbsp; =&nbsp;{n}")
print(f"phi =&nbsp;{phi}")
print(f"d &nbsp; =&nbsp;{d}")
print(f"验证: (d*e) mod phi =&nbsp;{(d*e) % phi}")
print()

# 步骤4: RSA解密
print("[步骤4] RSA解密flag.enc")
print("-"&nbsp;*&nbsp;70)

with&nbsp;open("flag.enc",&nbsp;"rb")&nbsp;as&nbsp;f:
&nbsp; &nbsp; ciphertext_bytes = f.read()

ciphertext = bytes_to_long(ciphertext_bytes)
print(f"密文长度:&nbsp;{len(ciphertext_bytes)}&nbsp;字节")

plaintext = pow(ciphertext, d, n)
plaintext_bytes = long_to_bytes(plaintext)
print(f"RSA解密完成")

# 去除PKCS#1填充
separator = plaintext_bytes.find(b'\x00',&nbsp;2)
if&nbsp;separator !=&nbsp;-1:
&nbsp; &nbsp; message = plaintext_bytes[separator+1:].decode('utf-8').strip()
&nbsp; &nbsp; print(f"去除PKCS#1填充")
&nbsp; &nbsp; print()
&nbsp; &nbsp; print("="&nbsp;*&nbsp;70)
&nbsp; &nbsp; print("解密成功!")
&nbsp; &nbsp; print("="&nbsp;*&nbsp;70)
&nbsp; &nbsp; print(f"Flag:&nbsp;{message}")
&nbsp; &nbsp; print("="&nbsp;*&nbsp;70)
else:
&nbsp; &nbsp; print("错误: 未找到PKCS#1填充分隔符")

总结

这道题目是一个优秀的RSA密码分析案例,展示了:

  1. 密码学基础知识:RSA算法、ASN.1编码、PKCS#1填充
  2. 数学原理应用:模运算、欧几里得算法、素性测试
  3. 密码分析技巧:从部分信息恢复完整密钥
  4. 实践能力培养:编程实现、调试技巧、工具使用

通过深入分析这道题,我们不仅学会了如何解决这个特定问题,更重要的是掌握了密码分析的思维方法和技术手段。

密码学的安全性建立在数学难题之上,但实现中的任何细微疏忽都可能导致整个系统的崩溃。作为安全研究人员,我们需要:

  • 深入理解密码算法的数学原理
  • 关注实现细节和潜在的弱点
  • 保持对新攻击技术的学习
  • 在实践中不断提升分析能力

希望这份详细的技术解析能够帮助你深入理解RSA密码学及其安全性。在实际应用中,请务必遵循密码学最佳实践,使用经过充分测试的密码库,并时刻保持对安全的警惕。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:破镜安全 破镜安全 破镜安全《RSA密码学题目完全解析:从部分私钥片段到完整密钥恢复》

评论:0   参与:  0