使您的软件运行起来: 摆弄数字
真正安全的软件需要精确的随机数生成器

级别: 初级

Gary McGrawReliable Software Technologies
John ViegaReliable Software Technologies

2000 年 4 月 01 日

计算机一直是具有完全确定性的机器,所以,特别在行为随机性方面表现不尽人意(软件缺陷情况除外)。所以当程序员需要一个或一组真正的随机数时,他们必须通过各种方式近似地生成随机数。在本专题,关于这个主题的三篇文章的第一篇中,GaryMcGraw 和 John Viega分析了随机数生成器是如何工作的,并展示了各种作为结果可以实现的技巧。在本系列的 下一部分,Gary 和 John将讨论如何通过硬件来真正地生成随机数。
在本文中,我们来讨论最近在新闻中一直议论的话题:随机数的生成。嗯,这样说还不精确。事实上,是对新闻中提到的有关随机数生成问题的揭秘,而对于产生问题的原因不深究。您也许已经看到了有关 Reliable Software Technology 的因特网赌博揭秘的 CNN 新闻(请参阅 参考资料),或者您也许想起几年前关于 Netscape 的 SSL 实现被泄露这一事件。即使这两个应用听起来差别很大,其实每个问题的根本原因是一样的。

正当许多开发人员认为 random() 函数已在他们喜欢使用的语言中正确命名时,问题就发生了。但遗憾的是,这是一个有缺陷的假定。通常,调用 random() 实际上被认为是调用“伪随机”数生成器,当然,这些随机数不是真正随机的。这个事实对软件的安全性有着深远的影响。这里提供了两个示例来演示为什么会这样。

随机性不是您所想象的那样呆板:一些数字流要比其它一些更随机。计算机一直是具有完全确定性的机器,所以,特别在随机的行为方面表现不尽人意(这里撇开那些不怎么样的 OS 软件不谈)。事实上,(部分)随机数真正的唯一来源涉及到测量物理现象,譬如,放射性衰变的计时,它可以用某些数学方面的技巧来提取纯的随机序列。

无须利用实际的设备,计算机程序需要随机数时,会自己来生成这些数字。但这种计算机的决定机制使得生成随机数的算法很困难。正是由于这样,而使大多数程序员转向伪随机数。在本文中,将看一些伪随机数生成器 (PRNG)。分析它们是如何工作的,并显示它们为什么对于安全性不是很好。在下一个专题,来看一些针对随机性的基于硬件的解决方案。然后,来看一下介于中间的软件解决方案,这些方案提供的安全性要比伪随机数生成器更安全,而比更为安全的基于硬件解决方案要更切实可行。

伪随机数是如何工作的?

我们建立了真正调用伪随机数生成器的 random() 。但什么是伪随机数生成器?假定需要生成介于 1 和 10 之间的随机数,每一个数出现的几率都是一样的。理想情况下,应生成 0 到 1 之间的一个值,不考虑以前值,这个范围中的每一个值出现的几率都是一样的,然后再将该值乘以 10。请注意,在 0 和 1 之间有无穷多个值,而计算机不能提供这样的精度。

为了编写代码来实现类似于前面提到的算法,常见情况下,伪随机数生成器生成 0 到 N 之间的一个整数,返回的整数再除以 N。得出的数字总是处于 0 和 1 之间。对生成器随后的调用采用第一次运行产生的整数,并将它传给一个函数,以生成 0 到 N 之间的一个新整数,然后再将新整数除以 N 返回。这意味着,由任何伪随机数生成器返回的数目会受到 0 到 N 之间整数数目的限制。

在大多数的常见随机数发生器中,N 是 2 32? (大约等于 40 亿),对于 32 位数字来说,这是最大的值。换句话说,我们经常碰到的这类生成器能够至多生成 40 亿个可能值。而这 40 亿个数根本不算大,只是指尖这么大。

