随机数生成:别让伪随机数坑了你的系统

说实话,随机数这个话题,我在安全审计里见过太多翻车案例了。很多开发者觉得随机数嘛,不就是 rand() 一下的事?嗯,如果你写的是贪吃蛇游戏,那没问题。但如果你在做密码学、Token生成、会话ID……那 rand() 就是个定时炸弹。

我个人习惯把随机数分成两类:普通随机数密码学安全随机数。这两者的区别,说白了就是「够用就行」和「必须扛住攻击」的区别。今天我们就来掰扯清楚。

rand() 与 srand():为什么说它俩是「玩具」?

先看一个经典代码:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

int main() {
    srand(time(NULL));
    int token = rand();
    printf("你的Token是:%d\n", token);
    return 0;
}

这段代码有什么问题?问题大了去了。

第一,种子可预测。 time(NULL) 返回的是当前秒级时间戳。攻击者只要知道你的程序大概在什么时间运行,就能把种子猜个八九不离十。我在一次渗透测试中,就遇到过某物联网设备用 srand(time(0)) 生成设备密钥,结果我写了个脚本,把前后几分钟的时间戳全试了一遍,密钥直接破掉。

第二,周期太短。 标准 rand() 的实现通常是线性同余生成器(LCG),周期只有 2^31 左右。你想想看,现代CPU一秒能跑几十亿次运算,这点周期根本不够看。

第三,分布不均匀。 低位的随机性尤其差。很多老教材里教你用 rand() % N 取模,这其实会引入偏差——除非 N 能整除 RAND_MAX+1,否则某些数字出现的概率就是比其他数字高。

警告: 永远不要用 rand() % N 来生成密码学场景下的随机数。攻击者可以利用这种偏差来缩小暴力破解的范围。

getrandom():Linux 下的正确姿势

那该用什么?在 Linux 系统上,我推荐 getrandom()。这个系统调用直接从内核的熵池里取数据,质量有保障。

#include <linux/random.h>
#include <sys/syscall.h>
#include <unistd.h>

int get_random_bytes(void *buf, size_t len) {
    ssize_t ret = syscall(SYS_getrandom, buf, len, 0);
    if (ret != len) {
        // 处理错误
        return -1;
    }
    return 0;
}

// 使用示例
int main() {
    unsigned char key[32];
    if (get_random_bytes(key, sizeof(key)) == 0) {
        // key 现在是 32 字节的密码学安全随机数
    }
    return 0;
}

这里有个细节要注意:getrandom() 在 Linux 3.17 之后才引入。如果你的目标平台比较老,可能需要用 /dev/urandom 来替代。不过我个人习惯是优先用 getrandom(),因为它不会因为文件描述符耗尽而出问题。

小技巧: getrandom() 的第三个参数可以传 GRND_NONBLOCK。如果熵池不够,它会直接返回错误而不是阻塞。这在初始化阶段特别有用——你可以先尝试非阻塞模式,不行再走阻塞模式。

arc4random():BSD 家族的宝贝

如果你在 macOS、iOS 或者各种 BSD 系统上开发,arc4random() 是你的好朋友。它基于 ChaCha20 流密码,性能好,而且不需要你手动播种。

#include <stdlib.h>

// 生成一个随机整数
uint32_t val = arc4random();

// 生成 [0, upper_bound) 范围内的随机数,无偏差
uint32_t bounded = arc4random_uniform(100);

// 生成随机字节
uint8_t buf[16];
arc4random_buf(buf, sizeof(buf));

你看,arc4random_uniform() 直接帮你解决了取模偏差的问题。我曾经在做一个抽奖系统时,看到有人用 arc4random() % 100,我当场就给他指出来了——明明有现成的无偏接口,何必自己造轮子?

不过要注意,arc4random() 在 macOS 10.12 之后被标记为废弃,苹果推荐改用 CCRandomGenerateBytes()。但很多开源项目仍然在用,因为它在 BSD 世界太普及了。

密码学安全随机数:到底「安全」在哪?

我们常说的 CSPRNG(Cryptographically Secure Pseudo-Random Number Generator),它和普通随机数的区别,我用一个表格来说明:

特性 普通随机数(rand) 密码学安全随机数
可预测性 知道种子就能预测全部序列 即使知道之前的所有输出,也无法预测下一个
种子来源 通常用 time(),熵极低 从硬件噪声、系统中断等物理源收集熵
周期 2^31 左右 极大(2^256 甚至更高)
抗攻击能力 能抵抗已知的密码分析攻击
典型用途 游戏、模拟、测试 密钥生成、Token、会话ID、盐值

说白了,密码学安全随机数的核心要求是:即使攻击者拿到了你之前生成的所有随机数,他也无法推断出下一个会是什么。这叫「前向安全性」。

各平台安全随机数 API 一览

不同平台有各自的推荐接口,我整理了一份速查表:

平台 推荐 API 头文件 备注
Linux (3.17+) getrandom() <linux/random.h> 系统调用,直接取内核熵
Linux (旧版) /dev/urandom 文件操作 不会阻塞,但初始化阶段可能熵不足
BSD/macOS arc4random_buf() <stdlib.h> 基于 ChaCha20,无需播种
Windows BCryptGenRandom() <bcrypt.h> 推荐用 BCRYPT_USE_SYSTEM_PREFERRED_RNG 标志
跨平台 (OpenSSL) RAND_bytes() <openssl/rand.h> 需要链接 OpenSSL 库
核心原则: 在密码学场景下,永远不要自己实现随机数生成器。用操作系统提供的 CSPRNG 接口,它们经过了大量安全专家的审查和实战检验。

知识体系:随机数安全的核心脉络

下面这张图,是我梳理的随机数安全知识体系。你可以把它当作一个检查清单:

随机数安全知识体系 随机数生成器 普通随机数(非安全) 密码学安全随机数 rand() / srand() 线性同余生成器 getrandom() arc4random() 游戏、模拟、测试、洗牌 密钥、Token、会话ID、盐值 避坑:不要用 rand() 做安全相关操作!不要自己实现随机数算法!

避坑指南:我踩过的那些坑

我曾经接手过一个项目,里面用 rand() 生成 API Token。更离谱的是,每次程序重启后,因为 srand(time(NULL)) 的种子相同,生成的 Token 序列居然一模一样。攻击者只要抓到一个 Token,就能推算出后续所有 Token。

还有一次,我看到有人用 /dev/random 而不是 /dev/urandom/dev/random 在熵池不足时会阻塞,导致程序卡死。其实对于绝大多数应用场景,/dev/urandom 完全够用,而且不会阻塞。

再次强调: 不要用 rand() % N 做任何安全相关的事情。不要用 time() 做种子。不要自己写随机数算法。这三个「不要」能帮你避开 90% 的随机数安全漏洞。

嗯,关于随机数安全,今天就聊这么多。记住一句话:安全随机数不是功能,而是基础设施。选对了,你的系统就多了一层保障;选错了,那就是给攻击者留后门。


公众号:蓝海资料掘金营,微信deep3321