Maison  >  Article  >  développement back-end  >  Explication détaillée d'exemples sûrs de nombres aléatoires mt_rand() en PHP

Explication détaillée d'exemples sûrs de nombres aléatoires mt_rand() en PHP

小云云
小云云original
2018-01-06 16:09:052165parcourir

De nombreuses failles de sécurité liées à mt_rand() ont été découvertes il y a quelque temps, et elles étaient essentiellement causées par une mauvaise compréhension de l'utilisation des nombres aléatoires. Ici, je voudrais mentionner un autre piège du manuel du site officiel de PHP. Jetez un œil à l'introduction de mt_rand() : version chinoise ^cn version anglaise ^en Vous pouvez voir que la version anglaise a un avertissement d'avertissement jaune supplémentaire. mt_rand() utilise l'algorithme mersennetwister pour renvoyer des entiers aléatoires. Tout le monde le sait, mais l'article suivant vous présente principalement les informations pertinentes sur la sécurité des nombres aléatoires mt_rand() en PHP. L'introduction dans l'article est très détaillée. si vous en avez besoin, vous pouvez vous y référer. Pour référence, apprenons avec l'éditeur ci-dessous.

De nombreux développeurs nationaux ont probablement lu la version chinoise de l'introduction et ont utilisé mt_rand() dans le programme pour générer des jetons de sécurité, des clés de cryptage et de déchiffrement de base, etc., provoquant de graves problèmes de sécurité.

Nombre pseudo-aléatoire

mt_rand() n'est pas une véritable fonction de génération de nombres aléatoires. En fait, les fonctions de nombres aléatoires dans la plupart des langages de programmation génèrent des nombres pseudo-aléatoires. nombre. La différence entre les vrais nombres aléatoires et les nombres pseudo-aléatoires ne sera pas expliquée ici. Il vous suffit de comprendre brièvement un peu

Le pseudo-aléatoire est généré par une fonction déterminable (congruence linéaire couramment utilisée) via une graine. (horloge couramment utilisée) de nombres pseudo-aléatoires. Cela signifie que si vous connaissez la graine, ou le nombre aléatoire qui a été généré, il est possible d'obtenir des informations sur la prochaine séquence de nombres aléatoires (prévisibilité).

Supposons simplement que la fonction qui génère des nombres aléatoires en interne dans mt_rand() est : rand = seed+(i*10) où seed est la graine du nombre aléatoire, et i est le nombre de fois que cette fonction de nombre aléatoire est appelé. Lorsque l’on connaît les deux valeurs de i et rand en même temps, on peut facilement calculer la valeur de seed. Par exemple, rand=21 et i=2 sont remplacés dans la fonction 21=seed+(2*10) pour obtenir seed=1. N'est-ce pas très simple ? Après avoir obtenu la graine, nous pouvons calculer la valeur du rand lorsque i est n'importe quelle valeur.

Amorcement automatique de PHP

De la section précédente, nous savons déjà que chaque fois que mt_rand() est appelé, un nombre pseudo-aléatoire sera calculé en fonction de la graine et du nombre actuel de m'appelle. Et la graine est automatiquement générée :

Remarque : Depuis PHP 4.2.0, il n'est plus nécessaire d'utiliser srand() ou mt_srand() pour amorcer le générateur de nombres aléatoires, car cela est désormais effectué automatiquement par le système. .

Alors la question se pose : quand le système termine-t-il automatiquement l'amorçage ? Si mt_rand() est automatiquement amorcé à chaque fois, alors cela ne sert à rien de casser la graine. Le manuel ne donne pas d'informations détaillées sur ce point. J'ai cherché partout sur Internet mais je n'ai pas trouvé de réponse fiable. Je n'ai pu regarder que le code source ^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) );
}

Vous pouvez voir que chaque fois que mt_rand() est appelé, il le sera en premier. vérifiez s'il a été ensemencé. S'il a été semé, générez directement des nombres aléatoires, sinon appelez php_mt_srand pour semer. C'est-à-dire que lors de chaque processus php cgi, seul le premier appel à mt_rand() sera automatiquement généré. Ensuite, des nombres aléatoires seront générés sur la base de cette première graine semée. Parmi les différents modes de fonctionnement de php, à l'exception du CGI (chaque requête démarre un processus cgi et le ferme une fois la requête terminée. Les variables d'environnement php.ini doivent être relues à chaque fois, ce qui entraîne une faible efficacité, et cela ne devrait pas être le cas. beaucoup utilisé maintenant), en gros, après qu'un processus a traité la demande, le serveur de veille attend le suivant et il ne sera pas recyclé tant que plusieurs demandes ne seront pas traitées (il sera également recyclé après l'expiration du délai).

Écrivez un script pour le tester

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

Résultats des tests : (windows+phpstudy)

apache 1000 requêtes

nginx 500 requêtes

Bien sûr, ce test ne fait que confirmer le nombre de requêtes qu'Apache et nginx peuvent traiter en un seul processus. Vérifions tout à l'heure la conclusion concernant l'amorçage automatique :

<?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;
 }
}
<.>Par Jugement pid, lorsqu'un nouveau processus démarre, obtenez aléatoirement le résultat de mt_rand() sur l'une des deux pages :

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
Prenez le premier nombre aléatoire 1513334371 pour faire exploser la graine :

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
Éclaté 3 graines possibles, le nombre est très petit Testé manuellement une par une :