伪随机数生成器将作为“种子”的数当作初始整数传给函数。这粒种子会使这个球(生成伪随机数)一直滚下去。伪随机数生成器的结果仅仅是不可预测。由伪随机数生成器返回的每一个值完全由它返回的前一个值所决定(最终,该种子决定了一切)。如果知道用于计算任何一个值的那个整数,那么就可以算出从这个生成器返回的下一个值。

结果,伪随机数生成器是一个生成完全可预料的数列(称为流)的确定性程序。一个编写得很好的的 PRNG 可以创建一个序列,而这个序列的属性与许多真正随机数的序列的属性是一样的。例如:

PRNG 可以以相同几率在一个范围内生成任何数字。
PRNG 可以生成带任何统计分布的流。
由 PRNG 生成的数字流不具备可辨别的模式。
PRNG 所不能做的是不可预测。如果知道种子和算法,就可以很容易地推算出这个序列。

一方面,伪随机数生成器有许多有用的应用方面用途。它们对于蒙特卡罗仿真(一组使用随机数来仿真物理事件或解决数学问题的方法)和其它统计采样或仿真模型很适用。这些应用通常只需要它们有中度复杂程度的“随机性”,并且序列中的模式不与自然发生的序列相混即可。

另一方面,在那种需要不可预料性(如洗虚拟牌或加密数据)的应用中,伪随机数生成器很难以一种安全的方式来使用。

示例: 下面再现了用 Borland 编译器分配的伪随机数生成器。这个生成器是属于线形拟合生成器一类的。这类生成器相当普遍,它们采用很具体的数学公式:

Xn+1 = (aXn + b) mod c

 

换句话说, 第 n+1 个 数等于 第 n 个 数乘以某个常数 a,再加上常数 b。如果结果大于或等于某个常数 c,那么通过除以 c,并取它的余数来将这个值限制在一定范围内。请注意,a、b 和 c 通常是质数。Donald Knuth 在 "The Art of Computer Programming" (请参阅 参考资料)一书中详细介绍了对于这些常数,如何挑选好的值。

回到 Borland 生成器。如果知道 RandSeed 的当前值是 12345,那么下一个生成的整数是 1655067934。只要将种子设为 12345,那么每次得到的结果都是一样的。

Random () 的 Borland 的实现

long long RandSeed = #### ; 
unsigned long Random(long max) 
{ 
long long x ; 
double i ; 
unsigned long final ; 
x = 0xffffffff; 
x += 1 ; RandSeed *= ((long long)134775813); 
RandSeed += 1 ; 
RandSeed = RandSeed % x ; 
i = ((double)RandSeed) / (double)0xffffffff ; 
final = (long) (max * i) ; return (unsigned long)final; 
}

 

修补破坏的种子

基于历史先例,PRNG 的种子通常是参照系统时钟生成的。这个想法是使用系统时间的某一点来作为种子。这意味着如果能算出生成器什么时间发生,那么就可以知道由生成器生成的每一个值(包括数字出现的次序)。这样的结果说明,用时钟播种的伪随机数,所有结果不是不可预测的。正如我们所见,这个事实对于洗牌算法和密钥有着深远的影响。

您可能认为,对于 PRNG,如果取一个真正的随机数作为种子开始,那么可能会是安全的。毕竟,如果我们的确定性流是从随机点开始的,那么一切都没事了,对吗?错!即使攻击者不知道生成器采用哪个特殊数字作为种子,他仍然可以通过观察您的程序和做一些猜测来预测出 PRNG 生成的数字。这个问题是攻击者只需要看从生成器中产生的一个数字来预测下一个(以及所有其它随后的数字)。如果您选择 0 和 50 之间的数字,对于攻击者来说这个工作要困难些,但它仍然不是件难事。

下面说明原因。在标准的随机数“轮盘”(带有许多随机数生成算法,包括线形拟合生成器;在 0 和 4,294,967,295 之间的所有数字确切地生成一次,然后重复该序列)上只有 40 亿个可能的位置。如果我们观察足够多的数字,即使调整在 0 和 50 之间的数字,最终都可以算出生成器是如何播种的。我们可以通过一定的顺序尝试所有 40 亿个可能的种子,并在 PRNG 流中查找与您程序中展示的一致的子序列。现在,40 亿不是一个大数目。如果有几台高性能的 PC 机,假定有足够大的空间来“蛮力”执行,几乎可以实时地完成这项任务。

