php mt_rand()乱数安全

6794 ワード

前言
この間たくさん掘ったよmt_rand()に関連するセキュリティ・ホールは,基本的に乱数の使い方を誤って理解しているためである.ここでまたphp公式サイトmanualの穴についてお話しします.mt_について見てください.rand()の紹介:中国語版[1]英語版[2]では、英語版に黄色のCaution警告が1枚増えていることがわかります
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()は真・乱数生成関数ではなく,実際にはほとんどのプログラミング言語における乱数関数が擬似乱数を生成する.真の乱数と偽の乱数の違いについてはここでは説明を展開せず,簡単に理解する必要がある.
擬似ランダムは、特定可能な関数(常用線形同余)によって、1つのシード(常用クロック)によって生成される擬似ランダム数である.これは、シードが分かっているか、またはすでに生成されているランダム数が分かれば、次の乱数シーケンスの情報(予測可能)が得られる可能性があることを意味する.
簡単に仮定してmt_rand()内部で乱数を生成する関数は、rand = seed+(i*10)のうちseedが乱数シードであり、iがこの乱数関数を何回目に呼び出すかである.irandの2つの値を同時に知っていると、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は詳細を示していない.ネットで検索しても当てにならない答えはソースコードをひっくり返すしかない[3].
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環境変数を再読み込みするなど効率が低下し、現在はあまり使われていないはずです)以外は、基本的には1つのプロセスがリクエストを処理した後にstandbyが次を待っています.複数のリクエストを処理してから回収されます(タイムアウトしても回収されます).スクリプトを書いてテストしてください.

テスト結果:(windows+phpstudy)
apache 1000リクエストnginx 500リクエスト
もちろん、このテストはapacheとnginxの1つのプロセスで処理できるリクエスト数だけを確認し、さっきの自動種まきに関する結論を検証します.
";
while(true){
    $pid = file_get_contents('http://localhost/pid1.php');
    if($pid!=$old_pid){
        echo "new_pid:{$pid}\r
"; for($i=0;$i<20;$i++){ $random = mt_rand(1,2); echo file_get_contents("http://localhost/pid".$random.".php?rand=1")." "; } break; } }

pidにより、新しいプロセスが開始されると、2つのページのうちの1つの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つの可能な種子が爆破され、手動で1つのテストを行うことはめったにありません.

出力:
1513334371 2014450250 1319669412 499559587 117728762 1465174656 1671827592 1703046841 464496438 1974338231 46646067 981271768 1070717272 571887250 922467166 606646473 134605134 857256637 1971727275 2104203195 1515656265
上位20位は上記のスクリプトとそっくりで、確認シードは1513334371です.シードがあれば任意の回数の呼び出しmtを計算できますrand()で生成された乱数です.たとえばこのスクリプトは21ビット生成され、最後のスクリプトは1515656265です.さっきのスクリプトを走った後、サイトにアクセスしなかったら、開きます.http://localhost/pid2.php同じ1515656265が見えます.結論を得ました
phpの自動種まきはphp cgiプロセスで初めてmt_を呼び出す.rand()のとき.アクセスしたページに関係なく、同じプロセスで処理されたリクエストであれば、同じ最初に自動的に種まきされたシードが共有されます.
php_mt_seed
乱数の生成はrand = seed+(i*10)と仮定した特定の関数に依存することを知った.このような単純な関数については,もちろん直接計算(口算)して1つの(グループ)解を出すことができるが,mt_rand()が実際に使用する関数はかなり複雑で逆演算できない.有効な解読方法は,実際にはすべての種子を窮挙し,種子に基づいて乱数シーケンスを生成し,既知の乱数シーケンスと比較して種子が正しいかどうかを検証することである.php_mt_seed[4]はこのようなツールであり、その速度は非常に速く、2^32ビットseedを走っても数分である.単一mt_に基づいてrand()の出力結果は、可能なシード(上に例がある)を直接爆破し、mt_rand(1100)のようにMIN MAX出力を限定したシード(以下の例で有用)を爆破することもできる.
あんぜんもんだい
こんなにたくさん言ったのに、いったい乱数はどうして安全ではないのだろうか.実は関数自体は問題ありませんが、生成された乱数がセキュリティ暗号化用途に適用されないことも明らかになった(中国語版manualでは書かれていないが).問題は、開発者がこれが真・乱数ではないことを意識していないことである.既知の乱数シーケンスによって種子を爆破できることが知られている.すなわち、任意のページに出力乱数またはその派生値が存在する限り(ランダム値を可逆的に押す)では、他の任意のページの乱数は「乱数」ではなくなります.一般的な出力乱数の例として、検証コード、ランダムファイル名などがあります.一般的な乱数は、暗号化keyなどのパスワード検証値を取り戻すなど、セキュリティ検証に使用されます.理想的な攻撃シーン:
夜が更けて人が静かになる.apache(nginx)がすべてのphpプロセスを回収するのを待つ(次のアクセスで種まきが再開されることを確認します)、一度認証コードページにアクセスし、認証コード文字に基づいて乱数を逆押し出し、乱数に基づいて乱数シードを爆破します.次にパスワードページにアクセスすると、生成された復元パスワードリンクは乱数に基づいています.このリンクを簡単に計算し、管理者のパスワードを取り戻すことができます......XXOO
≪インスタンス|Instance|emdw≫
  • PHPCMS MT_RAND SEED CRACKはauthkeyに雨牛が私よりよく書いたことを漏らして、彼の
  • を見るだけで十分です
  • Discuz x3.2 authkeyがこれを漏らすのも悪くない.公式にパッチが出ているので、興味があれば自分で分析してみてください.

  • http://php.net/manual/zh/function.mt-rand.php ↩
    http://php.net/manual/en/function.mt-rand.php ↩
    https://github.com/php/php-src/blob/e23da65550680230618bc26fb34d19baa89157fe/ext/standard/mt_rand.c ↩
    http://www.openwall.com/php_mt_seed/↩