深入理解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中使用模板的方法
May 24 PHP
PHP 一个页面执行时间类代码
Mar 05 PHP
无法载入 mcrypt 扩展,请检查 PHP 配置终极解决方案
Jul 18 PHP
基于Zend的Config机制的应用分析
May 02 PHP
ThinkPHP CURD方法之table方法详解
Jun 18 PHP
php多个文件及图片上传实例详解
Nov 10 PHP
php判断两个浮点数是否相等的方法
Mar 14 PHP
php判断文件夹是否存在不存在则创建
Apr 09 PHP
Yii框架实现图片上传的方法详解
May 20 PHP
使用PHPStorm+XDebug搭建单步调试环境
Nov 19 PHP
PHP排序算法之归并排序(Merging Sort)实例详解
Apr 21 PHP
关于Curl在Swoole协程中的解决方案详析
Sep 12 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
Discuz7.2版的faq.php SQL注入漏洞分析
2014/08/06 PHP
PHP经典面试题集锦
2015/03/19 PHP
php获取从百度、谷歌等搜索引擎进入网站关键词的方法
2015/07/08 PHP
php实现过滤字符串中的中文和数字实例
2015/07/29 PHP
php常用图片处理类
2016/03/16 PHP
PHP实现将多个文件压缩成zip格式并下载到本地的方法示例
2018/05/23 PHP
详解json在php中的应用
2018/09/30 PHP
PHP实现支持CURL字符串证书传输的方法
2019/03/23 PHP
RGB颜色值转HTML十六进制(HEX)代码的JS函数
2009/04/25 Javascript
利用jquery操作select下拉列表框的代码
2010/06/04 Javascript
解决Extjs 4 Panel作为Window组件的子组件时出现双重边框问题
2013/01/11 Javascript
js改变鼠标的形状和样式的方法
2014/03/31 Javascript
jQuery中is()方法用法实例
2015/01/06 Javascript
jquery实现多屏多图焦点图切换特效的方法
2015/05/04 Javascript
jQuery插件jRumble实现网页元素抖动
2015/06/05 Javascript
Jquery基础教程之DOM操作
2015/08/19 Javascript
浅谈JS原型对象和原型链
2016/03/02 Javascript
javascript加载xml 并解析各节点的值(实现方法)
2016/10/12 Javascript
jquery拼接ajax 的json和字符串拼接的方法
2017/03/11 Javascript
详解NodeJS框架express的路径映射(路由)功能及控制
2017/03/24 NodeJs
微信小程序中上传图片并进行压缩的实现代码
2018/08/28 Javascript
angular中的post请求处理示例详解
2020/06/30 Javascript
在Python中使用异步Socket编程性能测试
2014/06/25 Python
利用Python2下载单张图片与爬取网页图片实例代码
2017/12/25 Python
python实现从文件中读取数据并绘制成 x y 轴图形的方法
2018/10/14 Python
Python功能点实现:函数级/代码块级计时器
2019/01/02 Python
python科学计算之narray对象用法
2019/11/25 Python
Python HTMLTestRunner可视化报告实现过程解析
2020/04/10 Python
Python实现UDP程序通信过程图解
2020/05/15 Python
基于Python脚本实现邮件报警功能
2020/05/20 Python
基于python图书馆管理系统设计实例详解
2020/08/05 Python
HTML5如何实现元素拖拽
2016/03/11 HTML / CSS
日本非常有名的内衣丝袜品牌:GUNZE
2017/01/06 全球购物
汽车专业人才自我鉴定范文
2013/12/29 职场文书
Python中glob库实现文件名的匹配
2021/06/18 Python
vue动态绑定style样式
2022/04/20 Vue.js