php mt_rand()安全

这个知识点以前没有遇到过,在上上周的西湖论剑上有一道题涵盖了这个知识点,当时明文攻击都没有成功…现在电脑上已经有三个压缩解压工具啦(crying)

rand()、mt_rand()

rand()官方文档:

int rand( void)

int rand( int $min, int $max)
如果没有提供可选参数 min 和 max,rand() 返回 0 到 getrandmax() 之间的伪随机整数。例如想要 5 到 15(包括 5 和 15)之间的随机数,用 rand(5, 15)。
Note: 在某些平台下(例如 Windows)getrandmax() 只有 32767。如果需要的范围大于 32767,那么指定 min 和 max 参数就可以生成更大的数了,或者考虑用 mt_rand() 来替代之。
更新日志
4.2.0 随机数发生器自动进行播种。
---

mt_rand()官方文档:

int mt_rand( void)

int mt_rand( int $min, int $max)
很多老的 libc 的随机数发生器具有一些不确定和未知的特性而且很慢。PHP 的 rand() 函数默认使用 libc 随机数发生器。mt_rand() 函数是非正式用来替换它的。该函数用了 » Mersenne Twister 中已知的特性作为随机数发生器,它可以产生随机数值的平均速度比 libc 提供的 rand() 快四倍。
如果没有提供可选参数 min 和 max,mt_rand() 返回 0 到 mt_getrandmax() 之间的伪随机数。例如想要 5 到 15(包括 5 和 15)之间的随机数,用 mt_rand(5, 15)。
更新日志
4.2.0 随机数发生器自动进行播种。
在windows下测试的rand()默认的最大值为32767,mt_rand()默认的最大值为2147483647

真/伪随机数

注意,在官方文档描述中,上面两个函数生成的随机数都是伪随机数

在大一学的C++书中也提到了生成随机数的函数rand():

rand()函数(在调用它时头文件要包含< cstdlib >)可以用来产生随机数,但这不是真正意义上的随机数,是一个伪随机数,它是以一个数(可以称它为种子(seed))为基准,以某个递推公式算出来的一系列数(随机序列)。当计算机开机后,这个种子的值是确定的,为了改变这个种子的值,C++提供了srand()函数,功能是初始化随机产生器。
可以将time(0),即时间戳作为参数传到srand()函数中,这样就能产生真随机数了

在php官方文档描述的是随机数发生器自动进行播种,那么到底是怎样进行播种的呢?参考的文章总结如下:

php的自动播种发生在php cgi进程中第一次调用mt_rand()的时候。跟访问的页面无关,只要是同一个进程处理的请求,都会共享同一个最初自动播种的种子。
### php_mt_seed

在php中,可以使用mt_srand()进行播种

官方文档:

mt_srand — 播下一个更好的随机数发生器种子

说明

void mt_srand([ int $seed] )

用 seed 来给随机数发生器播种。 没有设定 seed 参数时,会被设为随时数。
Note: 自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 给随机数发生器播种 ,因为现在是由系统自动完成的。
可以使用工具爆破种子:https://github.com/lepiaf/php_mt_seed
git  clone https://github.com/lepiaf/php_mt_seed
cd php_mt_seed
make

现在做个测试:

在phpstudy网站根目录,建立index.php文件和rand.php文件,在rand.php中写入:

1
2
3
4
5
6
<?php

for($i=0; $i<10; $i++){
echo mt_rand()."<br>";
}
?>

现在,访问127.0.0.1/rand.php,,显示

1642237942
1234263933
308913717
1749113931
847118977
1982980330
1853851711
1405394624
1052363538
195010783

那么使用上面的工具爆破种子:

time ./php_mt_seed 1642237942

发现爆出了一个种子2972913307

在index.php中写入:

1
2
3
4
5
6
7
8
<?php

mt_srand(2972913307);

for($i=0; $i<20; $i++){
echo mt_rand()."<br>";
}
?>

访问127.0.0.1/index.php,输出:

1642237942
1234263933
308913717
1749113931
847118977
1982980330
1853851711
1405394624
1052363538
195010783
54559389
1040500600
1360486511
1429697000
214486007
1268030276
298349955
1246753272
1381254604
1935697126

发现前十位随机数是一样的,则预测到了下几次的随机数;

再次刷新rand.php,会发现显示的十个随机数与index.php显示的后十个随机数是一样的

GWCTF2019 枯燥的抽奖

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
header("Content-Type: text/html;charset=utf-8");
session_start();
if(!isset($_SESSION['seed'])){
$_SESSION['seed']=rand(0,999999999);
}

mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
$str_show = substr($str, 0, 10);
echo "<p id='p1'>".$str_show."</p>";


if(isset($_POST['num'])){
if($_POST['num']===$str){x
echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>";
}
else{
echo "<p id=flag>没抽中哦,再试试吧</p>";
}
}
show_source("check.php");

这里不是直接给了种子,而是给出了随机值的前十位;我们可以通过已知随机数将伪随机数转换为php_mt_seed可识别的数据:

1
2
3
4
5
6
7
8
9
10
11
str1='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
str2='xD8VmkVo8o'
str3 = str1[::-1]
length = len(str2)
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)

之后爆破出种子;这里使用https://www.openwall.com/php_mt_seed4.0这个工具,因为上面的工具爆不出

最后输出整个随机数即可

1
2
3
4
5
6
7
8
9
10
11
<?php
$seed=276441760;
mt_srand($seed);
$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;

要注意,php5与php7的随机数采用的算法不同,生成的结果也不同


参考文章:

https://mp.weixin.qq.com/s/3TgBKXHw3MC61qIYELanJg