这里,我们的问题之一是大多数机器目前使用的是 32 位的 PRNG。幸运的是,我们可以解决这个问题。使用真正的随机数来作为 64 位 PRNG 的种子,会使得这较难被攻破(提醒您,这不是不可能,即对于好的 PRNG),因为,即使有极高运算能力的计算机,破解 64 位可能也需要花费数月。如果使用好的 128 位的 PRNG 和真正的随机种子,那么这恐怕是足够好的(虽然并不总是,但也确是;我们过一会儿讨论为什么)。种子空间的改进与密钥改进的原因是一模一样的:使用的位数越多,这两个密钥就越难破解(只要正确使用所有东西)。

让我们来研究一下密码密钥/种子之间的相似性。蛮力的密码术攻击为了破解一个秘密消息,需要按某种顺序尝试每一个可能的密钥。同样地,蛮力攻击 PRNG,需要尝试每一个可能的种子。国际上有一些重要的研究组织正在研究必需的密钥长度。一般来讲,类似于以下情况(1999 年 10 月):

算法 弱密钥 典型密钥 强密钥
DES 40 或 56 56 Triple-DES
RC4 60 80 128
RSA 512 768 或 1024 2048
ECC 125 170 230

过去人们常常认为实时破解 56 位 DES,由于花费太长时间而觉得它是不可行的,但历史却证明是另外一回事。1997 年 1 月,在 96 天内恢复 DES 密钥。后来这个时间缩短到 41 天,然后是 56 小时,在 1999 年 1 月,是 22 小时 15 分钟。对于长度较短的密钥或小集合的 PRNG 种子,这种跳跃前进的破解能力不是一个好的兆头。

人们甚至发明了特殊的机器来破解密码算法。在 1998 年,EFF 发明了用于破解 DES 消息的专用机器。这台机器的目的只是强调 DES(一个流行的、政府支持的密码算法)是如何易受攻击的。(如果想要了解有关 DES 破解器的更多情况,请参阅 参考资料。)“破解” DES 的难易程度直接与密钥的长度相关。所以,发明专门用于恢复 RNG 种子的机器也是在可能的范围之内。

128 位的种子能解决所有问题吗?它提供的足够大的搜索空间排除了蛮力攻击的可能。但是,生成器可能会受到其它类型的攻击。实际上,它取决于正在使用的、生成随机数的算法。线形拟合生成器(事实上,所有多重拟合生成器)被证实为易于受到相对较小的信息的攻击,因此要不惜一切代价避免。从现在开始的两篇文章将论述基于密码原语的 PRNG 算法,这种算法似乎比传统的生成器要好些。

 

尝试不要使它可预测

我们已经注意到了传统 PRNG 的一个重要问题,但还没有进入实质问题!这里不想误导您认为 64 位的 PRNG (或更好)和好的算法一起使用就会有任何的安全性。即使 128 位的 PRNG 也是完全可以预测的!如果想要安全性,需要为数字生成器播种一个真正的随机数。您不能凭主观意愿将一个值编制硬代码到程序中。攻击者很有可能注意到每次运行该代码时生成相同的数字序列。同样地,您不能请其他人手工地输入数字,因为人为的输入不是很好的随机性来源。

当调用随机数生成器时,其它用于种子的常见来源包括网络地址、主机名、人名和程序员的娘家姓氏。这种数据也常用作密钥。请注意,这些数据不会经常更改,或根本不会更改。如果攻击者可以算出您是如何挑选数据的(通常,这是一个安全的赌注),那么这个数据是攻击者穷追直至获取的必不可少的信息。而且,有时这类信息和时钟值一样容易猜测,甚至更容易些。

