Je lisais sur la documentation de Math.random () et j'ai trouvé la note:
Math.random () ne fournit pas de nombres aléatoires sécurisés cryptographiquement. Ne les utilisez pas pour tout ce qui concerne la sécurité. Utilisez plutôt l'API Web Crypto, et plus précisément la méthode window.crypto.getRandomValues ().
Est-il possible de prédire quels numéros un appel à random
générera? Si oui, comment cela pourrait-il être fait?
En effet, Math.random()
n'est pas cryptographiquement sécurisé.
Math.random()
La définition de Math.random()
dans la spécification ES6 a laissé beaucoup de liberté sur l'implémentation de la fonction dans les moteurs JavaScript:
Renvoie une valeur numérique de signe positif, supérieure ou égale à 0 mais inférieure à 1, choisie de manière aléatoire ou pseudo aléatoire avec une distribution approximativement uniforme sur cette plage, à l'aide d'un algorithme ou d'une stratégie dépendant de l'implémentation. Cette fonction ne prend aucun argument.
Chaque fonction
Math.random
Créée pour un code distinct Les domaines doivent produire une séquence distincte de valeurs à partir d'appels successifs.
Voyons donc comment les moteurs JavaScript les plus populaires l'ont implémenté.
RandomNumberGenerator
)Xorshift128 + est l'un des générateurs de nombres aléatoires XorShift , qui sont parmi les générateurs de nombres aléatoires non sécurisés par cryptographie les plus rapides.
Je ne sais pas s'il y a une attaque contre l'une des implémentations répertoriées ci-dessus, cependant. Mais ces implémentations sont très récentes, et d'autres implémentations (et vulnérabilités) existaient dans le passé, et peuvent toujours exister si votre navigateur/serveur n'a pas été mis à jour.
Mise à jour: réponse de douggard explique comment quelqu'un peut récupérer l'état XorShift128 + et prédire les valeurs de Math.random()
.
En novembre 2015, Mike Malone a expliqué dans un article de blog que l'implémentation V8 de l'algorithme MWC1616 était en quelque sorte cassée : vous pouvez voir des modèles linéaires sur ce test ou sur celui-ci si vous utilisez un navigateur V8. L'équipe V8 l'a géré et a publié un correctif dans Chromium 49 (le 15 janvier 2016) et Chrome 49 (le 8 mars 2016).
Cet article publié en 2009 a expliqué comment déterminer l'état des PRNG des V8 sur la base des sorties précédentes de Math.random()
(la version MWC1616) .
Voici un script Python qui l'implémente (même si les sorties ne sont pas consécutives).
Cela a été exploité dans une attaque réelle sur CSGOJackbot , un site de paris construit avec Node.js. L'attaquant était assez juste pour se moquer de cette vulnérabilité.
Avant ES6, la Math.random()
définition ne spécifiait pas que des pages distinctes devaient produire des séquences de valeurs distinctes.
Cela a permis à un attaquant de générer des nombres aléatoires, de déterminer l'état du PNRG, de rediriger l'utilisateur vers une application vulnérable (qui utiliserait Math.random()
pour les choses sensibles) et de prédire quel nombre Math.random()
allait revenir. Ce billet de blog présente un code sur la façon de le faire (Internet Explorer 8 et versions antérieures).
La spécification ES6 (qui avait été approuvée en tant que norme le 17 juin 2015) garantit que les navigateurs gèrent correctement cette affaire.
Deviner la graine choisie pour initialiser la séquence peut également permettre à un attaquant de prédire les nombres dans la séquence. C'est aussi un scénario réel, puisque il a été utilisé sur Facebook en 2012.
Cet article publié en 2008 explique différentes méthodes pour divulguer certaines informations grâce au manque d'aléatoire des navigateurs.
Tout d'abord, assurez-vous toujours que vos navigateurs/serveurs sont mis à jour régulièrement.
Ensuite, vous devez utiliser des fonctions cryptographiques si nécessaire:
crypto.getRandomValues
, une partie de Web Crypto API (vérifiez le tableau de support ).crypto.randomBytes
.Les deux s'appuient sur l'entropie au niveau du système d'exploitation et vous permettront d'obtenir des valeurs aléatoires cryptographiquement.
Vous pouvez les attaquer en utilisant le prouveur de théorème Z3. J'ai implémenté une telle attaque en Python afin de prédire les valeurs dans un simulateur de loterie.
Comme mentionné précédemment, XorShift128 + est utilisé dans la plupart des endroits maintenant, c'est donc ce que nous attaquons. Vous commencez par implémenter l'algorithme normal pour pouvoir le comprendre.
def xs128p(state0, state1):
s1 = state0 & 0xFFFFFFFFFFFFFFFF
s0 = state1 & 0xFFFFFFFFFFFFFFFF
s1 ^= (s1 << 23) & 0xFFFFFFFFFFFFFFFF
s1 ^= (s1 >> 17) & 0xFFFFFFFFFFFFFFFF
s1 ^= s0 & 0xFFFFFFFFFFFFFFFF
s1 ^= (s0 >> 26) & 0xFFFFFFFFFFFFFFFF
state0 = state1 & 0xFFFFFFFFFFFFFFFF
state1 = s1 & 0xFFFFFFFFFFFFFFFF
generated = (state0 + state1) & 0xFFFFFFFFFFFFFFFF
return state0, state1, generated
L'algorithme prend deux variables d'état, les XOR et les déplace, puis retourne la somme des variables d'état mises à jour. Ce qui est également important, c'est la façon dont chaque moteur prend le uint64 retourné et le convertit en double. J'ai trouvé ces informations en fouillant dans le code source de chaque implémentation.
# Firefox (SpiderMonkey) nextDouble():
# (Rand_uint64 & ((1 << 53) - 1)) / (1 << 53)
# Chrome (V8) nextDouble():
# ((Rand_uint64 & ((1 << 52) - 1)) | 0x3FF0000000000000) - 1.0
# Safari (WebKit) weakRandom.get():
# (Rand_uint64 & ((1 << 53) - 1) * (1.0 / (1 << 53)))
Chacun est un peu différent. Vous pouvez ensuite prendre les doubles produits par Math.random () et récupérer quelques bits inférieurs de uint64 produits par les algorithmes.
Ensuite, implémentez le code dans Z3 afin qu'il puisse être exécuté symboliquement et que l'état puisse être résolu. Voir le lien Github pour plus de contexte. Il ressemble assez au code normal, sauf que vous dites au solveur que les bits inférieurs doivent être égaux aux bits inférieurs récupérés à partir du navigateur.
def sym_xs128p(slvr, sym_state0, sym_state1, generated, browser):
s1 = sym_state0
s0 = sym_state1
s1 ^= (s1 << 23)
s1 ^= LShR(s1, 17)
s1 ^= s0
s1 ^= LShR(s0, 26)
sym_state0 = sym_state1
sym_state1 = s1
calc = (sym_state0 + sym_state1)
condition = Bool('c%d' % int(generated * random.random()))
if browser == 'chrome':
impl = Implies(condition, (calc & 0xFFFFFFFFFFFFF) == int(generated))
Elif browser == 'firefox' or browser == 'safari':
# Firefox and Safari save an extra bit
impl = Implies(condition, (calc & 0x1FFFFFFFFFFFFF) == int(generated))
slvr.add(impl)
return sym_state0, sym_state1, [condition]
Si vous fournissez 3 doubles générés consécutivement à Z3, il devrait être en mesure de récupérer votre état. Ci-dessous, un extrait de la fonction principale. Il appelle l'algorithme XorShift128 + exécuté symboliquement sur deux des entiers 64 bits de Z3 (les variables d'état inconnues), fournissant les bits inférieurs (52 ou 53) des uint64 récupérés.
Si cela réussit, le solveur retournera SATISFAIT et vous pouvez obtenir les variables d'état pour lesquelles il a été résolu.
for ea in xrange(3):
sym_state0, sym_state1, ret_conditions = sym_xs128p(slvr, sym_state0, sym_state1, generated[ea], browser)
conditions += ret_conditions
if slvr.check(conditions) == sat:
# get a solved state
m = slvr.model()
state0 = m[ostate0].as_long()
state1 = m[ostate1].as_long()
Il y a un résumé légèrement plus détaillé ici qui se concentre sur l'utilisation de cette méthode pour prédire les numéros de loterie gagnants dans un simulateur de powerball.
Math.random
(et d'autres fonctions similaires) partent d'une graine et créent un nouveau nombre. Il semble aléatoire pour l'utilisateur car bien sûr l'algorithme est réglé de telle manière qu'il apparaît ainsi. Mais le fait est qu'il n'y a nulle part de véritable source de hasard. Si vous connaissez l'état interne du générateur (qui est un logiciel 100% déterministe et rien de spécial du tout), vous savez tous les nombres futurs (et selon l'algorithme peut-être aussi passés) générés par lui.
Une "vraie" source aléatoire serait des choses comme la mesure de la décroissance d'une particule radioactive, ou en termes plus réels, tout type de bruit électrique blanc, ou plus concrètement, des choses comme l'utilisateur déplaçant la souris ou des écarts minimes entre les pressions de touches et de telles choses. Rien du tout n'est dans Math.random
.
Math.random
est (comme la fonction random
de la plupart des langues/bibliothèques) conçu de cette manière, et la propriété que vous pouvez récupérer une chaîne de nombres "aléatoires" de la même graine est en fait une fonctionnalité utile dans de nombreux cas . Mais pas pour la sécurité.