深入理解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新手上路(八)
Oct 09 PHP
推荐php模板技术[转]
Jan 04 PHP
关于Appserv无法打开localhost问题的解决方法
Oct 16 PHP
基于python发送邮件的乱码问题的解决办法
Apr 25 PHP
PHP读取文件内容后清空文件示例代码
Mar 18 PHP
Linux下从零开始安装配置Nginx服务器+PHP开发环境
Dec 21 PHP
thinkPHP简单导入和使用阿里云OSSsdk的方法
Mar 15 PHP
yii2中LinkPager增加总页数和总记录数的实例
Aug 28 PHP
Yii2框架可逆加密简单实现方法
Aug 25 PHP
PHP简单实现模拟登陆功能示例
Sep 15 PHP
Laravel 加载第三方类库的方法
Apr 20 PHP
PHP封装的非对称加密RSA算法示例
May 28 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编写和读取XML的几种方式
2013/01/12 PHP
php封装的mongodb操作类代码
2017/08/06 PHP
thinkPHP通用控制器实现方法示例
2017/11/23 PHP
postman的安装与使用方法(模拟Get和Post请求)
2018/08/06 PHP
php实现微信企业转账功能
2018/10/02 PHP
PHP实现递归的三种方法
2020/07/04 PHP
让回调函数 showResponse 也带上参数的代码
2007/08/13 Javascript
jquery 简短右键菜单 多浏览器兼容
2010/01/01 Javascript
点击弹出层外区域关闭弹出层jquery特效示例
2013/08/25 Javascript
Node.js异步I/O学习笔记
2014/11/04 Javascript
JavaScript 自定义事件之我见
2017/09/25 Javascript
微信小程序实现收藏与取消收藏切换图片功能
2018/08/03 Javascript
jquery操作select常见方法大全【7种情况】
2019/05/28 jQuery
vue实现自定义多选按钮
2020/07/16 Javascript
Vue的自定义组件不能使用click方法的解决
2020/07/28 Javascript
[07:47]DOTA2国际邀请赛采访专栏:探访Valve总部
2013/08/08 DOTA
[11:57]《一刀刀一天》第十七期:TI中国军团加油!
2014/05/26 DOTA
[41:56]Spirit vs Liquid Supermajor小组赛A组 BO3 第一场 6.2
2018/06/03 DOTA
Python字符串逐字符或逐词反转方法
2015/05/21 Python
使用python调用zxing库生成二维码图片详解
2017/01/10 Python
python 定义n个变量方法 (变量声明自动化)
2018/11/10 Python
Python第三方Window模块文件的几种安装方法
2018/11/22 Python
使用CodeMirror实现Python3在线编辑器的示例代码
2019/01/14 Python
Pycharm 使用 Pipenv 新建的虚拟环境(图文详解)
2020/04/16 Python
OpenCV 之按位运算举例解析
2020/06/19 Python
装上这 14 个插件后,PyCharm 真的是无敌的存在
2021/01/11 Python
荷兰电脑专场:Paradigit
2018/05/05 全球购物
.net开发工程师面试题
2014/02/25 面试题
机电职业生涯规划书范文
2014/03/08 职场文书
班级课外活动总结
2014/07/09 职场文书
舌尖上的中国观后感
2015/06/02 职场文书
嘉年华活动新闻稿
2015/07/17 职场文书
opencv-python图像配准(匹配和叠加)的实现
2021/06/23 Python
如何用vue实现网页截图你知道吗
2021/11/17 Vue.js
PostgreSQL事务回卷实战案例详析
2022/03/25 PostgreSQL
Oracle使用别名的好处
2022/04/19 Oracle