天天看點

深入了解PHP中mt_rand()随機數的安全

前言

在前段時間挖了不少跟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+(i10) 其中 seed 是随機數種子, i 是第幾次調用這個随機數函數。當我們同時知道 i 和 rand 兩個值的時候,就能很容易的算出seed的值來。比如 rand=21 , i=2 代入函數 21=seed+(210) 得到 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;/【php教程_linux常用指令_網絡運維技術】/

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}rn";

while(true){

$pid = file_get_contents('http://localhost/pid1.php');

if($pid!=$old_pid){

echo "new_pid:{$pid}rn";

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洩露 這個其實也差不多。官方已出更新檔,有興趣的可以自己去分析一下。