web-dev-qa-db-fra.com

Prédire les nombres Math.random ()?

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?

34
Abe Miessler

En effet, Math.random() n'est pas cryptographiquement sécurisé.


Définition de 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é.


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().


L'algorithme MWC1616 du V8

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é.


Absence de compartimentation

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.


Graine mal choisie

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.


Solutions

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:

Les deux s'appuient sur l'entropie au niveau du système d'exploitation et vous permettront d'obtenir des valeurs aléatoires cryptographiquement.

35
Benoit Esnard

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.

21
douggard

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é.

2
AnoE