19、随机数生成安全:伪随机数生成器的弱点,安全的随机数生成方法
随机数,听起来很简单对吧?
但在安全领域,这玩意儿坑特别多。我见过不少项目,加密算法选得挺强,密钥长度也够,结果随机数生成器没用好,整个安全体系直接崩塌。说白了,你锁再结实,钥匙是别人能猜到的,那跟没锁有什么区别?
伪随机数生成器的本质
计算机本质上是个确定性的机器。你给它相同的输入,它永远给你相同的输出。所以,真正的随机数在普通计算机上很难生成。
我们平时用的 rand()、random(),其实都是伪随机数生成器(PRNG)。它们的工作原理是这样的:
- 拿一个初始值,叫种子(seed)
- 用某个数学公式反复计算
- 输出看起来"随机"的数列
嗯,这里要注意:只要种子相同,生成的随机数序列就完全相同。这是伪随机数生成器最根本的弱点。
伪随机数生成器的典型弱点
我在项目中遇到过好几次因为随机数出问题导致的安全事故。总结下来,主要有这么几类:
1. 种子可预测
最常见的错误,就是用time(NULL)做种子。
// 危险示例:千万别这么写!
srand(time(NULL));
int key = rand(); // 攻击者只要知道大致时间,就能猜出key
为什么危险?因为time()返回的是秒级时间戳。攻击者可以缩小时间范围到几秒内,然后暴力枚举所有可能的种子。我曾经审计过一个物联网设备固件,里面就是用时间做种子生成会话密钥。结果呢?攻击者只要看设备启动时间,就能算出所有后续通信的密钥。
time()、getpid()、rand() 等函数生成安全相关的随机数。这些值太容易预测了。
2. 周期太短
很多老旧的PRNG算法,比如rand()使用的线性同余生成器(LCG),周期非常短。有的只有2^31左右。你想想看,对于现代计算机来说,枚举2^31个值根本不算事。
我记得有一次做渗透测试,目标系统用的是一个自定义的LCG生成器。我写了个脚本,跑了不到两分钟,就把它的整个状态空间给枚举完了。从那以后,我对任何"自己写的随机数算法"都持怀疑态度。
3. 内部状态泄露
有些PRNG,如果你拿到它输出的几个连续值,就能反推出它的内部状态,进而预测后续所有输出。这叫做状态恢复攻击。
比如早期的rand()实现,你只要拿到一个输出值,基本上就能算出下一个。这在安全场景下是致命的。
安全的随机数生成方法
那到底该怎么生成安全的随机数呢?我个人习惯分场景来处理:
场景一:Linux/Unix 系统
用 /dev/urandom 或者 getrandom() 系统调用。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
// 安全的随机数生成函数
int generate_secure_random_bytes(unsigned char *buf, size_t len) {
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) return -1;
ssize_t ret = read(fd, buf, len);
close(fd);
return (ret == (ssize_t)len) ? 0 : -1;
}
int main() {
unsigned char key[32]; // 256位密钥
if (generate_secure_random_bytes(key, sizeof(key)) == 0) {
// 成功生成安全随机数
printf("安全随机密钥已生成\n");
}
return 0;
}
/dev/urandom 会从系统的物理噪声源(比如硬件中断、磁盘I/O时间、鼠标移动等)收集熵,然后用密码学安全的算法生成随机数。它不会阻塞,适合大多数应用场景。
/dev/random。但它可能会因为熵池耗尽而阻塞。我个人建议:日常用 /dev/urandom 就够了,除非你在做非常敏感的操作。
场景二:Windows 系统
Windows 上推荐用 BCryptGenRandom() 函数:
#include <windows.h>
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")
// Windows下安全随机数生成
int generate_secure_random_bytes_windows(unsigned char *buf, size_t len) {
NTSTATUS status = BCryptGenRandom(
NULL, // 使用默认算法提供者
buf, // 输出缓冲区
(ULONG)len, // 需要的字节数
BCRYPT_USE_SYSTEM_PREFERRED_RNG // 使用系统首选RNG
);
return (status >= 0) ? 0 : -1;
}
这个函数底层也是从系统的硬件随机源获取熵的,安全级别很高。
场景三:跨平台 C 代码
如果你需要写跨平台的代码,我建议用 OpenSSL 库:
#include <openssl/rand.h>
// 使用OpenSSL生成安全随机数
int generate_secure_random_openssl(unsigned char *buf, int len) {
// RAND_bytes 返回1表示成功,0表示失败
return RAND_bytes(buf, len);
}
OpenSSL 内部会根据不同平台自动选择最合适的随机源。我在多个跨平台项目里都用这个方案,没出过问题。
安全随机数生成的核心原则
总结一下,我个人认为安全随机数生成要遵循这几个原则:
| 原则 | 说明 | 常见错误 |
|---|---|---|
| 使用密码学安全PRNG | 必须用CSPRNG(密码学安全伪随机数生成器) | 用 rand()、random() 生成密钥 |
| 熵源要足够 | 种子必须来自硬件噪声等不可预测源 | 用 time()、pid 做种子 |
| 不要自己造轮子 | 使用操作系统或标准库提供的安全接口 | 自己实现LCG、梅森旋转算法 |
| 定期重新播种 | 长时间运行的进程要定期更新种子 | 进程启动时播种一次,永远不变 |
避坑指南:我曾经踩过的坑
我曾经接手过一个项目,里面用 srand(time(NULL)) 生成加密密钥。更离谱的是,代码里还写了个注释:"随机数种子,足够随机"。我当时就无语了。
修复方案其实很简单:
- 把
rand()全部替换成/dev/urandom或BCryptGenRandom - 对于已有的加密数据,全部重新加密
- 在代码审查流程里加一条规则:禁止使用
rand()和srand()做安全用途
还有一个坑:不要混淆"统计随机"和"密码学安全随机"。有些PRNG在统计学上看起来挺随机,比如梅森旋转算法(Mersenne Twister),但它不是密码学安全的。攻击者只要观察624个连续输出,就能完全预测后续所有输出。我见过有人拿它生成会话令牌,结果被攻击者轻松预测。
- 安全场景下,永远用
/dev/urandom、BCryptGenRandom、RAND_bytes等系统级接口 - 不要用
time()、pid、rand()做安全随机数 - 不要自己实现随机数算法
- 定期审查代码中的随机数使用情况
最后说一句:随机数安全,说白了就是不可预测性。你生成的随机数,攻击者能不能猜到?如果能,那你的加密再强也没用。记住这一点,很多安全问题就能避免。