每个伪随机数生成器算法都有其可比较的优点和缺点,同时也有许多可供选择的算法。没有一个算法能适合所有的任务。事实上,对于上面所描述的原因,没有一个基于易于理解的算法的伪随机数生成器算法是(靠它自己)完全适合于安全性的。

如果有一个视伪随机数是必不可少的应用,那么这里有几个可以进一步了解不同算法特性的有价值的资料。较著名的参考资料是 Donald Knuth 的 "The Art of Computer Programming,Volume 2 (Seminumerical Algorithms)" 的第 3 章(请参阅 参考资料)。除此以外,还有在 Salzburg 大学数学系的 pLab 中的研究人员,他们花费了所有时间从理论上和实践上来研究随机数的生成。他们的网页(请参阅 参考资料)提供了许多有价值的线索来帮助您选择适合于您的应用的生成器。

不正确地使用伪随机数生成器会导致惊人的安全性问题。希望您不会发生这类问题。

如何在在线赌博中欺骗

Reliable Software Technologies (RST)(请参阅 参考资料)的软件安全性组最近在 Texas Hold 'em Poker(由 ASF 软件公司发行的)(请参阅 参考资料)实现中发现了一系列缺陷。这个揭秘允许欺骗性的玩家可以实时计算每人手中确切的牌。这意味着,使用这个揭秘的玩家可以知道每个对家手中的牌和将要发出的牌(指在一圈下赌后,将它面朝上放置的牌)。欺骗者每次可以“知道什么时候持有它们以及什么时候牌面朝下退出”。一个臭名昭著的攻击者可能使用这个揭秘,在未被发现的情况下来诈骗不知情玩家的钱财。

这个缺陷存在于用来生成每副牌的洗牌算法中。具有讽刺意味的是,这个代码曾公布在 http://www.planetpoker.com/ppfaq.htm 上,为的是显示这个游戏是很公平的,来吸引玩家(这个页面从公布了它的缺陷后已经被拿掉了)。在这段代码中,调用 randomize() 来在每副牌生成前生成一副随机牌。这个实现是用 Delphi 4 (一种 Pascal IDE)构建的,随机数生成器的种子是按照系统时钟,用午夜后的毫秒数选取的。这意味着随机数生成器的输出是容易预测的。正如我们所讨论的,可预测的随机数生成器是一个很严重的安全性问题。

ASF 软件中使用的洗牌算法总是以一副有序的牌开始,然后生成用来记录纸牌的随机数序列。在实际的纸牌中,有 52!(大约是 2 226)种可能的、不同的洗牌顺序。回想前面所讲,用于 32 位的随机数生成器的种子必须是 32 位数字,这意味者只有刚刚超过 40 亿个的可能的种子。由于在每次洗牌前,纸牌要重新初始化并重新生成生成器的种子,而这个算法只有 40 亿个可能的洗牌方法。40 亿可能的洗牌方法远远少于 52!。

使事情更糟糕的是,这个有缺陷的算法使用 Pascal 函数 Randomize() 来挑选用于随机数生成器的种子。这个特定的 Randomize() 函数基于午夜后的毫秒数挑选种子。一天之内只有 86,400,000 个毫秒。由于这个数字被用作用于随机数生成器的种子,所以现在可能的洗牌方法数量减少到 86,400,000。86,400,000 远远少于 40 亿。但这还不是最糟的。还有更糟糕的。

系统时钟种子甚至进一步使 Reliable Software Technologies 减少可能的洗牌方法数。通过将他们的程序与生成伪随机数的服务器上的系统时钟同步,使得可能组合的数量减少到大约 200,000 种可能性。在这之后,系统就变得不堪一击,由于在这个微小集合的洗牌方法中进行搜索是不费吹灰之力的,这可以在 PC 机上实时地做到。

