19、随机数生成安全:伪随机数生成器的弱点,安全的随机数生成方法

随机数,听起来很简单对吧?

但在安全领域,这玩意儿坑特别多。我见过不少项目,加密算法选得挺强,密钥长度也够,结果随机数生成器没用好,整个安全体系直接崩塌。说白了,你锁再结实,钥匙是别人能猜到的,那跟没锁有什么区别?

伪随机数生成器的本质

计算机本质上是个确定性的机器。你给它相同的输入,它永远给你相同的输出。所以,真正的随机数在普通计算机上很难生成。

我们平时用的 rand()random(),其实都是伪随机数生成器(PRNG)。它们的工作原理是这样的:

  1. 拿一个初始值,叫种子(seed)
  2. 用某个数学公式反复计算
  3. 输出看起来"随机"的数列

嗯,这里要注意:只要种子相同,生成的随机数序列就完全相同。这是伪随机数生成器最根本的弱点。

伪随机数生成器的典型弱点

我在项目中遇到过好几次因为随机数出问题导致的安全事故。总结下来,主要有这么几类:

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)) 生成加密密钥。更离谱的是,代码里还写了个注释:"随机数种子,足够随机"。我当时就无语了。

修复方案其实很简单:

  1. rand() 全部替换成 /dev/urandomBCryptGenRandom
  2. 对于已有的加密数据,全部重新加密
  3. 在代码审查流程里加一条规则:禁止使用 rand()srand() 做安全用途

还有一个坑:不要混淆"统计随机"和"密码学安全随机"。有些PRNG在统计学上看起来挺随机,比如梅森旋转算法(Mersenne Twister),但它不是密码学安全的。攻击者只要观察624个连续输出,就能完全预测后续所有输出。我见过有人拿它生成会话令牌,结果被攻击者轻松预测。

🔑 核心要点:
  • 安全场景下,永远用 /dev/urandomBCryptGenRandomRAND_bytes 等系统级接口
  • 不要用 time()pidrand() 做安全随机数
  • 不要自己实现随机数算法
  • 定期审查代码中的随机数使用情况

最后说一句:随机数安全,说白了就是不可预测性。你生成的随机数,攻击者能不能猜到?如果能,那你的加密再强也没用。记住这一点,很多安全问题就能避免。

随机数生成安全:核心知识体系 伪随机数生成器的弱点 种子可预测 用 time()、pid 等做种子 周期太短 LCG 周期仅 2^31 内部状态泄露 观察输出可反推状态 后果:加密密钥、会话令牌可被预测 安全的随机数生成方法 Linux:/dev/urandom 从硬件噪声源收集熵 Windows:BCryptGenRandom 系统首选RNG接口 跨平台:OpenSSL RAND_bytes 自动适配各平台随机源 核心原则:使用CSPRNG,不自己造轮子
公众号:蓝海资料掘金营,微信 deep3321