深入理解PHP中mt_rand()随机数的安全


Posted in PHP onOctober 12, 2017

前言

在前段时间挖了不少跟mt_rand()相关的安全漏洞,基本上都是错误理解随机数用法导致的。这里又要提一下php官网manual的一个坑,看下关于mt_rand()的介绍:中文版^cn 英文版^en,可以看到英文版多了一块黄色的 Caution 警告

This function does not generate cryptographically secure values, and should not be used for cryptographic purposes. If you need a cryptographically secure value, consider using random_int(), random_bytes(), or openssl_random_pseudo_bytes() instead.

很多国内开发者估计都是看的中文版的介绍而在程序中使用了mt_rand()来生成安全令牌、核心加解密key等等导致严重的安全问题。

伪随机数

mt_rand()并不是一个 真·随机数 生成函数,实际上绝大多数编程语言中的随机数函数生成的都都是伪随机数。关于真随机数和伪随机数的区别这里不展开解释,只需要简单了解一点

伪随机是由可确定的函数(常用线性同余),通过一个种子(常用时钟),产生的伪随机数。这意味着:如果知道了种子,或者已经产生的随机数,都可能获得接下来随机数序列的信息(可预测性)。

简单假设一下 mt_rand()内部生成随机数的函数为: rand = seed+(i*10) 其中 seed 是随机数种子, i 是第几次调用这个随机数函数。当我们同时知道 i 和 rand 两个值的时候,就能很容易的算出seed的值来。比如 rand=21 , i=2 代入函数 21=seed+(2*10) 得到 seed=1 。是不是很简单,当我们拿到seed之后,就能计算出当 i 为任意值时候的 rand 的值了。

PHP的自动播种

从上一节我们已经知道每一次mt_rand()被调用都会根据seed和当前调用的次数i来计算出一个伪随机数。而且seed是自动播种的:

Note: 自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 给随机数发生器播种 ,因为现在是由系统自动完成的。

那么问题就来了,到底系统自动完成播种是在什么时候,如果每次调用mt_rand()都会自动播种那么破解seed也就没意义了。关于这一点manual并没有给出详细信息。网上找了一圈也没靠谱的答案 只能去翻源码^mtrand了:

PHPAPI void php_mt_srand(uint32_t seed)
{
 /* Seed the generator with a simple uint32 */
 php_mt_initialize(seed, BG(state));
 php_mt_reload();

 /* Seed only once */
 BG(mt_rand_is_seeded) = 1; 
}
/* }}} */

/* {{{ php_mt_rand
 */
PHPAPI uint32_t php_mt_rand(void)
{
 /* Pull a 32-bit integer from the generator state
 Every other access function simply transforms the numbers extracted here */

 register uint32_t s1;

 if (UNEXPECTED(!BG(mt_rand_is_seeded))) {
 php_mt_srand(GENERATE_SEED());
 }

 if (BG(left) == 0) {
 php_mt_reload();
 }
 --BG(left);

 s1 = *BG(next)++;
 s1 ^= (s1 >> 11);
 s1 ^= (s1 << 7) & 0x9d2c5680U;
 s1 ^= (s1 << 15) & 0xefc60000U;
 return ( s1 ^ (s1 >> 18) );
}

可以看到每次调用mt_rand()都会先检查是否已经播种。如果已经播种就直接产生随机数,否则调用php_mt_srand来播种。也就是说每个php cgi进程期间,只有第一次调用mt_rand()会自动播种。接下来都会根据这个第一次播种的种子来生成随机数。而php的几种运行模式中除了CGI(每个请求启动一个cgi进程,请求结束后关闭。每次都要重新读取php.ini 环境变量等导致效率低下,现在用的应该不多了)以外,基本都是一个进程处理完请求之后standby等待下一个,处理多个请求之后才会回收(超时也会回收)。

写个脚本测试一下

<?php
//pid.php
echo getmypid();
<?php
//test.php
$old_pid = file_get_contents('http://localhost/pid.php');
$i=1;
while(true){
 $i++;
 $pid = file_get_contents('http://localhost/pid.php');
 if($pid!=$old_pid){
 echo $i;
 break;
 }
}

测试结果:(windows+phpstudy)

apache 1000请求

nginx 500请求

