之前在身边有很多学PHP的朋友写一些小程序的时候,很多时候会使用PHP随机数函数rand()和mt_rand()函数去生成随机数
可是,随机数真的随机吗?这篇文章讲从多个实例中探讨随机数,当然,有写作不当的地方,还望斧正!
关于随机函数rand()和mt_rand()
rand()和mt_rand()两个函数皆是PHP中生成随机数的函数,可相比之下,mt_rand()的生成速度缺是rand()的四倍!在没有参数的情况下,两者生成的数值范围也是不一样的。
echo mt_getrandmax()." and ".getrandmax();
可以看到页面上的输出,mt_rand()相比较rand()默认取值的范围更大。而且经过测试,在不同的PHP版本中,rand随机数种子相同的时候,随机数缺不相同!
左边web页面使用的是PHP7.0.12,而右边的Powershell却是使用的PHP5.2.17。从上所述,只有mt_rand()所生成的随机数是无版本差异的!
而这两个生成随机数的时候,是可以通过srand()和mt_srand()设置随机数的种子
1 <?php 2 mt_srand(666); 3 srand(666); 4 echo "rand 函数在种子是666时产生的随机数序列:<br/>"; 5 for($i=1;$i<5;$i++){ 6 echo rand()."<br/>"; 7 } 8 echo '<br/>'; 9 echo "mt_rand 函数在种子是666时产生的随机数序列:<br/>"; 10 for($i=1;$i<5;$i++){ 11 echo mt_rand()."<br/>"; 12 } 13 ?>
运行如下图所示
当你发现,设置的种子为666的时候,不管你刷新多少次,页面中生成的随机数值都不会变!
所以,当种子泄露的时候,随机数的安全就不堪一击了。
可有什么方法能够得到种子?
目前我们唯一能知道的就是随机数是不会变的,而之前有说过mt_srand()生成的随机数范围在0-2147483647之间,我们就可以通过最普通的爆破方式
去获取随机数的种子,这里推荐使用php_mt_seed,项目地址:http://www.openwall.com/php_mt_seed/
这里踩到个坑,生成后的随机数分为PHP7.x版本的和5.x版本,具体原因还不知道,但是在php7.x版本以上,mt_srand()函数定义的种子将不能超过int,也就是2147483647,但可以等于该值
所以,为了更好的研究,下面将会使用PHP5.2.17的版本
首先通过mt_rand()创建一个随机数,然后利用脚本爆破
php -r "echo mt_rand();"
爆破出来的种子所生成的随机数正好是一样的
那么,页面只播种一次,生成的多个随机数也会一样吗
事实证明,一次播种,全页不愁。
竟然已经了解了随机数的相关安全性,下面就放上一些CTF中,或者在某个CMS中出现的有关随机数的审计
审计案例①(mt_rand()安全性)
此案例是出自第三届陕西网络空间安全技术大赛
1 <?php 2 error_reporting(0); 3 function cpassword($pw_length = 10){ 4 $randpwd = ""; 5 for ($i = 0; $i < $pw_length; $i++) 6 { 7 $randpwd .= chr(mt_rand(33, 126)); 8 } 9 return $randpwd; 10 } 11 12 session_start(); 13 mt_srand(time()); 14 $pwd=cpassword(10); 15 if($pwd === $_GET['pwd']) 16 { 17 echo "Good job, you get the key is:".$pwd; 18 } 19 else{ 20 echo "Wrong!"; 21 } 22 23 $_SESSION['userLogin']=cpassword(32).rand(); 24 ?>
从给出的代码中可以看到
$pwd=cpassword(10);
程序通过时间戳播种,然后通过cpassword建立一个密码,并验证密码是否正确,否则就输出flag
通过之前研究发现mt_rand()生成的随机数是有固定的,只需要知道mt_srand(time());的结果行了
写个脚本,直接用time函数生成的时间戳设置种子,再发送数据内容过去,就能获取到flag
脚本如下
1 <?php 2 function do_get($url, $params) { 3 $url = "{$url}?" . http_build_query ( $params ); 4 $ch = curl_init (); 5 curl_setopt ( $ch, CURLOPT_URL, $url ); 6 curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, 1 ); 7 curl_setopt ( $ch, CURLOPT_CUSTOMREQUEST, 'GET' ); 8 curl_setopt ( $ch, CURLOPT_TIMEOUT, 60 ); 9 curl_setopt ( $ch, CURLOPT_POSTFIELDS, $params ); 10 $result = curl_exec ( $ch ); 11 curl_close ( $ch ); 12 return $result; 13 } 14 15 function cpassword($pw_length = 10){ 16 $randpwd = ""; 17 for ($i = 0; $i < $pw_length; $i++) 18 { 19 $randpwd .= chr(mt_rand(33, 126)); 20 } 21 return $randpwd; 22 } 23 24 mt_srand(time()); 25 $pwd=cpassword(10); 26 echo 'send:'.$pwd.'<br/>'; 27 $url="http://localhost/index.php"; 28 $params=array('pwd'=>$pwd); 29 $result=do_get($url,$params); 30 echo 'result:'.json_encode($result); 31 ?>
发送的密码与服务器所生成的密码是一样的!
假如按照刚才的生成密码的函数
1 function cpassword($pw_length = 10){ 2 $randpwd = ""; 3 for ($i = 0; $i < $pw_length; $i++) 4 { 5 $randpwd .= chr(mt_rand(33, 126)); 6 } 7 return $randpwd; 8 }
这次给出生成后的密码,现在怎样才能得到mt_srand()设置的种子
生成的密码:
{|M}2x0:kW
按照程序执行流程,应该先得到每个字符在chr(33)~chr(126)中的位置索引
1 <?php 2 $str = "!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~"; 3 $ss = "{|M}2x0:kW"; 4 for($i=0;$i<strlen($ss);$i++){ 5 $pos = strpos($str,$ss[$i]); 6 echo $pos." ".$pos." "."0 ".(strlen($str)-1)." "; 7 //整理成方便 php_mt_seed 测试的格式 8 //php_mt_seed VALUE_OR_MATCH_MIN [MATCH_MAX [RANGE_MIN RANGE_MAX]] 9 } 10 ?>
通过这个脚本换成方便php_mt_seed测试的格式
90 90 0 93 91 91 0 93 44 44 0 93 92 92 0 93 17 17 0 93 87 87 0 93 15 15 0 93 25 25 0 93 74 74 0 93 54 54 0 93
最终得到种子数1555747516
审计案例②(rand()函数安全性)
1 <?php 2 include('config.php'); 3 session_start(); 4 5 if($_SESSION['time'] && time() - $_SESSION['time'] > 60){ 6 session_destroy(); 7 die('timeout'); 8 } else { 9 $_SESSION['time'] = time(); 10 } 11 12 echo rand(); 13 if(isset($_GET['go'])){ 14 $_SESSION['rand'] = array(); 15 $i = 5; 16 $d = ''; 17 while($i--){ 18 $r = (string)rand(); 19 $_SESSION['rand'][] = $r; 20 $d .= $r; 21 } 22 echo md5($d); 23 }else if(isset($_GET['check'])){ 24 if($_GET['check'] === $_SESSION['rand']){ 25 echo $flag; 26 } else { 27 echo 'die'; 28 session_destroy(); 29 } 30 } else { 31 show_source(__FILE__); 32 } 33 ?>
这是出自0ctf的一道赛题
首先我们先了解rand()生成的随机数是有规律可循的
可以参考这篇文章:http://www.sjoerdlangkemper.nl/2016/02/11/cracking-php-rand/
state[i] = state[i-3] + state[i-31] return state[i] >> 1
下面这个程序形象的预测了rand()的随机数
1 <?php 2 $randStr = array(); 3 for($i=0;$i<60;$i++){ //先产生 32个随机数 4 $randStr[$i]=rand(0,30); 5 if($i>=31) { 6 //echo "$randStr[$i]=(".$randStr[$i-31]."+".$randStr[$i-3].") mod 31"."<br/>"; 7 if ($randStr[$i] == $randStr[$i-31]+$randStr[$i-3]){ 8 echo "$randStr[$i]=(".$randStr[$i-31]."+".$randStr[$i-3].") mod 31"."<br/>"; 9 } 10 } 11 } 12 ?>