ホームページ >バックエンド開発 >PHPチュートリアル >PHP における mt_rand() 乱数の安全な例の詳細な説明
mt_rand() に関連する多くのセキュリティ脆弱性が少し前に発見されましたが、それらは基本的に乱数の使用法に対する誤解が原因でした。ここで、PHP 公式 Web サイトのマニュアルのもう 1 つの落とし穴について言及したいと思います。mt_rand() の紹介を見てください。英語版には、余分な黄色の注意警告があることがわかります。 mt_rand() は mersennetwister アルゴリズムを使用してランダムな整数を返します。これは誰もが知っていますが、次の記事では主に PHP の mt_rand() 乱数のセキュリティに関する関連情報を紹介しています。必要な方は参考にしてください。以下のエディタで学習してみましょう。
おそらく多くの国内開発者が中国語版の入門書を読み、プログラム内で mt_rand() を使用してセキュリティ トークン、コア暗号化キー、復号化キーなどを生成し、重大なセキュリティ問題を引き起こしたと考えられます。
擬似乱数
mt_rand() は真の乱数生成関数ではありません。実際、プログラミング言語のほとんどの乱数関数は擬似乱数を生成します。真の乱数と擬似乱数の違いについてはここでは説明しません。擬似乱数はシード (一般的に使用されるクロック) によって決定可能な関数 (一般的に使用される) によって生成されることを少しだけ理解する必要があります。 )。これは、シード、つまり生成された乱数を知っていれば、次の乱数列に関する情報を得ることができる (予測可能性) ことを意味します。
mt_rand() の内部で乱数を生成する関数が次のとおりであると単純に仮定します: rand = seed+(i*10) ここで、seed は乱数シード、i はこの乱数関数が呼び出される回数です。 i と rand の 2 つの値が同時にわかると、seed の値を簡単に計算できます。たとえば、rand=21 と i=2 を関数 21=seed+(2*10) に代入すると、seed=1 が得られます。非常に簡単ではないでしょうか。シードを取得したら、i が任意の値の場合の rand の値を計算できます。
PHP の自動シード
前のセクションから、mt_rand() が呼び出されるたびに、シードと現在の呼び出し回数 i に基づいて擬似乱数が計算されることがすでにわかりました。そして、シードは自動的にシードされます:
注: PHP 4.2.0 以降、乱数ジェネレーターのシードに srand() または mt_srand() を使用する必要はありません。これは、乱数ジェネレーターのシードがシステムによって自動的に行われるようになったためです。
それでは、システムはいつシードを自動的に完了するのかという疑問が生じます。mt_rand() が毎回自動的にシードされるのであれば、シードをクラックする意味はありません。マニュアルにはこの点に関する詳細な情報は記載されていません。インターネット中を検索しましたが、信頼できる答えが見つからなかったので、ソース コードを確認する必要がありました ^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 環境変数は毎回再読み込みする必要があるため、効率が低くなります。基本的に、1 つのプロセスがリクエストを処理した後、スタンバイは次のリクエストを待ち、複数のリクエストが処理されるまでリサイクルされません (タイムアウト後にもリサイクルされます)。
テストするスクリプトを書いてください
<?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が1つのプロセスで処理できるリクエストの数を確認するだけです、もう一度検証してみましょう。 自動シードについて結論を述べました:
<?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 によって判断します。 新しいプロセスが開始されると、2 つのページのいずれかで 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 つずつ手動でテストしました。 出力:
最初の 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 ビット シードの実行にかかる時間はわずか数分です。単一の mt_rand() の出力に基づいて可能なシードを直接展開できます (上記の例)。もちろん、mt_rand(1,100) のような MIN MAX 出力を制限するシードを展開することもできます (以下の例で役立ちます)。
セキュリティの問題
ここまで言いましたが、なぜ乱数は安全ではないのでしょうか?実際、機能自体には何も問題はなく、生成された乱数をセキュリティの暗号化目的に使用してはならないと公式にも明記されています(ただし中国語版マニュアルにはこのことは書かれていません)。問題は、開発者がこれが真の乱数ではないことに気づいていないことです。既知の乱数シーケンスからシードを爆発させることができることはすでにわかっています。つまり、どのページにも出力乱数またはその派生値(可逆乱数値)が存在する限り、他のページにある乱数は「乱数」ではなくなるのです。乱数を出力する一般的な例には、検証コード、ランダムなファイル名などが含まれます。一般的な乱数は、暗号化キーなどのパスワード検証値の取得などのセキュリティ検証に使用されます。理想的な攻撃シナリオ:
真夜中に、Apache (nginx) がすべての PHP プロセスを取り戻すのを待ちます (次回の訪問時に確実に再シードされるようにするため)。一度検証コード ページにアクセスし、コードを逆にします。検証コードの文字に基づいて乱数を生成し、乱数に基づいてブラストして乱数シードを生成します。次に、パスワード取得ページにアクセスすると、生成されたパスワード取得リンクは乱数に基づいています。このリンクを簡単に計算して、管理者のパスワードを取得できます... XXOO
例
PHPCMS MT_RAND SEED CRACK による認証キー リークの原因 Yu Niu は私よりも上手に書いています。彼の Discuz x3.2 認証キー リークを見てください。実際、これと似ています。公式パッチが公開されているので、興味のある方はご自身で解析してみてください。
関連する推奨事項:
PHP の true および false 乱数php の乱数生成用の rand() 関数乱数重複排除ジェネレーターの JavaScript 実装の例以上がPHP における mt_rand() 乱数の安全な例の詳細な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。