简介
Java
的Random
类是最常用的一种随机数生成类,可以满足绝大部分场景的使用,但其并不能满足安全性方面的需求。
本文根据第三届强网杯一道密码学题目对其安全性进行分析。如果可以得到随机数生成器的第一个到第二个随机数,那么可以很容易的预测到此后的随机数。
问题
原题大致如下:
主要python
代码:
|
|
主要java
代码:
|
|
题目意思比较简单,使用java.util.Random
类默认种子生成随机数,请根据前两个随机数预测第三个随机数。
源码分析
由于编程语言生成的随机数都是伪随机数,所以一般而言要预测随机数需要破解其种子seed
,而此处使用默认种子,所以先查看Random
类源码分析其默认种子。
主要代码如下:
|
|
可以看到默认种子为seedUniquifier() ^ System.nanoTime()
,经简单测试可以发现其中seedUniquifier()
方法生成的是一个常数,而System.nanoTime()
返回的是一个纳秒,但此时间与当前时间无关,只能用于计算时间差,并且其长度很长,暴力破解难度很大。所以要破解默认种子几乎是不可能的。
接下来分析nextInt
方法:
|
|
可以看到nextInt
调用了next
方法,传递的参数是32,即Java-Int的长度。
再接着分析next
方法,其中核心代码为nextseed = (oldseed * multiplier + addend) & mask;
,在类定义开头处可以看到:
|
|
所以可以知道,新种子是根据旧种子乘以常数multiplier
再加上常数addend
,最后再取后48位。而while (!seed.compareAndSet(oldseed, nextseed))
只是为了保证新旧种子不一致,一般都为true
。
最后再将新种子无符号右移16位作为最终的返回值。
预测
所以,如果可以得到两个随机数,那么只需要暴力破解低16位,即可完成预测。
破解思路如下:
- 先将第一个随机数左移16位作为旧种子,此时种子的低16位为
0x0000
; - 暴力破解低16位,范围为
0x0000-0xffff
; - 暴力破解时每次生成一个新种子并无符号右移16位,判断是否与第二个随机数相等,如果相等则破解成功。
使用java代码生成三个随机数,使用前两个随机数预测第三个,预测的java代码如下:
|
|
测试多次可以看到均能正确预测。
此处需要注意的是,由于Java的Int是使用补码形式保存的,48位的long无符号右移16位并转换为int会产生负数,所以如果使用其他语言时需要注意此情况。由于正数补码位其本身,所以当几个随机数均为正数时预测不会有问题;但负数的补码为其绝对值取反再加1,所以需要进行处理。
预测的python3
代码如下:
|
|
此外,还有一点需要注意的是,在进行破解时,可能存在多解的情况,不过由于只需要尝试2^16
次,遇到此情况概率不大,故上述代码没有考虑。
其他方法
原题只考察了nextInt
方法,但在java.util.Random
类中,除了常用的nextInt
方法,还有其他几个常用方法,分析结果如下:
nextLong():
123public long nextLong() {return ((long)this.next(32) << 32) + (long)this.next(32);}简单分析可以发现,
nextLong()
生成的随机数是第一个nextInt
左移32位再加上第二个nextInt
,所以预测nextLong()
条件更加简单,只需要知道任意一个随机数即可预测后续的随机数,完全没有任何安全性。预测的java代码如下:
|
|
nextBoolean():
123public boolean nextBoolean() {return this.next(1) != 0;}可以看到这里调用了
this.next(1)
,也就是将48位的新种子丢弃掉低47位,由于丢弃数据太多,所以无法暴力破解。nextFloat():
123public float nextFloat() {return next(24) / ((float)(1 << 24));}
可以看出此方法基本与nextInt
方法相同,不过是将32位改成了24位,再除以一个常数。
预测的java代码如下:
|
|
这里由于预测空间变成了2^24
,存在多解的可能性变大,所以一般需要尝试多次(大约1-3次)才能成功。
nextDouble():
1234private static final double DOUBLE_UNIT = 1.1102230246251565E-16D;public double nextDouble() {return (((long)(next(26)) << 27) + next(27)) * DOUBLE_UNIT;}
可以看出基本上与nextLong
方法相似,只是位数上有所区别,并且最终乘以了一个常数。所以依然只需要知道一个随机数即可预测出后续的所有随机数。
预测的java代码如下:
|
|
除了这几个常用的方法外,java.util.Random
类还有一些方法,此处不再分析。
总结
java.util.Random
类的默认种子仅与System.nanoTime()
有关,而此方法产生的时间与系统当前时间无关,破解难度较大,所以使用默认种子生成的第一个随机数是很难预测到的。
但是对于nextLong
和nextDouble
方法,只需要知道任意一个随机数即可预测后续随机数;而对于nextInt
和nextFloat
,如果同时泄漏第一个和第二个随机数,那么后面的随机数也都是可预测的。
在Java
中,要使用安全的随机数应该使用java.Security.SecureRandom
类,即使使用相同的初始seed,其生成的结果也完全不同,可以满足随机数的不可预测性需求。而其他的如Math.random()
和ThreadLocalRandom
也都是不安全的,使用时需要特别注意。