<?php
mt_srand(735487048);//手工播种
for($i=0;$i<21;$i++){
 echo mt_rand()." ";
}
Résultat :

Les 20 premières sont exactement les mêmes que celles obtenues. par le script ci-dessus. La graine confirmée est 1513334371 . Avec la graine, nous pouvons calculer le nombre aléatoire généré en appelant mt_rand() un certain nombre de fois. Par exemple, j'ai généré 21 chiffres dans ce script, et le dernier chiffre est 1515656265. Si vous n'avez pas visité le site après avoir exécuté le script tout à l'heure, vous pouvez voir le même 1515656265 en ouvrant http://localhost/pid2.php.

Nous arrivons donc à la conclusion :

L'amorçage automatique de php se produit lorsque mt_rand() est appelé pour la première fois dans le processus php cgi. Quelle que soit la page visitée, tant que la demande est traitée par le même processus, ils partageront la même graine initialement semée automatiquement.

php_mt_seed


Nous savons déjà que la génération de nombres aléatoires dépend d'une fonction spécifique, supposée ci-dessus être rand = seed+(i*10) . Pour une fonction aussi simple, on peut bien sûr calculer directement (oralement) une solution (un groupe), mais la fonction réellement utilisée par mt_rand() est assez complexe et ne peut être inversée. Une méthode de craquage efficace consiste à énumérer de manière exhaustive toutes les graines, à générer une séquence de nombres aléatoires basée sur la graine, puis à la comparer avec une séquence de nombres aléatoires connue pour vérifier si la graine est correcte. php_mt_seed^phpmtseed est un tel outil. Il est très rapide et ne prend que quelques minutes pour exécuter la graine 2^32 bits. Il peut directement exploser les graines possibles en fonction de la sortie d'un seul mt_rand() (exemple ci-dessus). Bien sûr, il peut également exploser les graines qui limitent la sortie MIN MAX comme mt_rand(1,100) (utile dans les exemples ci-dessous).

Problèmes de sécurité

Cela dit, pourquoi les nombres aléatoires sont-ils dangereux ? En fait, il n'y a rien de mal avec la fonction elle-même, et le responsable rappelle également clairement que les nombres aléatoires générés ne doivent pas être utilisés à des fins de cryptage de sécurité (bien que le manuel de la version chinoise ne l'écrive pas). Le problème est que les développeurs ne réalisent pas qu’il ne s’agit pas d’un nombre véritablement aléatoire. Nous savons déjà que des graines peuvent être extraites d'une séquence connue de nombres aléatoires. C'est-à-dire que tant qu'il y a un nombre aléatoire de sortie ou sa valeur dérivée (valeur aléatoire réversible) dans n'importe quelle page, alors le nombre aléatoire dans n'importe quelle autre page ne sera plus un « nombre aléatoire ». Les exemples courants de génération de nombres aléatoires incluent les codes de vérification, les noms de fichiers aléatoires, etc. Des nombres aléatoires courants sont utilisés pour la vérification de la sécurité, comme la récupération des valeurs de vérification du mot de passe, telles que les clés de cryptage, etc. Un scénario d'attaque idéal :

En pleine nuit, en attendant qu'Apache (nginx) reprenne tous les processus PHP (pour s'assurer que la prochaine visite sera ré-amorcée), visitez une fois la page de codes de vérification, inversez le nombre aléatoire en fonction des caractères du code de vérification, puis explosez les graines de nombres aléatoires en fonction de nombres aléatoires. Visitez ensuite la page de récupération de mot de passe, et le lien de récupération de mot de passe généré est basé sur des nombres aléatoires. Nous pouvons facilement calculer ce lien et récupérer le mot de passe de l'administrateur...XXOO

Exemple

PHPCMS MT_RAND SEED CRACK provoquant une fuite de clé d'authentification Yuniu écrit mieux que moi, il suffit de regarder ce qu'il a dit

La fuite de clé d'authentification Discuz x3.2 est en fait similaire. Le patch officiel a été publié et ceux qui sont intéressés peuvent l'analyser eux-mêmes.

Recommandations associées :

Vrais et faux nombres aléatoires de PHP

Fonction rand() de PHP pour la génération de nombres aléatoires

Exemple d'implémentation JavaScript d'un générateur de déduplication de nombres aléatoires

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn