不安全的随机数漏洞与防护(随机性不足的熵源与PRNG攻击)
字数 3036 2025-12-09 17:51:15
不安全的随机数漏洞与防护(随机性不足的熵源与PRNG攻击)
知识点描述:
随机数在安全系统中扮演着至关重要的角色,广泛应用于密码学操作(如密钥生成、初始化向量、盐值、nonce)、会话标识符生成、CSRF令牌、密码重置令牌等场景。不安全的随机数漏洞的核心在于,由于熵源(随机性来源)不足或伪随机数生成器(PRNG)的实现或使用不当,导致生成的随机值具有可预测性或熵值过低,从而被攻击者推测、暴力破解或重放。这是一个基础但常被忽视的威胁,能够从根本上瓦解依赖随机性的安全机制。
解题/讲解过程:
让我们循序渐进地剖析这个漏洞的原理、攻击方式,并最终构建完整的防护策略。
第一步:理解随机数的分类与安全要求
首先,我们需要明确随机数的两种核心类型及其安全属性:
- 真随机数(TRNG):其随机性来源于物理世界的不可预测过程(如硬件噪声、鼠标移动、键盘敲击时序、系统中断时序等)。理论上不可预测,但生成速度较慢。
- 伪随机数(PRNG/CSPRNG):
- 伪随机数生成器(PRNG):一种确定性算法,从一个初始种子(seed)开始,通过数学公式产生一长串“看起来”随机的数字序列。如果种子已知或可预测,则整个序列可预测。 普通PRNG适用于模拟、游戏等非安全场景。
- 密码学安全的伪随机数生成器(CSPRNG):这是安全场景的“金标准”。它不仅是PRNG,还必须满足额外的安全属性:即使在已知之前生成的随机数序列的情况下,下一个随机数也必须是不可预测的。这通常通过从高质量的熵源(TRNG)收集足够的熵来“播种”和周期性地“重新播种”算法来保证。
安全随机数的核心要求是“不可预测性”,这直接依赖于初始种子的熵(随机性度量,单位比特)。熵值越高,攻击者猜测种子的可能性越低。
第二步:剖析常见漏洞根源(不安全的来源与实现)
漏洞的产生,通常源于以下一个或多个方面:
-
熵源不足:
- 场景:在系统启动早期、虚拟机/容器环境中,或嵌入式设备上,可用的硬件熵源(如硬件RNG模块)可能非常有限或不存在。如果系统在熵池未充分填满前就生成密钥,生成的随机数实际上熵值很低。
- 示例:早期的Linux
/dev/random在熵池耗尽时会阻塞,而某些应用会退而求其次使用非阻塞的/dev/urandom或自行实现脆弱的随机函数,导致安全隐患。
-
使用非密码学安全的PRNG:
- 函数误用:这是最常见的错误。使用编程语言中为通用目的设计的随机函数来生成安全令牌。
- 致命示例:在Python中,使用
random.randint()而不是secrets.randbelow()或os.urandom()来生成会话ID。 - 致命示例:在PHP的历史版本中,使用
rand()或mt_rand(),其内部状态可通过观测有限输出被逆向推导。 - 致命示例:在C/C++中使用
rand()函数。
- 致命示例:在Python中,使用
- 种子可预测:即使是CSPRNG,如果种子容易被猜中,也形同虚设。
- 示例:使用当前时间戳(秒级甚至毫秒级)作为唯一种子。攻击者可以枚举一个很小时间窗口内的所有可能时间戳。
- 示例:使用进程ID等低熵值、有限取值空间的系统信息作为种子。
- 函数误用:这是最常见的错误。使用编程语言中为通用目的设计的随机函数来生成安全令牌。
-
随机数暴露与重用:
- 生成的随机数(如初始化向量IV)如果被攻击者获取,可能会破坏加密模式的安全性(例如,在CBC模式中重用IV是危险的)。Nonce虽然可以公开,但绝不能重复使用。
-
算法实现缺陷:
- 历史上某些PRNG算法(如Dual_EC_DRBG)被怀疑存在后门,其输出存在可被掌握特定参数的攻击者预测的风险。
第三步:攻击手法演示
假设一个Web应用使用不安全的方式生成密码重置令牌。
- 漏洞代码示例(Python,错误示范):
import random import time def generate_reset_token(): # 错误1:使用非密码学安全的PRNG random.seed(int(time.time())) # 错误2:使用当前时间戳作为种子,熵极低 token = ''.join(random.choices('0123456789abcdef', k=32)) # 错误3:基于可预测种子生成 return token - 攻击过程:
- 攻击者在时间T请求一个密码重置令牌,收到令牌A。
- 攻击者可以合理推测服务器生成令牌的时间戳在T附近,误差在几秒内。
- 由于
random.seed(int(time.time()))重置了全局随机种子,接下来的随机序列完全由这个时间戳决定。 - 攻击者编写一个脚本,枚举从
T-5秒到T+5秒之间的所有整数时间戳(例如,总共11个值)。 - 对于每一个候选时间戳,攻击者都用其作为种子初始化同样的
random模块,并生成一个32字符的令牌。 - 攻击者将生成的候选令牌与收到的令牌A进行比较。一旦匹配成功,攻击者就掌握了服务器当前的随机种子。
- 灾难性后果:攻击者现在可以精确预测服务器为其他用户在同一时间段内生成的所有后续密码重置令牌、会话ID等。这意味着他可以重置任意用户的密码。
第四步:构建安全防护策略
防护的核心是“正确的来源,正确的使用”。
-
永远使用密码学安全的随机数生成器(CSPRNG):
- Python:使用
secrets模块(secrets.token_bytes,secrets.token_hex,secrets.randbelow)或os.urandom()。 - Java:使用
java.security.SecureRandom。 - C/C++:在类Unix系统上使用
/dev/urandom(现代观点认为其是安全的,不会在熵低时产生低质量输出);在Windows上使用BCryptGenRandom或CryptGenRandom。 - Go:使用
crypto/rand包。 - Node.js:使用
crypto.randomBytes()。
- Python:使用
-
确保足够的熵:
- 对于密钥生成等极高安全要求的场景,确保操作系统熵池健康。在虚拟化环境中,确保启用了 virtio-rng 等虚拟随机数设备,为宿主机提供熵。
- 现代操作系统(Linux内核 >= 5.4)的
/dev/urandom在系统启动后初始化时即从内核CSPRNG中获取输出,该CSPRNG在初始化后即被视为密码学安全,无需担心阻塞问题。在新项目中,应优先使用/dev/urandom或其语言封装。
-
使用适当的随机数类型和长度:
- 密钥、密码盐:使用高熵随机字节(如
os.urandom(32)生成256位密钥)。 - 会话令牌、CSRF令牌:使用足够长的随机字符串(如128位/16字节以上),并通过安全比较函数(常数时间比较)进行验证。
- Nonce和IV:确保其唯一性(不重复)。对于IV,在GCM等认证加密模式中,nonce的重复是灾难性的。
- 密钥、密码盐:使用高熵随机字节(如
-
安全存储与处理种子:
- 绝不使用可预测值作为CSPRNG的唯一种子。让CSPRNG自行从系统熵源获取种子。
- 在需要跨进程或跨重启复现随机序列的非安全场景(如科学模拟),可以保存种子,但必须确保其保密性。
-
代码审计与依赖检查:
- 在代码审查中,严格检查所有随机数生成调用,确认使用了正确的CSPRNG API。
- 定期更新语言运行时和密码学库,以获取CSPRNG实现的修复和改进。
总结:
不安全的随机数漏洞是“根基性”的威胁。防护的关键在于深刻理解不同随机数生成器的用途,在所有安全敏感的场景中,无一例外地使用经过验证的、密码学安全的随机数生成器(CSPRNG),并确保为其提供了良好的熵源环境,同时遵循生成、使用随机数的安全最佳实践,从而为上层安全机制打下坚实的基础。