当然这个测试仅仅确认了apache和nginx一个进程可以处理的请求数,再来验证一下刚才关于自动播种的结论:

<?php
//pid1.php
if(isset($_GET['rand'])){
 echo mt_rand();
}else{
 echo getmypid();
}
<?php
//pid2.php
echo mt_rand();
<?php
//test.php
$old_pid = file_get_contents('http://localhost/pid1.php');
echo "old_pid:{$old_pid}\r\n";
while(true){
 $pid = file_get_contents('http://localhost/pid1.php');
 if($pid!=$old_pid){
 echo "new_pid:{$pid}\r\n";
 for($i=0;$i<20;$i++){
  $random = mt_rand(1,2);
  echo file_get_contents("http://localhost/pid".$random.".php?rand=1")." ";
 }

 break;
 }
}

通过pid来判断,当新进程开始的时候,随机获取两个页面其中一个的 mt_rand() 的输出:

old_pid:972 new_pid:7752 1513334371 2014450250 1319669412 499559587 117728762 1465174656 1671827592 1703046841 464496438 1974338231 46646067 981271768 1070717272 571887250 922467166 606646473 134605134 857256637 1971727275 2104203195

拿第一个随机数 1513334371 去爆破种子:

smldhz@vm:~/php_mt_seed-3.2$ ./php_mt_seed 1513334371 Found 0, trying 704643072 - 738197503, speed 28562751 seeds per second seed = 735487048 Found 1, trying 1308622848 - 1342177279, speed 28824291 seeds per second seed = 1337331453 Found 2, trying 3254779904 - 3288334335, speed 28811010 seeds per second seed = 3283082581 Found 3, trying 4261412864 - 4294967295, speed 28677071 seeds per second Found 3

爆破出了3个可能的种子,数量很少 手动一个一个测试:

<?php
mt_srand(735487048);//手工播种
for($i=0;$i<21;$i++){
 echo mt_rand()." ";
}

输出:

前20位跟上面脚本获取的一模一样,确认种子就是 1513334371 。有了种子我们就能计算出任意次数调用mt_rand()生成的随机数了。比如这个脚本我生成了21位,最后一位是 1515656265 如果跑完刚才的脚本之后没访问过站点,那么打开 http://localhost/pid2.php 就能看到相同的 1515656265 。

所以我们得到结论:

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

php_mt_seed

我们已经知道随机数的生成是依赖特定的函数,上面曾经假设为 rand = seed+(i*10)  。对于这样一个简单的函数,我们当然可以直接计算(口算)出一个(组)解来,但 mt_rand() 实际使用的函数可是相当复杂且无法逆运算的。有效的破解方法其实是穷举所有的种子并根据种子生成随机数序列再跟已知的随机数序列做比对来验证种子是否正确。php_mt_seed^phpmtseed就是这么一个工具,它的速度非常快,跑完2^32位seed也就几分钟。它可以根据单次mt_rand()的输出结果直接爆破出可能的种子(上面有示例),当然也可以爆破类似mt_rand(1,100)这样限定了MIN MAX输出的种子(下面实例中有用到)。

安全问题

说了这么多,那到底随机数怎么不安全了呢?其实函数本身没有问题,官方也明确提示了生成的随机数不应用于安全加密用途(虽然中文版本manual没写)。问题在于开发者并没有意识到这并不是一个 真·随机数 。我们已经知道,通过已知的随机数序列可以爆破出种子。也就是说,只要任意页面中存在输出随机数或者其衍生值(可逆推随机值),那么其他任意页面的随机数将不再是“随机数”。常见的输出随机数的例子比如验证码,随机文件名等等。常见的随机数用于安全验证的比如找回密码校验值,比如加密key等等。一个理想中的攻击场景:

夜深人静,等待apache(nginx)收回所有php进程(确保下次访问会重新播种),访问一次验证码页面,根据验证码字符逆推出随机数,再根据随机数爆破出随机数种子。接着访问找回密码页面,生成的找回密码链接是基于随机数的。我们就可以轻松计算出这个链接,找回管理员的密码…………XXOO

实例

PHPCMS MT_RAND SEED CRACK致authkey泄露 雨牛写的比我好,看他的就够了

Discuz x3.2 authkey泄露 这个其实也差不多。官方已出补丁,有兴趣的可以自己去分析一下。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

