mt_rand()使用mersennetwister演算法傳回隨機整數,這個大家都知道,但下面這篇文章主要給大家介紹的是關於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 (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
實例
以上是深入理解PHP中mt_rand()隨機數的安全的詳細內容。更多資訊請關注PHP中文網其他相關文章!