为何 glibc 的 rand() 不够随机?——深入源码与攻击案例
tr17ish 历史精选 828浏览 · 2025-01-31 12:11

序言

在许多 C 语言程序中,rand() 函数被广泛用于生成随机数。然而,glibc 提供的 rand() 真的足够“随机”吗?在安全性要求较高的场景下,它是否会带来隐患?

事实上,rand() 采用的是线性同余生成器(LCG),其随机性远不及现代加密安全的随机数生成器(CSPRNG)。在特定条件下,攻击者甚至可以通过观察少量输出值来预测后续的随机数,进而利用这一漏洞进行攻击。

本文将深入 glibc 的 rand() 实现源码,分析其工作机制及随机性缺陷,并结合实际案例,探讨如何利用这些弱点进行预测攻击。

源码分析

分析环境:

GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.8) stable release version 2.35. Copyright (C) 2022 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled by GNU CC version 11.4.0. libc ABIs: UNIQUE IFUNC ABSOLUTE For bug reporting instructions, please see: https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs.

源码来源:

https://elixir.bootlin.com/glibc/glibc-2.35/source

rand部分

要了解随机数的生成机制,我们先写一个poc来跟踪rand函数的函数调用过程:

gdb调试跟踪找到如下的调用链:



找到具体的random_r函数源码:

其中有一个关键的结构体random_data

字段名
类型
作用
fptr
int32_t *
指向当前前端(front)的状态变量,参与随机数计算。
rptr
int32_t *
指向当前后端(rear)的状态变量,通常滞后于 fptr
state
int32_t *
指向存储随机数生成器状态的数组,所有的内部状态都存储在这里。
rand_type
int
指示使用的 RNG 算法类型(不同 rand_type 代表不同的算法)。
rand_deg
int
表示状态数组 state 中用于计算的“多项式阶数”(degree of recursion),即状态变量的数量。
rand_sep
int
rand_sep = fptr - rptr,表示 fptrrptr 之间的间隔,用于计算新的随机数。
end_ptr
int32_t *
指向 state 数组的末尾,用于检测是否超出边界。

glibc 定义了 5 种 rand_type,用于控制 random_data 结构体的工作方式:

rand_type
名称
说明
0
TYPE_0
经典的 LCG(低质量,最简单)
1
TYPE_1
7 级移位寄存器
2
TYPE_2
15 级移位寄存器
3
TYPE_3
31 级移位寄存器
4
TYPE_4
63 级移位寄存器

默认情况下,glibc 使用 TYPE_3,即 31 级移位寄存器,它的随机性比传统 LCG 强,但仍然可以通过足够的样本进行预测。直接用gdb查看也能证明:



回到函数__random_r的源码,我们可以发现其实在这个函数中只是对于是否是使用了TYPE_0进行了区分,如果是使用TYPE_0,是最简单的线性同余算法 (Linear Congruential Generator, LCG):
这个算法:

乘以 1103515245,加 12345,再取 0x7fffffff 保持非负数。

计算出的值存回 state[0],并赋值给 result 作为输出。

这也就意味着只需要知道随机数中的任意一次结果就能精准预测出后面的所有结果,如果是处理其他类型 (TYPE_1 及以上):

fptrrptr 分别是前向指针和后向指针,通常用于更高级别的随机数生成器(如 TYPE_1 及以上的 Tausworthe 或者 rand48 算法)。

计算:

val = *fptr += (uint32_t) *rptr:将 rptr 处的值加到 fptr 处,并存储到 fptr

*result = val >> 1:去掉最低位(最不随机的位)

移动 fptrrptr 指针:

fptr 递增,超过 end_ptr 时,回绕到 state

rptr 也递增,超过 end_ptr 时,同样回绕。

可以发现在这个过此中,整个随机数的生成都是依赖于一个数组state,一旦泄漏了这个数组,就能实现任意随机数的预测。

srand部分

那么这个数组是什么时候初始化的呢,根据我们的代码很容易看出来是srand函数,我们继续跟踪这个函数,找到源码:

处理过程和random_r一样,只是对是否是TYPE_0做了区分,如果 rand_typeTYPE_0,即选择了最简单的随机数生成算法,则跳到 done 结束函数,因为不需要进一步的复杂初始化,如果不是,初始化过程更复杂,使用了一个线性递推算法(类似于 LFSRTausworthe 生成器):

word 根据递推公式更新,其中:

hilo 是将当前的 word 分为两部分,用于避免 31 位溢出。

更新公式为: word=16807×lo−2836×hiword = 16807 imes lo - 2836 imes hiword=16807×lo−2836×hi

如果结果为负数,则加上 2147483647 保证结果为正数。

每次更新后,将 word 写入 state 数组。

