随机数生成:别让伪随机数坑了你的系统
说实话,随机数这个话题,我在安全审计里见过太多翻车案例了。很多开发者觉得随机数嘛,不就是 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 库 |
知识体系:随机数安全的核心脉络
下面这张图,是我梳理的随机数安全知识体系。你可以把它当作一个检查清单:
避坑指南:我踩过的那些坑
我曾经接手过一个项目,里面用 rand() 生成 API Token。更离谱的是,每次程序重启后,因为 srand(time(NULL)) 的种子相同,生成的 Token 序列居然一模一样。攻击者只要抓到一个 Token,就能推算出后续所有 Token。
还有一次,我看到有人用 /dev/random 而不是 /dev/urandom。/dev/random 在熵池不足时会阻塞,导致程序卡死。其实对于绝大多数应用场景,/dev/urandom 完全够用,而且不会阻塞。
rand() % N 做任何安全相关的事情。不要用 time() 做种子。不要自己写随机数算法。这三个「不要」能帮你避开 90% 的随机数安全漏洞。
嗯,关于随机数安全,今天就聊这么多。记住一句话:安全随机数不是功能,而是基础设施。选对了,你的系统就多了一层保障;选错了,那就是给攻击者留后门。