发现这个薄弱环节的 RST 开发的工具需要知道纸牌中的五张牌。基于这五张牌,程序从几十万个可能的洗牌方法中搜索并推出最优匹配。在 Texas Hold 'em Poker 情形中,这意味着程序将欺骗性玩家手中的两张牌作为输入,再加上三个玩家的第一张翻开的牌(发牌)。这五张牌在第一圈下赌注后是已知的,这足够确定(实时,在玩牌期间)确切的洗牌方法。这幅图显示了 RST 揭开这个赌博事实的图形界面。左上角的 Site Parameters 框是用来同步时钟的。右上角的 Game Parameters 框是用来输入这五张牌和启动搜索的。这副图是程序已经确定所有牌之后捕捉的屏幕。欺骗性的攻击者提前知道谁手中拿有什么牌,余下的发牌会象什么样,以及谁将会胜利。

Reliable Software Technology 的因特网扑克牌揭秘的界面

一旦程序知道了这五张牌,它会生成洗牌顺序,直到发现包含这五张已知牌正确次序的洗牌顺序。由于 Randomize() 函数是基于服务器的系统时间,所以,合理地、并有一定程度精确性地猜出启始种子不是很困难的。(您越接近,所经历的可能洗牌顺序也越少。)虽然这是一个令人吃惊的结果:一旦发现了正确的种子之后,有可能在几秒之内将这个揭秘计划与服务器程序同步。这个公布出来的、实际的同步允许程序决定随机数生成器所使用的种子,并在一秒之内识别出所有以后游戏中使用的洗牌顺序!

撇开技术细节,该揭秘收到了令人惊叹的新闻效应。这个新闻效应强调了在此类发现中人为的一面。请访问 RST 网站(已在 参考资料中列出)来了解原始的新闻发布、CNN 录像片段和 纽约时报的新闻。

如何读“秘密的”Netscape 消息

在另一个独立的但也同等重要的开发中,Ian Goldberg 和 David Wagner 从 1996 年 1 月开始的研究,演示了随机数对于密码安全性是如何的重要(请参阅 参考资料)。他们研究出来的这份揭秘显示了 Netscape 早期的 SSL 实现之一存在着严重的缺陷,这使得有可能解密编码的通信数据。当然,这个缺陷已经成为历史,但带来的教训却足以让我们不能再重蹈覆辙。

SSL 用一个密钥来加密消息(象大多数密码算法一样)。这个想法是创建一个密钥,这个密钥是只有消息发送方和接受方知道的大的随机数。正如洗牌这个情形一样,如果密钥是可预测的,那么整个系统会象一幢纸牌做的房子一样坍塌掉。很简单,密钥必须要从不可预测的来源得到。(听起来很类似?)

Netscape 的问题是它们选择了一种不好的方法来给伪随机数生成器播种。它们的种子完全可由一天的时间、进程标识和父进程标识来决定。这不是很好。

使用与被攻击的浏览器相同的机器(如在多用户机器上)的攻击者可以通过使用 ps 命令很容易发现进程标识(UNIX 环境下)。Goldberg 和 Wagner 还详细说明了攻击可以在不需要知道进程标识情况下(基于通常每个进程只有 15 位的事实)实现。剩下的工作就是猜测一天的时间。嗅探器可以用来偷取通过包的精确时间,利用这就能够猜测运行 Netscape 系统上的的时间。在一秒之内就可以获得,这相当的容易,并且也可以在毫秒的级别范围内实时地破解密码(正如扑克揭秘所演示的)。

Goldberg 和 Wagner 使用这种攻击方式成功地攻击了 Netscape 版本 1.1。40 位的国际版本和 128 位的国内版都可被攻破。给我们带来的教训很简单:如果需要生成密码术中使用的随机数,要小心谨慎。

在本文的下两个专题中,将要提供关于在硬件方面和软件方面如何正确地处理随机数生成的一些指示(以及大量代码)。如果您想提前了解这些,那么很好,可以看 "Randomness Recommendations for Security" (RFC 1750) 这本资料,但它有点枯燥。另外一本值得看的资料是 Bruce Schneier 的优秀作品 "Applied Cryptography"