深入理解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 相关文章推荐
删除无限级目录与文件代码共享
Jul 12 PHP
PHP 和 MySQL 开发的 8 个技巧
Oct 09 PHP
洪恩在线成语词典小偷程序php版
Apr 20 PHP
Android ProgressBar进度条和ProgressDialog进度框的展示DEMO
Jun 19 PHP
php简单的留言板与回复功能具体实现
Feb 19 PHP
PHP使用Session遇到的一个Permission denied Notice解决办法
Jul 30 PHP
php使用date和strtotime函数输出指定日期的方法
Nov 14 PHP
php下Memcached入门实例解析
Jan 05 PHP
PHP环境搭建的详细步骤
Jun 30 PHP
详解Yii实现分页的两种方法
Jan 14 PHP
深入理解Yii2.0乐观锁与悲观锁的原理与使用
Jul 26 PHP
laravel config文件配置全局变量的例子
Oct 13 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
mysql+php分页类(已测)
2008/03/31 PHP
PHP上传文件时文件过大$_FILES为空的解决方法
2013/11/26 PHP
PHP简单检测网址是否能够正常打开的方法
2016/09/04 PHP
PHP编程实现多维数组按照某个键值排序的方法小结【2种方法】
2017/04/27 PHP
JavaScript 组件之旅(四):测试 JavaScript 组件
2009/10/28 Javascript
创建公共调用 jQuery Ajax 带返回值
2012/08/01 Javascript
提升PHP安全:8个必须修改的PHP默认配置
2014/11/17 Javascript
jquery实现的省市区三级联动
2015/04/02 Javascript
用瀑布流的方式在网页上插入图片的简单实现方法
2016/09/23 Javascript
Windows系统下安装Node.js的步骤图文详解
2016/11/15 Javascript
JavaScript获取短信验证码(周期性)
2016/12/29 Javascript
JavaScrip关于创建常量的知识点
2017/12/07 Javascript
完美解决手机网页中输入框被输入法遮挡的问题
2017/12/19 Javascript
最后说说Vue2 SSR 的 Cookies 问题
2018/05/25 Javascript
react中实现搜索结果中关键词高亮显示
2018/07/31 Javascript
如何使用pm2快速将项目部署到远程服务器
2019/03/12 Javascript
ECharts地图绘制和钻取简易接口详解
2019/07/12 Javascript
简单文件操作python 修改文件指定行的方法
2013/05/15 Python
理解python多线程(python多线程简明教程)
2014/06/09 Python
利用python计算windows全盘文件md5值的脚本
2019/07/27 Python
python误差棒图errorbar()函数实例解析
2020/02/11 Python
python利用线程实现多任务
2020/09/18 Python
Python字节单位转换(将字节转换为K M G T)
2021/03/02 Python
美国户外烹饪产品购物网站:Outdoor Cooking
2020/01/10 全球购物
怎样从/向数据文件读/写结构
2014/11/23 面试题
数据库基础的一些面试题
2012/02/25 面试题
自我评价正确写法范文
2013/12/10 职场文书
白酒业务员岗位职责
2013/12/27 职场文书
教你打造完美的创业计划书
2014/01/06 职场文书
给分销商的致歉信
2014/01/14 职场文书
现场施工员岗位职责
2014/03/10 职场文书
2015元旦晚会主持词(开场白+结束语)
2014/12/14 职场文书
先进员工事迹材料
2014/12/20 职场文书
2015年清明节网上祭英烈留言寄语
2015/03/04 职场文书
预备党员转正意见
2015/06/01 职场文书
Spring Boot 启动、停止、重启、状态脚本
2021/06/26 Java/Android