PHP 相关文章推荐
PHP编实现程动态图像的创建代码
Sep 28 PHP
在PHP中养成7个面向对象的好习惯
Jan 28 PHP
javascript,php获取函数参数对象的代码
Feb 03 PHP
jQuery中的RadioButton,input,CheckBox取值赋值实现代码
Feb 18 PHP
php二分查找二种实现示例
Mar 12 PHP
ThinkPHP的Widget扩展实例
Jun 19 PHP
php5.3不能连接mssql数据库的解决方法
Dec 27 PHP
PHP生成器简单实例
May 13 PHP
PHP之预定义接口详解
Jul 29 PHP
PHP实现的DES加密解密实例代码
Apr 06 PHP
PHP微信开发之有道翻译
Jun 23 PHP
tp5递归 无限级分类详解
Oct 18 PHP
php表单习惯用的正则表达式
Oct 11 #PHP
彻底搞懂PHP 变量结构体
Oct 11 #PHP
利用php + Laravel如何实现部署自动化详解
Oct 11 #PHP
Laravel 5使用Laravel Excel实现Excel/CSV文件导入导出的功能详解
Oct 11 #PHP
laravel migrate初学常见错误的解决方法
Oct 11 #PHP
Laravel学习基础之migrate的使用教程
Oct 11 #PHP
ThinkPHP 在阿里云上的nginx.config配置实例详解
Oct 11 #PHP
You might like
用PHP实现小型站点广告管理(修正版)
2006/10/09 PHP
使用淘宝IP库获取用户ip地理位置
2013/10/27 PHP
php一个解析字符串排列数组的方法
2015/05/12 PHP
php eval函数一句话木马代码
2015/05/21 PHP
PHP实现的简单AES加密解密算法实例
2017/05/29 PHP
PHP 实现链式操作
2021/03/09 PHP
asp.net 30分钟掌握无刷新 Repeater
2011/09/16 Javascript
使用jQuery fancybox插件打造一个实用的数据传输模态弹出窗体
2013/01/15 Javascript
自己写了一个展开和收起的多更能型的js效果
2013/03/05 Javascript
jQuery的cookie插件实现保存用户登陆信息
2014/04/15 Javascript
两款JS脚本判断手机浏览器类型跳转WAP手机网站
2015/10/16 Javascript
javascript显示倒计时控制按钮的简单实现
2016/06/07 Javascript
BootStrap智能表单demo示例详解
2016/06/13 Javascript
connection reset by peer问题总结及解决方案
2016/10/21 Javascript
JavaScript利用正则表达式替换字符串中的内容
2016/12/12 Javascript
node.js调用Chrome浏览器打开链接地址的方法
2017/05/17 Javascript
详解webpack打包vue时提取css
2017/05/26 Javascript
vue中component组件的props使用详解
2017/09/04 Javascript
Vue结合SignalR实现前后端实时消息同步
2017/09/19 Javascript
vue.js中proxyTable 转发请求的实现方法
2018/09/20 Javascript
[02:40]DOTA2超级联赛专访430 从小就爱玩对抗性游戏
2013/06/18 DOTA
[00:15]TI9观赛名额抽取
2019/07/10 DOTA
Python基于dom操作xml数据的方法示例
2018/05/12 Python
使用html5 canvas创建太空游戏的示例
2014/05/08 HTML / CSS
超30万乐谱下载:Musicnotes.com
2016/09/24 全球购物
美国最大的家庭鞋类零售商之一:Shoe Carnival
2017/10/06 全球购物
英国现代家具和照明购物网站:Heal’s
2019/10/30 全球购物
武汉英思工程科技有限公司&ndash;ORACLE面试测试题目
2012/04/30 面试题
创文明城市标语
2014/06/16 职场文书
群众路线领导班子整改方案
2014/10/25 职场文书
投资入股合作协议书
2014/10/28 职场文书
奥巴马开学演讲观后感
2015/06/12 职场文书
党员读书活动心得体会
2016/01/14 职场文书
信息技术课教学反思
2016/02/23 职场文书
Python控制台输出俄罗斯方块的方法实例
2021/04/17 Python
spring IOC容器的Bean管理XML自动装配过程
2022/05/30 Java/Android