每次写入的数量是由buf->rand_deg决定的,而这个值也是由rand_type决定的,因此由于glibc默认的rand_typeTYPE_3,默认的buf->rand_deg也就是DEG_3,因此对于一般的随机数预测程序,只需要泄漏124字节就能实现精准预测。

这里我们注意到,为了保证计算过程中不会出现除0异常,这里的处理是将种子0和种子1做了相同的处理,因此,srand(0)srand(1)的效果是完全一致的。

type部分

那么如何重新设置随机数生成器的rand_type呢?我们看剩下两个函数setstate_rinitstate_r

1 initstate_r 函数

initstate_r 的作用是初始化随机数生成器的状态信息。它会根据给定的字节数 n 选择一个合适的随机数生成器类型,并进行状态设置。函数的工作流程如下:参数:seed:初始化种子,用于生成随机数。arg_state:指向存储随机数生成器状态的内存区域。n:表示状态数组的字节数,用来确定应使用哪个类型的随机数生成器。buf:指向 random_data 结构体的指针,存储有关随机数生成器的状态。这个函数会将state替换成用户指定的内存空间,并利用用户提供的新的seed和对应的rand_type进行初始化。

1 setstate_r 函数

setstate_r 的作用是恢复随机数生成器的状态。它将给定的状态数组 arg_state 恢复到 buf 中,以便继续使用之前的状态生成随机数。工作流程如下:参数:arg_state:包含恢复的随机数生成器状态的内存区域。buf:指向 random_data 结构体的指针,存储当前的随机数生成器状态。如果用户传入的state是合规的,将会将随机数生成器的状态恢复成用户指定的状态。

攻击手法

从种子角度

根据前面的源码分析,随机数生成器的初始化基本上完全依赖于随机数种子,因此,如果种子是一个固定值或者是一个可以预测的值,那么攻击者就可以很轻易的生成一个完全一致的随机数生成器来对随机数进行预测:

1种子是一个固定值

攻击脚本:

效果:



1种子依赖于时间

攻击脚本:

效果:



从泄漏state角度

根据前面的分析,如果我们能泄漏出整个state就能实现任意随机数的泄漏:

有一个很明显的数组越界可以用来泄漏出state中的数据,自行构建一个random函数进行预测即可。

攻击脚本:

效果:



从生成方式的角度

当使用较低安全性的生成器时(如TYPE_0),根据random_r函数中的生成方式,我们可以很轻易得根据任意一个随机数推断出之后的每一个随机数:

攻击脚本:

效果:



总结

通过对 glibc 的 rand() 函数源码的深入分析,我们可以清晰地看到其随机数生成的机制及其潜在的缺陷。rand() 函数使用的是线性同余生成器(LCG)或类似的算法,虽然这些算法在计算上非常高效,但在安全性方面存在明显的不足。尤其是在安全性要求较高的场景下,rand() 的随机性远远不够,攻击者可以通过多种方式预测随机数序列,进而实施攻击。

主要问题总结

1 种子依赖性rand() 的随机性完全依赖于初始种子。如果种子是固定的或可预测的(如基于时间),攻击者可以轻松重现相同的随机数序列。

2 状态泄漏rand() 的内部状态(state 数组)一旦泄漏,攻击者可以通过已知的状态值预测未来的随机数输出。

3 算法弱点rand() 使用的 LCG 或类似算法在数学上并不复杂,攻击者可以通过观察少量输出值推断出内部状态,进而预测后续的随机数。

攻击手法总结

1 种子预测攻击:通过固定种子或基于时间的种子,攻击者可以重现随机数序列,从而绕过基于随机数的安全机制。

2 状态泄漏攻击:通过数组越界或其他漏洞,攻击者可以泄漏 state 数组的内容,进而预测未来的随机数。

3 算法推断攻击:对于使用简单 LCG 的 TYPE_0 类型,攻击者可以通过已知的随机数输出推断出后续的随机数序列。

改进建议

1 使用加密安全的随机数生成器(CSPRNG):在安全性要求较高的场景下,应使用如 /dev/urandomgetrandom() 等加密安全的随机数生成器,而不是 rand()

2 避免固定种子:在使用 rand() 时,应确保种子是不可预测的,避免使用固定值或基于时间的种子。

3 定期重置状态:如果必须使用 rand(),建议定期重置随机数生成器的状态,增加攻击者预测的难度。

4 使用更复杂的随机数生成算法:如果性能允许,可以考虑使用更复杂的随机数生成算法,如 Mersenne Twister 或其他现代随机数生成器。

结论

虽然 rand() 函数在简单的随机数生成场景中表现良好,但在安全性要求较高的应用中,它的随机性远远不够。通过本文的分析和攻击案例,我们可以看到,攻击者可以通过多种方式预测 rand() 的输出,进而实施攻击。因此,在设计和实现安全相关的系统时,开发者应谨慎选择随机数生成器,避免使用 rand(),转而使用更安全的替代方案。

0 条评论
某人
表情
可输入 255