在CTF竞赛中,密码学(Crypto)类题目往往需要扎实的数学基础和灵活的思维方式。今天我们来分析一道名为"反常规"的RSA题目。这道题目看似简单,实则蕴含着对RSA加密原理深刻理解的考察。
题目给出了一个名为cipher.txt的文件,内容如下:
N = 2277984791022346369005533904783614818826102788659651508959767202083843778453131366658916382803461140562467908905967443285040501371560088604538394878005827646410146244954745505114406792711000349929611271710262426493710967674490536959788665890671796421985910748091011210709414415838780453626144971988788672588103654983
e = 65537
c = 415510106371698055042355817455792784402467839071261284227679808181073943762112386236619891503158397068812942349049185918370823556100880803528976860244812587012654626659823858350868438615582709075400040571632681052556974452098591809573228654622307014559692352778252371646024960520522510301144376842967556042367321117
这是典型的RSA密码学题目,包含三个关键参数:
N:RSA模数(公钥的一部分)
e:公钥指数(通常是65537)
c:密文
我们的任务是解密密文c,获得明文flag。
在开始解题之前,让我们先回顾一下RSA加密算法的基本原理。这对于理解解题思路至关重要。
RSA是一种非对称加密算法,其密钥生成过程如下:
选择两个大质数:随机选择两个足够大的质数p和q
计算模数:N = p × q
计算欧拉函数:φ(N) = (p-1) × (q-1)
选择公钥指数e:通常选择65537,要求gcd(e, φ(N)) = 1
计算私钥指数d:满足 d × e ≡ 1 (mod φ(N))
其中,(N, e)构成公钥,d构成私钥。
加密过程:c = m^e mod N(m为明文)
解密过程:m = c^d mod N(需要私钥d)
RSA的安全性建立在大整数分解的困难性上。具体来说:
公开的模数N很难被分解成p和q
不知道p和q,就无法计算φ(N)
不知道φ(N),就无法计算私钥d
没有私钥d,就无法解密密文c
因此,破解RSA的关键往往在于如何分解模数N。
面对这道题目,我们首先想到的是常规的RSA破解方法。
对于RSA题目,最常用的工具是factordb.com等在线大整数分解网站。我们将N提交到factordb进行查询。
结果:分解失败
这个N在factordb数据库中没有记录,说明它不是常见的可分解大整数。
既然在线分解不成功,我们需要仔细观察这个N的特征。
首先,我们计算N的位数:
N = 2277984791022346369005533904783614818826102788659651508959767202083843778453131366658916382803461140562467908905967443285040501371560088604538394878005827646410146244954745505114406792711000349929611271710262426493710967674490536959788665890671796421985910748091011210709414415838780453626144971988788672588103654983
print(f"N的十进制位数: {len(str(N))}")
print(f"N的二进制位数: {N.bit_length()}")
输出:
N的十进制位数: 616
N的二进制位数: 2048
这个N是一个616位的十进制数,二进制位数为2048位。虽然这个长度对于常规的质因数分解来说已经很大了,但并不算特别异常。
此时,我们注意到题目的名称——"反常规"。这个名称暗示着这道题的解法可能不走寻常路。
在标准的RSA实现中,N必须是两个大质数p和q的乘积。那么,是否有可能这道题目故意违反了这个规则呢?
既然题目强调"反常规",我们不妨大胆假设:N本身就是一个质数。
如果N是质数,那意味着:
N无法被分解为两个质数的乘积(除了N = N × 1)
这完全违反了RSA的设计原则
但这也意味着我们可以直接计算φ(N)
让我们验证这个假设。
我们使用gmpy2库的is_prime函数进行质数判定:
import gmpy2
N = 2277984791022346369005533904783614818826102788659651508959767202083843778453131366658916382803461140562467908905967443285040501371560088604538394878005827646410146244954745505114406792711000349929611271710262426493710967674490536959788665890671796421985910748091011210709414415838780453626144971988788672588103654983
is_prime = gmpy2.is_prime(N)
print(f"N是否为质数: {is_prime}")
运行结果:
N是否为质数: True
果然!N确实是一个质数!
这个发现彻底改变了我们的解题思路。
这里简单介绍一下gmpy2.is_prime使用的Miller-Rabin算法:
Miller-Rabin是一种概率性素性测试算法:
如果返回False,则该数一定是合数
如果返回True,则该数极有可能是质数(误判概率可以忽略不计)
该算法基于费马小定理的推广,通过多轮随机测试来判断一个数是否为质数。对于2048位的大整数,该算法可以在合理时间内给出准确结果。
发现N是质数后,问题的关键转移到了欧拉函数的计算上。
欧拉函数φ(n)定义为:小于或等于n的正整数中,与n互质的数的个数。
例如:
φ(6) = 2,因为1和5与6互质
φ(7) = 6,因为1,2,3,4,5,6都与7互质
欧拉函数有几个重要性质:
质数的欧拉函数:如果p是质数,则φ(p) = p - 1
证明:质数p只能被1和p整除,因此1到p-1的所有数都与p互质,共有p-1个。
积性性质:如果gcd(m, n) = 1,则φ(m × n) = φ(m) × φ(n)
质数幂的欧拉函数:φ(p^k) = p^k - p^(k-1) = p^(k-1) × (p-1)
在标准RSA中,N = p × q,其中p和q是两个不同的质数。
由于gcd(p, q) = 1,根据积性性质:
φ(N) = φ(p × q) = φ(p) × φ(q) = (p-1) × (q-1)
这就是为什么RSA密钥生成中需要知道p和q才能计算φ(N)。
但在本题中,N本身就是质数,根据性质1:
φ(N) = N - 1
这个计算非常简单,完全不需要分解N!
这就是"反常规"RSA的破绽所在:出题者使用质数作为模数N,导致任何人都可以轻松计算出φ(N),进而计算出私钥d。
现在我们已经掌握了所有必要的知识,可以开始完整的解题过程了。
import gmpy2
N = 2277984791022346369005533904783614818826102788659651508959767202083843778453131366658916382803461140562467908905967443285040501371560088604538394878005827646410146244954745505114406792711000349929611271710262426493710967674490536959788665890671796421985910748091011210709414415838780453626144971988788672588103654983
is_prime = gmpy2.is_prime(N)
print(f"N是质数: {is_prime}")
由于N是质数,直接使用公式:
phi = N - 1
print(f"φ(N) = {phi}")
私钥d需要满足:d × e ≡ 1 (mod φ(N))
换句话说,d是e在模φ(N)意义下的乘法逆元。我们使用扩展欧几里得算法计算:
e = 65537
d = gmpy2.invert(e, phi)
print(f"私钥d = {d}")
扩展欧几里得算法简介:
扩展欧几里得算法不仅能计算两个数的最大公约数gcd(a, b),还能找到整数x和y,使得:
a × x + b × y = gcd(a, b)
当gcd(e, φ(N)) = 1时,可以找到d,使得:
e × d + φ(N) × y = 1
即:e × d ≡ 1 (mod φ(N))
这正是我们需要的私钥d。
使用RSA解密公式:
c = 415510106371698055042355817455792784402467839071261284227679808181073943762112386236619891503158397068812942349049185918370823556100880803528976860244812587012654626659823858350868438615582709075400040571632681052556974452098591809573228654622307014559692352778252371646024960520522510301144376842967556042367321117
m = pow(c, d, N)
print(f"明文m = {m}")
Python的内置函数pow(base, exp, mod)使用快速幂算法,可以高效地计算大整数的模幂运算。
在RSA中,通常将明文字符串转换为大整数进行加密。解密后需要执行逆向转换:
from Crypto.Util.number import long_to_bytes
flag = long_to_bytes(m)
print(f"flag = {flag.decode()}")
long_to_bytes函数将大整数转换为字节串,然后decode()将字节串解码为UTF-8字符串。
将以上步骤整合,得到完整的解题代码:
import gmpy2
from Crypto.Util.number import long_to_bytes
# 题目给定的参数
N = 2277984791022346369005533904783614818826102788659651508959767202083843778453131366658916382803461140562467908905967443285040501371560088604538394878005827646410146244954745505114406792711000349929611271710262426493710967674490536959788665890671796421985910748091011210709414415838780453626144971988788672588103654983
e = 65537
c = 415510106371698055042355817455792784402467839071261284227679808181073943762112386236619891503158397068812942349049185918370823556100880803528976860244812587012654626659823858350868438615582709075400040571632681052556974452098591809573228654622307014559692352778252371646024960520522510301144376842967556042367321117
# 步骤1: 验证N是质数
print("[*] 步骤1: 检查N是否为质数")
is_prime = gmpy2.is_prime(N)
print(f"N是质数: {is_prime}")
if is_prime:
# 步骤2: 计算欧拉函数
print("\n[*] 步骤2: 计算欧拉函数φ(N)")
print("对于质数N,φ(N) = N - 1")
phi = N - 1
# 步骤3: 计算私钥d
print("\n[*] 步骤3: 计算私钥d")
print("d = e^(-1) mod φ(N)")
d = gmpy2.invert(e, phi)
print(f"d = {d}")
# 步骤4: 解密密文
print("\n[*] 步骤4: 解密密文")
print("m = c^d mod N")
m = pow(c, d, N)
print(f"m = {m}")
# 步骤5: 转换为字符串
print("\n[*] 步骤5: 将明文转换为字符串")
flag = long_to_bytes(m)
print(f"flag = {flag.decode()}")
执行以上代码,得到输出:
[*] 步骤1: 检查N是否为质数
N是质数: True
[*] 步骤2: 计算欧拉函数φ(N)
对于质数N,φ(N) = N - 1
[*] 步骤3: 计算私钥d
d = e^(-1) mod φ(N)
d = 2207424510176787934394837140715825049959401708949272752483451114679321098588934704097041407490416202354405137416898460727574761136513214626980481918878131381365780514493202660989984774779094240245660055121589879294690059418402895016636385625202462355174317315089428093420858161140629419540650147795779517372670381875
[*] 步骤4: 解密密文
m = c^d mod N
m = 1825392592398934075154884194155486729881993593317630611143701820268438931986696074302898704594184880585913946148874048180915529228920269824674776445020933723106862350781238376927880596150034464
[*] 步骤5: 将明文转换为字符串
flag = flag{bbe6ef5272d9be08a9a6e452b485aaf6}
成功获取flag:flag{bbe6ef5272d9be08a9a6e452b485aaf6}
RSA加密算法的安全性建立在以下几个前提之上:
大整数分解困难性:当前没有已知的多项式时间算法可以有效分解足够大的合数
N必须是两个大质数的乘积:只有这样,φ(N)才难以直接计算
p和q必须保密:一旦p和q泄露,φ(N)立即可计算
p和q的选择有要求:不能太接近、不能太小、p-1和q-1需要有大质因子等
当N本身就是质数时:
φ(N)可直接计算:任何人都可以用φ(N) = N - 1计算出欧拉函数值
私钥d可直接计算:知道φ(N)后,通过扩展欧几里得算法即可求出d
完全失去安全性:加密形同虚设,任何人都可以解密
从数学角度看,这相当于:
正常RSA:N = p × q,φ(N) = (p-1)(q-1),需要分解N才能得到p和q
本题RSA:N = N × 1,φ(N) = N - 1,无需分解即可得到φ(N)
在CTF竞赛中,出题者会故意设置一些"陷阱"或"反常规"的场景,目的是:
考察基础知识:检验选手是否真正理解RSA的数学原理,而非只会套用工具
培养思维灵活性:鼓励选手在常规方法失败时,能够换个角度思考
强调细节重要性:密码学实现中,任何细节的偏差都可能导致整个系统崩溃
提升问题意识:让选手意识到什么是正确的实现,什么是错误的实现
欧拉函数在数论中有广泛应用:
欧拉定理:如果gcd(a, n) = 1,则a^φ(n) ≡ 1 (mod n)
费马小定理:欧拉定理的特殊情况,当n为质数p时,a^(p-1) ≡ 1 (mod p)
计算模逆元:a^(-1) ≡ a^(φ(n)-1) (mod n)
降幂运算:在模运算中简化大指数幂的计算
除了本题利用的"N为质数"漏洞外,RSA还可能面临以下攻击:
小公钥指数攻击:当e很小且明文m也很小时,c = m^e可能小于N,直接开e次方根即可
低私钥攻击(Wiener's Attack):当d < N^0.25时,可以通过连分数算法恢复d
共模攻击:使用相同的N对不同消息加密,可能导致信息泄露
中国剩余定理攻击:当同一明文用不同的N加密时,可能被破解
侧信道攻击:通过时间、功耗等信息推断私钥
Coppersmith攻击:针对特定情况下的小根问题
Fermat分解法:当p和q很接近时有效
为了确保RSA的安全性,实际应用中应该:
使用足够大的密钥长度:至少2048位,推荐3072位或4096位
随机生成p和q:使用密码学安全的随机数生成器
p和q的选择要求:
|p - q|不能太小(避免Fermat分解)
p-1和q-1应有大质因子(避免Pollard p-1算法)
gcd(p-1, q-1)应该较小
使用标准的填充方案:如OAEP,防止各种攻击
保护私钥安全:使用安全的存储和传输机制
定期更新密钥:避免长期使用同一密钥对
本题使用的两个主要库:
gmpy2:高性能大整数运算库
is_prime():素性测试
invert():模逆元计算
powmod():模幂运算
gcd():最大公约数
PyCryptodome(Crypto):密码学库
Crypto.Util.number.long_to_bytes():整数转字节
Crypto.Util.number.bytes_to_long():字节转整数
Crypto.Util.number.getPrime():生成质数
提供AES、RSA、DES等多种加密算法
收集信息:确认给定了哪些参数(N、e、c、p、q、d等)
尝试分解N:使用factordb、yafu等工具
观察参数特征:
N的大小是否异常
e的值是否特殊(过大或过小)
是否有多组加密数据
考虑特殊攻击:
N是质数(本题情况)
e=3的小公钥攻击
共模攻击
Wiener攻击等
检查实现漏洞:padding不当、随机数不安全等
重视题目名称和描述:往往包含重要提示
不要局限于常规方法:当常规工具失效时,考虑特殊情况
理解原理而非死记硬背:只有深刻理解才能应对各种变化
善用脚本验证假设:编写代码快速验证各种可能性
记录解题过程:便于复盘和总结经验
这道"反常规"RSA题目通过让模数N本身成为质数,巧妙地考察了选手对RSA数学原理的理解,特别是欧拉函数的性质。
解题的关键在于:
意识到常规分解方法不可行
从题目名称"反常规"得到启发
大胆假设N可能是质数
验证假设并利用质数的欧拉函数性质φ(N) = N - 1
按照标准流程计算私钥并解密
这道题目虽然技术难度不高,但思维难度较大。它提醒我们:
密码学的安全性依赖于严格的数学假设
任何违反安全假设的实现都是脆弱的
理解原理比记住工具更重要
CTF中要保持开放和灵活的思维
通过这道题目的学习,我们不仅掌握了一种特殊的RSA破解方法,更重要的是加深了对RSA加密原理、欧拉函数性质以及密码学安全性基础的理解。这些知识将帮助我们在未来的CTF竞赛和实际安全工作中更好地分析和解决问题。