基础介绍
文中若有不当之处还请各位大佬多多点评
我的博客:https://whoamianony.top/
公众号:WHOAMIAnony
随机数并不随机
首先需要声明的是,计算机不会产生绝对随机的随机数,计算机只能产生“伪随机数”。其实绝对随机的随机数只是一种理想的随机数,即使计算机怎样发展,它也不会产生一串绝对随机的随机数。计算机只能生成相对的随机数,即伪随机数。
伪随机数并不是假随机数,这里的“伪”是有规律的意思,就是计算机产生的伪随机数既是随机的又是有规律的。 怎样理解呢?产生的伪随机数有时遵守一定的规律,有时不遵守任何规律;伪随机数有一部分遵守一定的规律;另一部分不遵守任何规律。比如“世上没有两片形状完全相同的树叶”,这正是点到了事物的特性,即随机性,但是每种树的叶子都有近似的形状,这正是事物的共性,即规律性。从这个角度讲,你大概就会接受这样的事实了:计算机只能产生伪随机数而不能产生绝对随机的随机数。
真随机数和伪随机数的概念
- 真随机数发生器:英文为:true random number generators ,简称为:TRNGs,是利用不可预知的物理方式来产生的随机数。
- 伪随机数发生器:英文为:pseudo-random number generators ,简称为:PRNGs,是计算机利用一定的算法来产生的。
PHP的伪随机数有关的两个函数
mt_rand()
mt_rand() 函数使用 Mersenne Twister 算法返回随机整数(随机数生成器)。
语法
mt_rand(min,max)
说明
如果没有提供可选参数 min 和 max,mt_rand() 返回 0 到 RAND_MAX 之间的伪随机数。
很多老的 libc 的随机数发生器具有一些不确定和未知的特性而且很慢。PHP 的 rand() 函数默认使用 libc 随机数发生器。mt_rand() 函数是非正式用来替换它的。该函数用了 Mersenne Twister 中已知的特性作为随机数发生器,它可以产生随机数值的平均速度比 libc 提供的 rand() 快四倍。
例子
在本例中,我们会返回一些随机数:
<?php
echo(mt_rand());
echo(mt_rand());
echo(mt_rand(10,100));
?>
输出类似:
3150906288
513289678
35
mt_srand()
mt_srand() 播种 Mersenne Twister 随机数生成器。
语法
mt_srand(seed)
参数 seed:规定播种值、用 seed 来给随机数发生器播种。
例子
在本例中,我们将播种随机数生成器:
<?php
mt_srand(mktime());
echo(mt_rand());
?>
输出类似:
1132656473
这段代码的意思是,是通过mt_srand分发seed种子,然后种子有了后,才能靠mt_rand()来生成随机数。
为什么随机数要用种子播种,对种子的理解
首先我们要知道,计算机不能产生绝对的随机数,只能产生伪随机数。伪就是有规律的意思。伪随机数就是说计算机产生的随机数是有规律的。那么计算机是怎么产生随机数的?当然是通过算法,这个算法是有映射关系的,如我放进1,他会出来一个特定的数
RAND_SEED=(RAND_SEED*123+59)%65536;
这是某个系统的随机数算法。
我们可以把这个算法看成是一个黑盒子,你放进一个数,就会出来一个特定的数,并把这个数当做下一次的种子在放进去。在这里,你第一次放进去的数就是随机数种子,也就是说随机种子(Random Seed)就是这些随机数的初始值。
系统实现随机数是把当前的系统时间放进去,每次时间都不一样,所以可以实现每次出来的数都不一样。如果你每次都放进一样的种子,生成的随机数列就是一样的了。
注释:自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 函数给随机数发生器播种了,随机数生成器可以自动播种,因此没有必要使用该函数。
PHP伪随机数安全
可预测性
mt_rand()并不是一个真随机数生成函数,实际上绝大多数编程语言中的随机数函数生成的都是伪随机数。
伪随机是由可确定的函数,通过一个种子(常用时钟)播种,产生的伪随机数。这意味着:如果知道了种子,或者已经产生的随机数,或者已知产生的随机数的一部分,都可能获得接下来随机数序列的信息,知道了你的随机数序列,就可以确定你的种子值。 (可预测性)。
简单假设一下 mt_rand()内部生成随机数的函数为: rand = seed+(i10) 其中 seed 是随机数种子, i 表示第几次调用这个随机数函数。当我们同时知道 i 和 rand 两个值的时候,就能很容易的算出seed的值来。比如 rand=21 , i=2 代入函数 21=seed+(210) 就能得到 seed=1 。是不是很简单,当我们拿到seed之后,就能计算出当 i 为任意值时候的 rand 的值了。
PHP的自动播种
我们已经知道每一次mt_rand()被调用都会根据seed和当前调用的次数i来计算出一个伪随机数。而且seed是自动播种的:
Note: 自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 给随机数发生器播种,因为现在是由系统自动完成的。
那么问题就来了,到底系统自动完成播种是在什么时候,如果每次调用mt_rand()都会自动播种那么破解seed也就没意义了。
不幸的是,php每次调用mt_rand()函数时,都会先检查是否已经播种。如果已经播种就直接产生随机数,否则调用php_mt_srand来播种。也就是说每个php cgi进程期间,只有第一次调用mt_rand()会自动播种。接下来都会根据这个第一次播种的种子来生成随机数。
php_mt_seed
我们已经知道随机数的生成是依赖特定的函数,上面曾经假设为 rand = seed+(i*10) 。对于这样一个简单的函数,我们当然可以直接计算出一组解来,但 mt_rand() 实际使用的函数可是相当复杂且无法逆运算的。有效的破解方法其实是穷举所有的种子并根据枚举出的种子生成随机数序列再跟已知的随机数序列做比对来验证种子是否正确。php_mt_seed就是这么一个工具,它的速度非常快,跑完2^32位seed也就几分钟。 它可以根据单次mt_rand()的输出结果直接爆破出可能的种子,当然也可以爆破类似mt_rand(1,100)这样限定了MIN MAX输出的种子。
很多国内开发者在程序中使用了mt_rand()来生成安全令牌、核心加解密key等等导致严重的安全问题。
用到的是爆破,已经有写好的C脚本了。
这里简单的介绍下这个脚本咋用
种子爆破工具——php_mt_seed
下载地址:http://www.openwall.com/php_mt_seed
它可以根据单次mt_rand()的输出结果直接爆破出可能的种子,当然也可以爆破类似mt_rand(1,100)这样限定了MIN MAX输出的种子。
下载后,执行如下命令编译生成:
kali下,进入目录,make
具体使用
php_mt_seed的用法可能很简单,也可能很复杂,具体取决于用例的详细信息。这是一个最简单的用法示例:
首先使用PHP生成一个“随机”数字,例如:
$ php5 -r 'mt_srand(1234567890); echo mt_rand(),"\n";'
1328851649
然后运行cracker(在此示例中,在与上面用于构建的系统相同的系统上):
$ time ./php_mt_seed 1328851649
Pattern: EXACT
Version: 3.0.7 to 5.2.0
Found 0, trying 0xfc000000 - 0xffffffff, speed 16261.0 Mseeds/s
Version: 5.2.1+
Found 0, trying 0x1e000000 - 0x1fffffff, speed 91.8 Mseeds/s
seed = 0x1fd65f9a = 534142874 (PHP 7.1.0+)
Found 1, trying 0x26000000 - 0x27ffffff, speed 91.9 Mseeds/s
seed = 0x273a3517 = 658126103 (PHP 5.2.1 to 7.0.x; HHVM)
Found 2, trying 0x48000000 - 0x49ffffff, speed 91.9 Mseeds/s
seed = 0x499602d2 = 1234567890 (PHP 5.2.1 to 7.0.x; HHVM)
seed = 0x499602d2 = 1234567890 (PHP 7.1.0+)
Found 4, trying 0xfe000000 - 0xffffffff, speed 91.9 Mseeds/s
Found 4
real 0m47.028s
user 6m15.211s
sys 0m0.015s
php_mt_seed首先搜索旧版PHP 3.0.7至5.2.0的种子,通常只需一秒钟即可完成。然后,它会同时搜索PHP 5.2.1至7.0.x和PHP 7.1.0+的种子,这需要一段时间。
下面演示一个复杂的用法示例:
php_mt_seed 在其命令行上可以有1、2、4或更多的数字。数字指定了对 mt_rand()函数输出的约束,如mt_rand(0,50)。
当仅使用一个数字调用时,这是mt_rand函数的第一个用来爆破种子的输出。
当用2个数字调用时,它们是第一个mt_rand()的输出应落入的范围(最小和最大,按该顺序)。
当用4个数字调用时,前2个给出第一个mt_rand()输出的界限,后两个2给出传递到mt_rand()的范围。
实例——[GWCTF 2019]枯燥的抽奖
进入题目
是个猜字符的游戏,并且告诉你了一部分值:utP4yKDDVV
,估计是已知产生的随机数的一部分。
F12 查看源码,发现check.php:
进入check.php,给出了你源码:
可知源码的逻辑是,先产生一个0到999999999之间的随机整数作为随机数种子被mt_srand()函数播种。然后在$str_long1
这一长串字符串中用mt_rand()来随机取出20个字符构成字符串$str
并输出$str
的前十位给你。最后,将你猜的输入的值拼上给你的值与$str
进行强类型比较,对比成功则返回flag。可见,如果最后能POST正确的20位,获得flag。那么关键就是,要根据给你的10位密码,反推出mt_srand的种子是什么,从而依葫芦画瓢构造私钥。
接下来我们用php_mt_seed工具对随机数种子进行爆破。
先用脚本将伪随机数转换成php_mt_seed可以识别的数据格式:
str1='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
str2='utP4yKDDVV'
str3 = str1[::-1] # 倒序
length = len(str2) # 10
res=''
for i in range(len(str2)):
for j in range(len(str1)):
if str2[i] == str1[j]:
res+=str(j)+' '+str(j)+' '+'0'+' '+str(len(str1)-1)+' '
break
print res
// 前两个str(j)代表第一个mt_rand()输出的界限,后两个参数0和str(len(str1)-1)表示传递到 mt_rand()的范围为0到61
得到:
20 20 0 61 19 19 0 61 51 51 0 61 30 30 0 61 24 24 0 61 46 46 0 61 39 39 0 61 39 39 0 61 57 57 0 61 57 57 0 61
将这一串拿到工具里去使用
爆破出伪随机数和php版本
./php_mt_seed 20 20 0 61 19 19 0 61 51 51 0 61 30 30 0 61 24 24 0 61 46 46 0 61 39 39 0 61 39 39 0 61 57 57 0 61 57 57 0 61
爆破的到种子为:206396968,如下改写源码,便可以得到完整的字符串
<?php
mt_srand(206396968);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
echo $str;
?>
拿着得到得字符转提交即可得到flag: