资讯动态

RSA弱密钥检测与生成加固:共享素数、批量GCD与Fermat分解

发布时间:2026/9/20 8:25:37 来源:尧图企业网站定制
简介这是一份面向信息安全、密码学方向本科生及毕业设计选题学生的学位论文文档聚焦RSA非对称加密算法在实际部署中因密钥生成不当而产生的弱密钥风险适合作为毕业设计参考、课程论文模板或安全入门研读材料。资源包仅含1个docx文件压缩后约29KB正文约一万字含封面、摘要、关键词、目录及六章完整结构并附中英文题目对照。论文从RSA算法原理、密钥生成与加解密流程讲起梳理弱密钥的定义、分类与成因重点分析小指数攻击、选择密文攻击、中间人攻击等利用方式及其影响并给出数学验证、随机性检查等检测手段与增大密钥长度、强化随机源、定期换钥等防护建议结论部分还指出研究不足与后续方向。目前已有642人浏览学习可为选题定位、框架搭建与内容扩写提供直接参照。1. RSA 弱密钥不是算法被攻破而是密钥生成环节漏了气给一批内网 HTTPS 资产做普查脚本跑完三千张证书不到十秒其中十七对证书的模数 n 共享同一个素因子——这十七张证书的私钥可以在毫秒级被还原出来。RSA 算法本身没崩崩的是密钥生成那一步。所谓弱密钥指的不是密码学被破解而是生成端在随机源、素数选取、参数配置上偷了懒导致 n 本身就带着可被批量筛出的结构性缺陷。弱密钥漏洞分析要回答三个问题哪些 n 一眼能拆、用哪些命令能成规模地筛、生成端该把参数卡在哪条线上。做渗透测试、代码审计、PKI 运维的读者都能从这套检测链条里直接取走可复用的部分。2. 从 n p × q 拆解RSA 弱密钥的七种典型形态2.1 RSA 安全性依赖的三条前提假设RSA 公钥是 (n, e)私钥是 d其中 n p × qp 与 q 是两个大素数。整套体系的安全性建立在三条假设上第一n 不能在可行时间内被分解回 p 和 q第二p、q 必须来自不可预测的随机源攻击者无法猜到第三e 与 φ(n) (p-1)(q-1) 必须互素且私钥指数 d 不能小到能被连分数逼近。这三条里任何一条落地失败攻击者都不需要去分解一个 2048 位的大整数绕过去就行。共享素数是最典型的例子两张证书的 n 单看都是合法大整数可一旦对它们做一次 gcd两个私钥同时暴露。这也是弱密钥漏洞和RSA 算法漏洞的根本区别。算法层面目前没有多项式时间的分解方法2128 位的整数分解依然是硬骨头但实现层面一个 rand() 用错地方就能把一个证书体系的强度从 2 的 2048 次方拉到 2 的 31 次方。分析的重心因此要放在生成链路而不是数学论文里。2.2 七种典型形态与触发条件实际审计里高频出现的弱密钥可以归到下面这张表里。每一行都对应一个可操作的特征检测脚本就是围绕这些特征写的。形态数学特征常见触发原因检测手段小模数n 位数不足如 512/1024 bit历史系统遗留、嵌入式资源受限直接统计 n.bit_length()共享素数gcd(n₁, n₂) 1熵池不足、多台设备同一镜像启动批量 GCD / 乘积树近素数|p - q| 很小同一次随机调用连取两个素数Fermat 分解小私钥指数d n^0.25 / 3为提速自行推导了过小的 dWiener 连分数攻击小公钥指数滥用e 3 且无填充或填充可预测实现偷懒、自研协议解析证书扩展与握手抓包结构化素数p kM (65537^a mod M)固件里的确定性素数生成器ROCA 类特征检测重复 / 黑名单模数n 命中已知弱密钥库随机源被确定性替换指纹比对 复用计数表格里最容易被忽略的是第二行和第三行。共享素数看起来需要两把密钥同时中招概率很低但熵池缺陷是成批出现的——同一个镜像刷出来的设备或者在同一个时间窗口内批量签发的证书命中率高得离谱。近素数的触发条件更隐蔽如果代码里是先随机取一个大数当 p再在它附近找下一个素数当 q那么两个素因子的差会小到 Fermat 分解几步就收敛。2.3 实现层为什么比算法层更容易翻车熵的问题几乎全部出在实现层。操作系统启动初期熵池还没被搅动/dev/urandom 在部分内核上会给出可预测的内容有些嵌入式设备干脆没有硬件随机源用启动时间戳加一个固定种子当随机数容器化和虚拟机又带来新的坑快照回滚之后多个实例可能从同一个随机状态开始跑。更细的一类问题来自进程模型。如果密钥生成发生在 fork 之前或者 fork 之后父子进程共享同一份 RNG 状态那么父子进程会算出几乎相同的素数进而产生一批共享素因子的模数。这类缺陷不会在单元测试里暴露只有把成千上万张证书的 n 收集到一起做交叉 gcd才会浮出水面。所以弱密钥分析的工程形态天生就是批量采集 批量比对而不是逐个手工验证。3. 搭一套可复现的弱密钥自检环境3.1 依赖与目录约定整套检测只依赖三样东西一个能解析 PEM/DER 的 Python 环境、一个 OpenSSL 命令行、以及可选的 Sage只在需要跑更复杂的格攻击时才用得上。Python 侧建议装 cryptography 处理编码装 gmpy2 做加速的大整数运算——纯标准库的 int 也能跑只是面对十万量级的模数时慢一截。目录上按采集 / 提取 / 检测 / 结果四层分开证书和公钥统一丢进 keys/ 目录提取出来的模数写成一行一条的文本检测脚本只读文本、不碰证书。这样做的原因是提取阶段会踩到各种编码问题把这一步和检测解耦重跑的时候不用重新解析一遍几千个文件。# 只装必需的依赖Sage 可选 pip install cryptography gmpy2 mkdir -p keys out # 把待检的 .crt / .pem / .cer 全部丢进 keys/ ls keys | head3.2 用 OpenSSL 做第一轮体检动手写脚本之前先用 OpenSSL 对单个密钥做一遍基础检查确认文件本身没坏、模数位数正常。# 查看私钥的完整结构重点看 modulus 的位数 openssl rsa -in server.key -text -noout # 一致性校验OpenSSL 会用私钥参数重算一遍模数并比对 openssl rsa -in server.key -check -noout # 只输出模数并算摘要方便跨机器比对两把密钥是否同源 openssl rsa -in server.key -noout -modulus | openssl dgst -sha256 # 从证书里抠出公钥再解析注意这里要走 pkey -pubin openssl x509 -in server.crt -noout -pubkey | openssl pkey -pubin -text -noout第一条命令输出里的modulus部分是十六进制的 n位数除以 4 就是比特长度低于 2048 bit 的直接标记为待迁移对象。第二条命令正常输出RSA key ok如果这里报错说明文件本身已经损坏或者公私钥不匹配属于比弱密钥更严重的问题。第三条命令的摘要值用来做去重——同一对密钥被重复部署到十台机器上摘要会一模一样这种复用本身也是风险点因为一处泄露等于十处泄露。最后一条命令在证书里抠公钥时常见报错是类似rsa public key not find的信息八成是文件是 DER 编码或者传输中被截断了加-inform DER或者重新导出即可。3.3 从证书和密钥里批量导出 n 与 e单把密钥手工看没问题但弱密钥的价值在批量。下面这段脚本遍历目录把所有证书和公钥里的 (n, e) 抽出来写成文本。# extract_n.py —— 批量导出模数 n 与公钥指数 e import os from cryptography import x509 from cryptography.hazmat.primitives.serialization import load_pem_public_key def load_pubkey(path): with open(path, rb) as f: data f.read() if bBEGIN CERTIFICATE in data: cert x509.load_pem_x509_certificate(data) return cert.public_key() # 证书从扩展里取 SubjectPublicKeyInfo return load_pem_public_key(data) # 裸公钥文件 def collect(keys_dir, out_pathout/moduli.txt): rows [] for name in sorted(os.listdir(keys_dir)): if not name.endswith((.pem, .crt, .cer)): continue try: pub load_pubkey(os.path.join(keys_dir, name)) nums pub.public_numbers() rows.append((name, nums.n, nums.e)) except Exception as exc: # 编码错误 / 截断 / 非 RSA 密钥都在这里被跳过不阻断整批 print(f[skip] {name}: {exc}) with open(out_path, w) as f: for name, n, e in rows: f.write(f{n:x}:{e}:{name}\n) # n 用十六进制写方便和 OpenSSL 输出对齐 print(fcollected {len(rows)} keys - {out_path}) return rows if __name__ __main__: collect(keys)这段逻辑的核心是三点。一是用文件头判断类型BEGIN CERTIFICATE走证书解析分支否则按裸公钥处理二是public_numbers()一次拿到 n 和 ee 的值通常就是 65537如果发现 e 是 3 或者 1说明生成端配置有问题直接进弱密钥名单三是输出格式选n:x而不是十进制因为 OpenSSL 的-modulus输出也是十六进制两边可以直接肉眼比对。捕获异常的那一行别删实际数据里混进 ECC 证书、截断文件、甚至纯文本误命名的概率不低跳过比让整批中断划算。4. 三类高频弱密钥的实战检测4.1 共享素数批量 GCD 一把筛出全部拿到摸数列表之后第一件事就是两两求最大公约数。朴素做法是 O(n²) 次 gcd一万个模数就是一亿次运算跑得动但没必要。用前缀积加后缀积可以把每对比较压成一次大整数乘法复杂度降到 O(n)十万量级也能在几分钟内出结果。# batch_gcd.py —— 用前缀积/后缀积筛共享素因子 import math def load_moduli(path): items [] for line in open(path): n_hex, e, name line.strip().split(:) items.append((int(n_hex, 16), int(e), name)) return items def batch_gcd(moduli): n len(moduli) if n 2: return [] # prefix[i] moduli[0] * ... * moduli[i-1] prefix [1] * (n 1) for i in range(n): prefix[i 1] prefix[i] * moduli[i] # suffix[i] moduli[i] * ... * moduli[n-1] suffix [1] * (n 1) for i in range(n - 1, -1, -1): suffix[i] suffix[i 1] * moduli[i] hits [] for i in range(n): # 除自己以外的所有模数之积 others prefix[i] * suffix[i 1] g math.gcd(moduli[i], others) if 1 g moduli[i]: # 严格小于自身说明 g 是真因子 hits.append((i, g)) return hits if __name__ __main__: items load_moduli(out/moduli.txt) moduli [it[0] for it in items] for idx, g in batch_gcd(moduli): n, e, name items[idx] print(f[HIT] {name} 共享素因子 p {hex(g)[:24]}...)判定条件1 g moduli[i]是关键。gcd 等于 1 说明两两互素正常gcd 等于自身说明这一轮没筛出东西只可能出现在列表里只有一个元素时只有落在开区间里g 才是 n 的真因子p 和 q n // g 一除就出来私钥 d 用扩展欧几里得求逆即可还原。注意这段代码没有做去重同一对共享素数会在两个下标上各报一次实际用的时候把 g 收进 set 或者按 p 排序后合并即可。规模再上一档比如要筛全网的证书前缀积会变成天文数字的巨型整数这时候换成乘积树加余数树把大整数运算控制在可控的位宽内。思路不变只是乘法和取模的组织形式改了。4.2 素数靠得太近Fermat 分解三秒出结果当 |p - q| 足够小时可以把 n 写成 a² - b² 的形式其中 a (pq)/2b (p-q)/2。从 a ⌈√n⌉ 开始逐次往上试只要 a² - n 是完全平方数就命中。如果 p 和 q 差得不多通常几十次迭代就能收敛对 2048 位的 n 来说耗时在毫秒级。# fermat.py —— 近素数场景下的快速分解 import math def fermat_factor(n, max_iter1 20): a math.isqrt(n) if a * a n: a 1 # a 从 ceil(sqrt(n)) 开始 for _ in range(max_iter): b2 a * a - n b math.isqrt(b2) if b * b b2: return a - b, a b # 命中返回 p 和 q a 1 return None # 超过迭代上限说明两素数差距太大 if __name__ __main__: n 0x00c1e3... # 填入待检模数 result fermat_factor(n) print(result if result else not close-prime vulnerable)这里的迭代上限max_iter是个可调参数。调太小会漏报差的位数在几十位以上的情况需要更多轮调太大则在正常密钥上白白浪费 CPU。实践经验是给到 2 的 20 次方正常生成的 2048 位密钥跑满也就几秒钟而真正有问题的近素数密钥基本在几百轮内就出结果。数学上的判据是 |p - q| n^(1/4) 时必然快速收敛超出这个范围就退化成暴力枚举不划算。顺便说一句用math.isqrt而不是int(math.sqrt(n))因为后者在 n 超过 2 的 53 次方之后会因为浮点精度丢失而算错整数部分这个坑在大整数场景里非常常见。4.3 私钥指数过小Wiener 连分数攻击有些实现为了加速解密故意把私钥指数 d 选得很小只要满足 d n^0.25 / 3就能用连分数展开 e/n 的方式把 d 逼出来。原理是 e·d ≡ 1 mod φ(n)可写成 e/φ(n) ≈ k/d而 k/d 恰好是 e/n 的一个收敛子。# wiener.py —— 小私钥指数检测 import math def continued_fraction(a, b): while b: q a // b yield q a, b b, a - q * b def convergents(cf): h0, h1, k0, k1 0, 1, 1, 0 for a in cf: h0, h1 h1, a * h1 h0 # 分子 k0, k1 k1, a * k1 k0 # 分母 yield h1, k1 def wiener_attack(e, n): for k, d in convergents(continued_fraction(e, n)): if k 0 or (e * d - 1) % k ! 0: continue phi (e * d - 1) // k # 由 ed - 1 k·φ(n) 反推 s n - phi 1 # p q disc s * s - 4 * n # 判别式需为完全平方才有整数解 if disc 0: continue t math.isqrt(disc) if t * t ! disc: continue if (s t) % 2 0: return d, (s t) // 2, (s - t) // 2 return None if __name__ __main__: e, n 65537, 0x00b4a1... # 填入待检的公钥参数 print(wiener_attack(e, n))检测的关键在disc这一行。算出候选 d 之后用 φ(n) 反解 p q再看判别式 s² - 4n 是不是完全平方是的话 p、q 都是整数攻击成立不是就换下一个收敛子继续试。整个过程不需要分解 n也不需要任何格基约减工具标准库足够。返回三元组说明密钥已经废了直接进吊销名单。4.4 别把「支持 RSA 密钥交换」当成弱密钥做资产扫描时经常看到报告里写目标主机支持 RSA 密钥交换【原理扫描】。这条结论说的是 TLS 握手阶段用 RSA 做密钥传输——客户端用服务端公钥加密预主密钥服务端用私钥解——因此不具备前向保密性一旦私钥泄露历史流量可以被解密。这是协议配置问题跟本章讨论的密钥本身弱不弱是两码事。区分方式很简单前者看的是 cipher suite 列表openssl s_client -connect host:443 -cipher RSA能握手成功就说明支持后者看的是 n 本身有没有结构缺陷必须把模数拿出来跑上面的批量 GCD、Fermat 和 Wiener。把两者混在一份报告里容易把修复方向带偏——协议层该做的是把 ECDHE 加进优先列表密钥层该做的是重新生成密钥并吊销旧的。两条路互不替代。5. 生成端加固与上线前的密钥自检5.1 参数该卡在哪条线上生成端的参数选择有明确的底线。模数位数不低于 2048 bit需要长期保护的场景上到 3072 或 4096公钥指数固定用 65537不要用 3也不要自己造私钥指数必须由 d ≡ e⁻¹ mod φ(n) 或 mod λ(n) 直接算出来绝不允许事后手动调小。随机源一律走操作系统提供的加密安全接口Python 里用secretsJava 里用SecureRandomC 里用getrandom(2)或者RAND_bytes()任何基于时间戳、进程号、rand()的生成逻辑都要当成缺陷处理。进程模型上还有一条容易被忽略的规则密钥生成不建议放在 fork 之前完成也不建议在容器启动脚本里批量生成一堆密钥。前者会让父子进程共享 RNG 状态后者可能在熵池尚未就绪时拿到可预测的随机数。稳妥做法是等熵池就绪后再生成或者干脆由独立的密钥管理服务统一签发。5.2 上线前跑一遍组合检查单个 RSA 密钥签发之后用下面这套流程过一遍能覆盖到绝大多数已知的弱密钥形态。检查项命令或脚本不通过的处置结构完整性openssl rsa -in k.pem -check -noout重新生成不要修复文件模数位数openssl rsa -in k.pem -noout -text | grep Private-Key低于 2048 bit 直接废弃公钥指数解析e必须等于 65537不等则重新生成近素数跑fermat_factor(n)给足迭代上限命中则重新生成并排查代码小私钥指数跑wiener_attack(e, n)命中则重新生成并排查代码共享素数全量模数入库定期跑批量 GCD命中的整批全部吊销密钥复用模数摘要去重计数摘要重复过的密钥全部吊销前四项在单机就能跑完属于签发时的门禁后三项依赖全局视图得有一套持续运行的巡检任务。共享素数和密钥复用这两项尤其不能只在签发时检查——新签发的密钥可能和半年前某台设备上的旧密钥撞了素因子只有把历年的模数都留档才在下次比对时筛得出来。5.3 一个能跑在日常巡检里的技巧把模数库当成一份持续增长的资产每次巡检只做三件事新采集的模数和全库做一次交叉 GCD、对每位新模数跑一次 Fermat 和 Wiener、按模数摘要统计复用次数。这套流程不需要昂贵的算力一台普通机器跑几万个模数在分钟级完成。真正要花心思的是模数的来源覆盖——漏采某个网段的证书就等于漏掉那批设备可能存在的共享素因子。还有一个细节值得留意签名算法为sha1WithRSAEncryption的证书往往是老系统的遗留这类资产同时出现小模数和弱参数的概率明显偏高巡检时可以把它们排在队列前面优先处理。检测脚本的输出建议按命中类型 证书指纹 首次发现时间三列落库这样既能追溯哪台设备反复出问题也能在重新生成密钥之后自动确认是否修干净了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价