>  기사  >  백엔드 개발  >  PHP에서 mt_rand() 난수의 안전한 예에 대한 자세한 설명

PHP에서 mt_rand() 난수의 안전한 예에 대한 자세한 설명

小云云
小云云원래의
2018-01-06 16:09:052154검색

mt_rand()와 관련된 보안 취약점이 얼마 전에 많이 발견되었는데, 이는 기본적으로 난수 사용법에 대한 오해에서 비롯되었습니다. 여기에서는 PHP 공식 웹사이트 매뉴얼의 또 다른 함정에 대해 언급하고 싶습니다. mt_rand()의 소개를 살펴보세요: 중국어 버전 ^cn 영어 버전 ^en 영어 버전에는 추가로 노란색 주의 경고가 있는 것을 볼 수 있습니다. mt_rand()는 mersennetwister 알고리즘을 사용하여 임의의 정수를 반환합니다. 이는 누구나 알고 있지만 다음 기사에서는 주로 PHP의 mt_rand() 난수 보안에 대한 관련 정보를 소개합니다. 참고할 수 있도록 아래 에디터와 함께 배워보겠습니다.

많은 국내 개발자들이 아마도 중국어 버전의 소개를 읽고 프로그램에서 mt_rand()를 사용하여 보안 토큰, 핵심 암호화 및 복호화 키 등을 생성하여 심각한 보안 문제를 일으켰을 것입니다.

의사 난수

mt_rand()는 실제 난수 생성 함수가 아닙니다. 실제로 프로그래밍 언어의 대부분의 난수 함수는 의사 난수를 생성합니다. 실제 난수와 의사 난수의 차이점은 여기서 설명하지 않습니다. 의사 난수는 일반적으로 사용되는 시계를 통해 결정 가능한 함수(일반적으로 사용되는 선형 합동)에 의해 생성됩니다. ). 즉, 시드나 생성된 난수를 알면 다음 난수열에 대한 정보를 얻을 수 있다는 뜻이다(예측성).

mt_rand()에서 내부적으로 난수를 생성하는 함수가 다음과 같다고 간단히 가정해 보세요. rand = seed+(i*10) 여기서 Seed는 난수 시드이고 i는 이 난수 함수가 호출되는 횟수입니다. i와 rand의 두 값을 동시에 알면 시드의 값을 쉽게 계산할 수 있습니다. 예를 들어, rand=21 및 i=2는 21=seed+(2*10) 함수로 대체되어 seed=1을 얻습니다. 아주 간단하지 않나요? 시드를 얻은 후에 i가 임의의 값일 때 랜드 값을 계산할 수 있습니다.

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()에 대한 첫 번째 호출만 자동으로 시드됩니다. 다음으로, 처음 뿌려진 시드를 기반으로 난수가 생성됩니다. CGI를 제외한 php의 여러 작동 모드 중(각 요청은 cgi 프로세스를 시작하고 요청이 완료된 후 닫힙니다. php.ini 환경 변수는 매번 다시 읽어야 하므로 효율성이 낮으며, 그렇게 해서는 안 됩니다. 현재 많이 사용됨) 기본적으로 하나의 프로세스가 요청을 처리한 후 대기 프로세스는 다음 프로세스를 기다리며 여러 요청이 처리될 때까지 재활용되지 않습니다(타임아웃 후에도 재활용됩니다).

테스트용 스크립트 작성

<?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비트 시드를 실행하는 데 몇 분 밖에 걸리지 않습니다. 단일 mt_rand()의 출력을 기반으로 가능한 시드를 직접 폭발시킬 수 있습니다(위의 예). 물론 mt_rand(1,100)과 같이 MIN MAX 출력을 제한하는 시드를 폭발시킬 수도 있습니다(아래 예에서 유용함).

보안 문제

이렇게 말했는데 난수는 왜 안전하지 않습니까? 실제로 기능 자체에는 아무런 문제가 없으며, 생성된 난수를 보안 암호화 목적으로 사용해서는 안 된다는 점을 관계자도 분명히 밝혔습니다(중국어 버전 매뉴얼에는 이런 내용이 적혀있지 않지만). 문제는 개발자가 이것이 진정한 난수가 아니라는 것을 깨닫지 못한다는 것입니다. 우리는 알려진 일련의 난수에서 씨앗이 폭발할 수 있다는 것을 이미 알고 있습니다. 즉, 임의의 페이지에 출력 난수 또는 그 파생 값(가역적 난수 값)이 있는 한 다른 페이지의 난수는 더 이상 "난수"가 아닙니다. 난수 출력의 일반적인 예로는 인증 코드, 임의의 파일 이름 등이 있습니다. 암호화 키 등 비밀번호 확인 값을 가져오는 등 보안 확인을 위해 일반적인 난수를 사용합니다. 이상적인 공격 시나리오:

한밤 중에 아파치(nginx)가 모든 PHP 프로세스를 다시 가져오기를 기다리고(다음 방문 시 다시 시드되도록 하기 위해) 확인 코드 페이지를 한 번 방문하고 인증번호 문자를 기준으로 난수를 생성한 후, 난수를 기준으로 폭발 난수 시드를 생성합니다. 그런 다음 비밀번호 검색 페이지를 방문하면 생성된 비밀번호 검색 링크는 임의의 숫자를 기반으로 합니다. 우리는 이 링크를 쉽게 계산하고 관리자 비밀번호를 검색할 수 있습니다... XXOO

Example

PHPCMS MT_RAND SEED CRACK이 인증 키 유출을 유발합니다. Yu Niu가 저보다 글을 더 잘 씁니다. 그의 Discuz x3.2 인증 키 유출이 실제로 비슷하다는 것을 보세요. 공식 패치가 공개되었으며, 관심 있는 분들은 직접 분석해 보시기 바랍니다.

관련 권장사항:

PHP의 참 및 거짓 난수

난수 생성을 위한 PHP의 rand() 함수

난수 중복 제거 생성기를 구현하는 JavaScript의 예

위 내용은 PHP에서 mt_rand() 난수의 안전한 예에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.