真正的随机数是使用物理现象产生的:比如掷钱币、骰子、转轮、使用电子元件的噪音、核裂变等等。这样的随机数发生器叫做物理性随机数发生器,虽然很可靠,但是使用计算机很难实现,它们的缺点是技术要求比较高。
在实际应用中往往使用伪随机数就足够了。这些数列看似是随机的数,实际上它们是通过一个固定的、可以重复的计算方法产生的。计算机产生的随机数有很长的周期性。它们不真正地随机,因为它们实际上是可以计算出来的,但是它们具有类似于随机数的统计特征。这样的发生器叫做伪随机数发生器。
伪随机数又有强弱之分。强伪随机数一般是指相对难以猜解的随机数,比如服务器占用的内存数量作为随机数;而弱伪随机数是指相对容易猜解的随机数呢,典型的例子是当前的时间戳等。
C、C++、Java等程序语言和软件中都有对应的随机数生成函数或者类。以我们最常用的Java语言为例,强伪随机数RNG实现java.security.SecureRandom类,该类使用临时文件夹中大小,线程休眠时间等的值作为随机数种子;而弱伪随机数实现PRNG java.util.Random类,默认使用当前时间作为种子,并且采用线性同余法计算下一个随机数。
Random r = new Random(10000); //10000作为seed,默认使用当前时间作为seed
for (int i=0;i<5;++i)
{
System.out.println(r.nextInt());
}
以上这段代码,无论你怎么跑都会打印出以下结果:
-498702880
-858606152
1942818232
-1044940345
1588429001
这是一个稳定的结果。这就是由于线性同余法带来的后果。那么,在我们的程序,如果使用Random类生成一个随机数,事实上很容易通过上一个产生的随机数来推断下一个随机数。
接下来,我们来分析一些常用的随机数应用场景,并且分析一下出错的原因。
很多账号体系都有一个找回密码功能,找回密码时给手机发送的验证码,给邮箱发送的验证码或者重置密码链接,以上种种都使用了伪随机数。
下面以某网站通过邮箱重置密码链接找回密码为例,通过页面操作之后,会在密保邮箱中发现以下重置密码的链接:
http://www.xxx.com/findpwd/setpwdfromemail?vc=2ABB36620A927644607491393EF0D5EF&u=xxx%40gmail.com
通过分析,我们发现,vc=2ABB36620A927644607491393EF0D5EF是一串md值,解开之后值是1339744000,是个unix时间戳!那么可以猜测,用户取回密码时产生一个时间戳与帐号绑定,那么修改这个用户密码只需知道这个时间戳就可以。况且,一般服务器时间都是跟标准时间同步,也就是说unix时间戳是可以预测的。我们可以通过暴力破解遍历当前标准时间+一个网络延迟来进行暴力破解。
伪随机数的应用里,验证码是另外一种典型应用。对于安全而言,验证码是一个非常有效的保护机制和人机区分机制,可以保障口令不被暴力破解,可以防止刷票,刷屏,重复提交恶意数据等。除了作为验证码之外,类似的应用还存在于一些活动的优惠券或者兑换码,如果兑换码设计不当,很容易被破解而破坏活动的公平性。
总结一下,使用随机数的场景需要注意以下几点:
不要使用时间戳作为随机数
保证不同用处的随机数使用不同的种子
对于安全性要求高的随机数,使用强伪随机数产生