PHP中的隨機數安全問題

原創作者:Fazx

引言

西湖論劍杯線上預選賽線上賭場一題,明文攻擊出來的hint中給了/flag/seed.txt以及一個字符串code,這裏需要稍微腦洞一點想到seed是指隨機數種子,以及Web頁面上的code值是每小時更換的"隨機數":

我們利用php_mt_seed工具(C編寫,速度很快)可以根據隨機數碰撞出隨機數的種子,從而獲取flag:

以下是原理分析:


隨機數的安全缺陷

隨機數廣泛應用於生成驗證碼、Token、密鑰等場景中,分爲真隨機數僞隨機數。我們通過算法(常用線性同餘)和種子(常用時鐘)得到的隨機數屬於僞隨機數:當知道種子或已產生的隨機數時,隨機數序列是可以被預測的。

可以看到PHP Manual其實提示了生成隨機數用於加密是不安全的,但是這個Caution不知爲何只存在於英文版的PHP Manual中,中文版被遺漏了…這可能也是很多國內的開發應用出現過此缺陷的一個原因。

PHP中生成隨機數的函數有rand()mt_rand(),它們分別對應srand()mt_strand()兩個用於播種隨機數種子的函數。我們建立rand.php進行測試:

<?php
mt_srand(2333);
srand(2333);
echo "seed=2333,rand()產生的隨機數序列:\n";
for($i=1;$i<=3;$i++){
   echo rand()."\n";
}
echo "seed=2333,mt_rand()產生的隨機數序列:\n";
for($i=1;$i<=3;$i++){
   echo mt_rand()."\n";
}
?>

執行:

可以看出當隨機數種子相同時,不管是rand()還是mt_rand()產生的隨機數序列都是相同的,如果seed泄露則會導致隨機數序列的泄露。當種子值爲固定如mt_srand(1000)時,隨機數形同虛設;而使用動態種子也未必安全,如:

//seed值較小,直接遍歷爆破
mt_srand(mt_rand(0,1000));

//用公開的time()作爲種子,和靜態種子一樣危險
mt_srand(time());
//破解時要注意服務器時間可能存在偏差,需要設定一個較小的範圍

自PHP 4.2.0 起,隨機數發生器會自動完成播種,不再需要手工調用srand()mt_srand(),但是這樣仍舊不安全,我們分別對兩個函數進行討論:

rand()

rand()在產生隨機數時不會自動調用srand(),產生的隨機數序列可以通過這個式子預測:

state[i] = state[i-3] + state[i-31]

所以我們可以收集rand()生成的32位以上的隨機序列,以預測後面的隨機序列。

詳細參考:Cracking-Php-Rand

並且在某些平臺下rand()最大值爲32767,非常容易遭到爆破。

mt_rand()

根據PHP Manual,mt_rand()產生隨機數值的平均速度比libc提供的rand()快四倍,rand() 函數默認使用 libc 隨機數發生器,mt_rand() 函數是非正式用來替換它的。

mt_rand()函數的安全缺陷主要出現在,所謂"自動播種"其實是PHP在同一個請求進程中只會進行一次播種,也就是說即使多次調用mt_rand()函數,也只會根據第一次播種的種子生成隨機數。這一結論的證明可以通過mt_rand()源碼分析或寫個小腳本測試來完成,不再展開,函數的核心實現代碼是這一部分:

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) );
}

由於根據種子生成隨機數序列的計算並不可逆,有效的破解方法應該是窮舉種子並生成隨機數序列,與已知的隨機數(序列)作比較,這也是文章開頭提到的php_mt_seed工具的實現邏輯。

安全建議

涉及到加密/權限/CSRF Token等敏感操作時:

  • 不要使用時間函數作爲種子或直接作爲隨機數:time()/microtime()
  • 不要直接使用rand()``mt_rand()這樣的弱僞隨機數生成器
  • 隨機數要足夠長以防禦暴力破解

相關題目學習

0CTF 2016 Rand2

NJCTF 2017 Guess

參考鏈接:

http://wonderkun.cc/index.html/?p=585

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章