深入理解PHP中mt_rand()隨機(jī)數(shù)的安全
前言
在前段時(shí)間挖了不少跟mt_rand()相關(guān)的安全漏洞,基本上都是錯(cuò)誤理解隨機(jī)數(shù)用法導(dǎo)致的。這里又要提一下php官網(wǎng)manual的一個(gè)坑,看下關(guān)于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.
很多國(guó)內(nèi)開發(fā)者估計(jì)都是看的中文版的介紹而在程序中使用了mt_rand()來生成安全令牌、核心加解密key等等導(dǎo)致嚴(yán)重的安全問題。
偽隨機(jī)數(shù)
mt_rand()并不是一個(gè) 真·隨機(jī)數(shù) 生成函數(shù),實(shí)際上絕大多數(shù)編程語言中的隨機(jī)數(shù)函數(shù)生成的都都是偽隨機(jī)數(shù)。關(guān)于真隨機(jī)數(shù)和偽隨機(jī)數(shù)的區(qū)別這里不展開解釋,只需要簡(jiǎn)單了解一點(diǎn)
偽隨機(jī)是由可確定的函數(shù)(常用線性同余),通過一個(gè)種子(常用時(shí)鐘),產(chǎn)生的偽隨機(jī)數(shù)。這意味著:如果知道了種子,或者已經(jīng)產(chǎn)生的隨機(jī)數(shù),都可能獲得接下來隨機(jī)數(shù)序列的信息(可預(yù)測(cè)性)。
簡(jiǎn)單假設(shè)一下 mt_rand()內(nèi)部生成隨機(jī)數(shù)的函數(shù)為: rand = seed+(i*10) 其中 seed 是隨機(jī)數(shù)種子, i 是第幾次調(diào)用這個(gè)隨機(jī)數(shù)函數(shù)。當(dāng)我們同時(shí)知道 i 和 rand 兩個(gè)值的時(shí)候,就能很容易的算出seed的值來。比如 rand=21 , i=2 代入函數(shù) 21=seed+(2*10) 得到 seed=1 。是不是很簡(jiǎn)單,當(dāng)我們拿到seed之后,就能計(jì)算出當(dāng) i 為任意值時(shí)候的 rand 的值了。
PHP的自動(dòng)播種
從上一節(jié)我們已經(jīng)知道每一次mt_rand()被調(diào)用都會(huì)根據(jù)seed和當(dāng)前調(diào)用的次數(shù)i來計(jì)算出一個(gè)偽隨機(jī)數(shù)。而且seed是自動(dòng)播種的:
Note: 自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 給隨機(jī)數(shù)發(fā)生器播種 ,因?yàn)楝F(xiàn)在是由系統(tǒng)自動(dòng)完成的。
那么問題就來了,到底系統(tǒng)自動(dòng)完成播種是在什么時(shí)候,如果每次調(diào)用mt_rand()都會(huì)自動(dòng)播種那么破解seed也就沒意義了。關(guān)于這一點(diǎn)manual并沒有給出詳細(xì)信息。網(wǎng)上找了一圈也沒靠譜的答案 只能去翻源碼^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) );
}
可以看到每次調(diào)用mt_rand()都會(huì)先檢查是否已經(jīng)播種。如果已經(jīng)播種就直接產(chǎn)生隨機(jī)數(shù),否則調(diào)用php_mt_srand來播種。也就是說每個(gè)php cgi進(jìn)程期間,只有第一次調(diào)用mt_rand()會(huì)自動(dòng)播種。接下來都會(huì)根據(jù)這個(gè)第一次播種的種子來生成隨機(jī)數(shù)。而php的幾種運(yùn)行模式中除了CGI(每個(gè)請(qǐng)求啟動(dòng)一個(gè)cgi進(jìn)程,請(qǐng)求結(jié)束后關(guān)閉。每次都要重新讀取php.ini 環(huán)境變量等導(dǎo)致效率低下,現(xiàn)在用的應(yīng)該不多了)以外,基本都是一個(gè)進(jìn)程處理完請(qǐng)求之后standby等待下一個(gè),處理多個(gè)請(qǐng)求之后才會(huì)回收(超時(shí)也會(huì)回收)。
寫個(gè)腳本測(cè)試一下
<?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;
}
}
測(cè)試結(jié)果:(windows+phpstudy)
apache 1000請(qǐng)求
nginx 500請(qǐng)求
當(dāng)然這個(gè)測(cè)試僅僅確認(rèn)了apache和nginx一個(gè)進(jìn)程可以處理的請(qǐng)求數(shù),再來驗(yàn)證一下剛才關(guān)于自動(dòng)播種的結(jié)論:
<?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來判斷,當(dāng)新進(jìn)程開始的時(shí)候,隨機(jī)獲取兩個(gè)頁(yè)面其中一個(gè)的 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
拿第一個(gè)隨機(jī)數(shù) 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個(gè)可能的種子,數(shù)量很少 手動(dòng)一個(gè)一個(gè)測(cè)試:
<?php
mt_srand(735487048);//手工播種
for($i=0;$i<21;$i++){
echo mt_rand()." ";
}
輸出:
前20位跟上面腳本獲取的一模一樣,確認(rèn)種子就是 1513334371 。有了種子我們就能計(jì)算出任意次數(shù)調(diào)用mt_rand()生成的隨機(jī)數(shù)了。比如這個(gè)腳本我生成了21位,最后一位是 1515656265 如果跑完剛才的腳本之后沒訪問過站點(diǎn),那么打開 http://localhost/pid2.php 就能看到相同的 1515656265 。
所以我們得到結(jié)論:
php的自動(dòng)播種發(fā)生在php cgi進(jìn)程中第一次調(diào)用mt_rand()的時(shí)候。跟訪問的頁(yè)面無關(guān),只要是同一個(gè)進(jìn)程處理的請(qǐng)求,都會(huì)共享同一個(gè)最初自動(dòng)播種的種子。
php_mt_seed
我們已經(jīng)知道隨機(jī)數(shù)的生成是依賴特定的函數(shù),上面曾經(jīng)假設(shè)為 rand = seed+(i*10) 。對(duì)于這樣一個(gè)簡(jiǎn)單的函數(shù),我們當(dāng)然可以直接計(jì)算(口算)出一個(gè)(組)解來,但 mt_rand() 實(shí)際使用的函數(shù)可是相當(dāng)復(fù)雜且無法逆運(yùn)算的。有效的破解方法其實(shí)是窮舉所有的種子并根據(jù)種子生成隨機(jī)數(shù)序列再跟已知的隨機(jī)數(shù)序列做比對(duì)來驗(yàn)證種子是否正確。php_mt_seed^phpmtseed就是這么一個(gè)工具,它的速度非???,跑完2^32位seed也就幾分鐘。它可以根據(jù)單次mt_rand()的輸出結(jié)果直接爆破出可能的種子(上面有示例),當(dāng)然也可以爆破類似mt_rand(1,100)這樣限定了MIN MAX輸出的種子(下面實(shí)例中有用到)。
安全問題
說了這么多,那到底隨機(jī)數(shù)怎么不安全了呢?其實(shí)函數(shù)本身沒有問題,官方也明確提示了生成的隨機(jī)數(shù)不應(yīng)用于安全加密用途(雖然中文版本manual沒寫)。問題在于開發(fā)者并沒有意識(shí)到這并不是一個(gè) 真·隨機(jī)數(shù) 。我們已經(jīng)知道,通過已知的隨機(jī)數(shù)序列可以爆破出種子。也就是說,只要任意頁(yè)面中存在輸出隨機(jī)數(shù)或者其衍生值(可逆推隨機(jī)值),那么其他任意頁(yè)面的隨機(jī)數(shù)將不再是“隨機(jī)數(shù)”。常見的輸出隨機(jī)數(shù)的例子比如驗(yàn)證碼,隨機(jī)文件名等等。常見的隨機(jī)數(shù)用于安全驗(yàn)證的比如找回密碼校驗(yàn)值,比如加密key等等。一個(gè)理想中的攻擊場(chǎng)景:
夜深人靜,等待apache(nginx)收回所有php進(jìn)程(確保下次訪問會(huì)重新播種),訪問一次驗(yàn)證碼頁(yè)面,根據(jù)驗(yàn)證碼字符逆推出隨機(jī)數(shù),再根據(jù)隨機(jī)數(shù)爆破出隨機(jī)數(shù)種子。接著訪問找回密碼頁(yè)面,生成的找回密碼鏈接是基于隨機(jī)數(shù)的。我們就可以輕松計(jì)算出這個(gè)鏈接,找回管理員的密碼…………XXOO
實(shí)例
PHPCMS MT_RAND SEED CRACK致authkey泄露 雨牛寫的比我好,看他的就夠了
Discuz x3.2 authkey泄露 這個(gè)其實(shí)也差不多。官方已出補(bǔ)丁,有興趣的可以自己去分析一下。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
- PHP安全配置優(yōu)化詳解
- PHP網(wǎng)站常見安全漏洞,及相應(yīng)防范措施總結(jié)
- 6個(gè)常見的 PHP 安全性攻擊實(shí)例和阻止方法
- PHP安全之register_globals的on和off的區(qū)別
- PHP開發(fā)api接口安全驗(yàn)證操作實(shí)例詳解
- Linux下PHP+Apache的26個(gè)必知的安全設(shè)置
- php解決安全問題的方法實(shí)例
- 實(shí)例分析10個(gè)PHP常見安全問題
- 針對(duì)PHP開發(fā)安全問題的相關(guān)總結(jié)
- PHP網(wǎng)頁(yè)安全認(rèn)證的實(shí)例詳解
- php常見的網(wǎng)絡(luò)攻擊及防御方法
相關(guān)文章
php絕對(duì)路徑與相對(duì)路徑之間關(guān)系的的分析
php絕對(duì)路徑與相對(duì)路徑之間關(guān)系的的深入研究2010-03-03
php頁(yè)面函數(shù)設(shè)置超時(shí)限制的方法
這篇文章主要介紹了php頁(yè)面函數(shù)設(shè)置超時(shí)限制的方法,可通過函數(shù)控制超時(shí)限制,也可通過修改php配置文件實(shí)現(xiàn)修改超時(shí)限制,需要的朋友可以參考下2014-12-12
PHP實(shí)現(xiàn)的分解質(zhì)因數(shù)操作示例
這篇文章主要介紹了PHP實(shí)現(xiàn)的分解質(zhì)因數(shù)操作,結(jié)合實(shí)例形式分析了php實(shí)現(xiàn)分解質(zhì)因數(shù)的相關(guān)原理、步驟與操作技巧,需要的朋友可以參考下2018-08-08
php的instanceof和判斷閉包Closure操作示例
這篇文章主要介紹了php的instanceof和判斷閉包Closure操作,結(jié)合實(shí)例形式分析了PHP使用instanceof判斷類實(shí)例以及判斷閉包Closure相關(guān)操作技巧,需要的朋友可以參考下2020-01-01
php實(shí)現(xiàn)二進(jìn)制和文本相互轉(zhuǎn)換的方法
這篇文章主要介紹了php實(shí)現(xiàn)二進(jìn)制和文本相互轉(zhuǎn)換的方法,實(shí)例分析了文本與數(shù)制轉(zhuǎn)換的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-04-04
php安裝php_rar擴(kuò)展實(shí)現(xiàn)rar文件讀取和解壓的方法
這篇文章主要介紹了php安裝php_rar擴(kuò)展實(shí)現(xiàn)rar文件讀取和解壓的方法,涉及php擴(kuò)展組件的安裝與使用相關(guān)操作技巧,需要的朋友可以參考下2016-11-11

