Mathis Hammel Profile picture
Nov 16, 2021 32 tweets 10 min read Read on X
THREAD : J'ai trouvé un bug qui affecte toutes les versions récentes de Python, et il est interdit de le réparer !

Je vous explique pourquoi ⤵🧵
Comme pour de très nombreux langages de programmation, le générateur de nombres aléatoires intégré à Python porte le nom de Mersenne Twister, ou MT19937.

C'est un RNG (Random Number Generator) très rapide et qui offre une entropie de très bonne qualité.
Mais le MT19937 n'est pas sécurisé contre les attaques cryptographiques : comme tous les générateurs de nombres aléatoires, il n'est en réalité que pseudo-aléatoire : après l'initialisation de ses variables internes, les bits en sortie sont produits de manière déterministe.
Si un attaquant parvient à récupérer environ 20 kilobits consécutifs produits par une instance de Mersenne Twister, il peut reconstituer l'état de la mémoire interne du générateur, et peut ainsi prédire tous les nombres suivants qu'il produira.
Cette vulnérabilité est bien connue, et c'est pour ça qu'il faut absolument utiliser un RNG dit "cryptographique" si l'on souhaite avoir des nombres aléatoires vraiment imprévisibles (je suis d'ailleurs en train de préparer un autre thread au sujet des casinos en ligne! #teasing)
En 2019 alors que j'étais en pleins préparatifs pour le CTF INS'hAck (Capture The Flag = compétition de cybersécurité), je voulais crééer un challenge de crypto sur lequel les participants devraient mettre en oeuvre une attaque contre le générateur MT19937 de Python.
En général, les challenges qui demandent une attaque sur Mersenne Twister on les voit venir de loin : comme il faut récupérer 624 sorties consécutives de 32 bits pour pouvoir réaliser l'attaque, on voit très souvent "random.getrandbits(32)" dans le code source à exploiter.
Moi, j'avais envie de le déguiser un peu mieux que ça, pour que la porte dérobée soit moins évidente : il existe plein d'autres fonctions qui génèrent des bits aléatoires et qui sont bien plus souvent utilisées dans le monde réel : randint, shuffle, randrange, sample, etc.
Par exemple, saviez-vous que pour mélanger de manière parfaitement uniforme une liste de longueur N, il suffit de O(N) opérations ? On commence par choisir un index entre 0 et N-1, avec lequel on échange l'élément à l'indice N-1. Pareil pour N-2, et ainsi de suite jusqu'à 1.
C'est ce genre d'opération que je voulais utiliser dans mon code source vulnérable, les joueurs devraient alors étudier le fonctionnement interne de l'interpréteur Python pour comprendre comment retrouver les bits d'entropie au lieu de leur donner gratuitement avec getrandbits.
Un très grand nombre de fonctions du module random de Python font appel à la fonction cachée _randbelow, qui gère les appels au générateur de bits aléatoires du Mersenne Twister. Sa seule utilité est, comme son nom l'indique, de renvoyer un nombre entier compris entre 0 et n-1.
Du coup, à partir d'un résultat donné d'une fonction shuffle/randint/sample on devrait pouvoir retrouver le résultat des différents appels à randbelow, et à partir de ceux-ci tous les bits qui ont été produits par getrandbits. C'est simple non ? Ben en pratique pas tant que ça 😉
Pour comprendre comment fonctionne randbelow, il faut d'abord que je vous parle de la méthode de sélection/rejet qui est utilisée pour simuler des tirages sur n'importe quelle loi de probabilités. Admettons par exemple que vous souhaitiez choisir un point au hasard sur un disque:
On peut par exemple choisir une coordonnée x aléatoire entre -1 et 1 puis une coordonnée y aléatoire entre -sqrt(1 - x²) et sqrt(1 - x²) qui nous garantit que le point tiré sera forcément dans le cercle.
Cependant, on va rencontrer un problème : on a ici autant de chances de tomber dans la zone bleue que dans la zone jaune, alors que celles-ci ont des aires différentes : la densité du tirage n'est pas uniforme !
La méthode par rejet permet quant à elle de faire des tirages aléatoires compliqués en se basant un générateur de nombres aléatoires simple. On peut par exemple partir du carré, sur lequel il est facile de générer des points uniformes en tirant indépendamment X et Y dans [-1, 1].
On commence donc par tirer un point au hasard uniformément dans le carré, puis on regarde si ce point est dans le disque.
Si c'est le cas, on a notre point aléatoire sur le disque, et sinon on recommence. Normalement, il suffit de quelques itérations pour trouver un point valide.
Pour le cercle il existe d'autres méthodes n'utilisant pas de rejet (avec des coordonnées polaires selon des distributions bien choisies notamment), mais le tirage sélection/rejet reste très souvent utilisé car très rapide (en moyenne) et peu susceptible aux erreurs de précision.
Le tirage de nombres entiers entre 0 et N-1 c'est un peu la même chose : on pourrait par exemple générer un nombre de 1000 bits et prendre son modulo N, mais ce n'est pas parfaitement équiprobable si le nombre n'est pas un multiple de N.
De même, on peut se dire que la fonction random (qui génère un nombre réel entre 0 et 1) peut nous aider : la partie entière de random()*N est théoriquement uniforme sur les entiers de 0 à N-1. Mais comme la représentation des flottants n'est pas continue en pratique, même souci.
Pour assurer un tirage le plus uniforme possible, Python va ainsi utiliser la méthode par rejet : à l'aide de la fonction getrandbits, il génère un entier codé sur autant de bits que notre nombre N. Tant que le nombre tiré n'est pas dans [0, N-1], on en génère un nouveau.
Les tirages par rejet peuvent entraîner un "gâchis" d'entropie : si le nombre tiré ne respecte pas le critère de sélection, les bits utilisés pour sa génération sont perdus. Pour mon challenge de CTF, il fallait du zéro déchet pour que l'attaquant puisse récupérer tous les bits.
Il existe un seul cas dans lequel _randbelow serait garanti de ne faire qu'un seul tirage : si N est une puissance de 2, alors n'importe quel nombre tiré avec getrandbits() est dans [0, N-1] et il n'y a pas de rejet. Sauf que j'ai trouvé un bug dans l'implémentation de Python !
Pour calculer le nombre de bits à tirer aléatoirement, on va appeler la fonction n.bit_length(). Dans le cas où N est une puissance de deux, ce nombre de bits est incorrect : en prenant n=1024 (2^10), on ne devrait à avoir à tirer que 10 bits. Cependant, n.bit_length() vaut 11 !
Par conséquent, au lieu d'avoir un tirage parfait qui consomme exactement le bon nombre de bits et réussit du premier coup, on se retrouve avec des tirages qui n'ont que 50% de chances de réussir, et qui utilisent en moyenne 2x plus de temps et d'entropie que nécessaire. Du coup,
Je me suis mis en tête de proposer un patch du code source de Python. Comme pour tout projet titanesque, la procédure est assez lourde : il faut créer une issue détaillée sur le tracker externe, signer un accord de propriété intellectuelle, compiler et tester le fork, ...
Avec une modification de seulement 4 lignes de code, mon fork fonctionne bien et on observe une accélération effective de 2x sur les appels à _randbelow sur les puissances de 2. Victoire !
Mais petit souci : y a des dizaines de tests unitaires qui ne passent pas !
En effet, le module random de Python doit respecter le reproductibilité : pour une même seed, le RNG devrait produire exactement les mêmes résultats entre toutes les versions de Python. Mais ce n'est pas le cas ici parce qu'on a réparé les fuites d'entropie !
Pour conclure, ce bug assez spécifique ne permet pas de justifier un tel changement, et le bug devra donc rester éternellement dans Python. Mais ça n'a pas été peine perdue, le bug que j'ai ouvert a engendré un refactoring et un nouveau fonctionnement interne à getrandbits :)
Voilà, j'espère que ce thread vous a plu et que vous saurez briller en société avec vos nouvelles connaissances sur les tirages par sélection/rejet et sur les internals de Python ;)
Si mes threads techniques vous intéressent, je vous invite également à jeter un oeil à @Guardia_School: c'est une école que je parraine et qui propose un cursus post-bac en 3/5 ans pour apprendre les métiers de la cybersécurité, ouverture en octobre 2022 ! guardia.school
@Guardia_School Et comme d'habitude, si vous aimez ce que je fais, la chose la plus sympa que vous puissiez faire pour donner de la visibilité à mes contenus c'est de les partager : que ce soit via un retweet ou un message sur le Slack de votre boîte, pensez-y ;)

• • •

Missing some Tweet in this thread? You can try to force a refresh
 

Keep Current with Mathis Hammel

Mathis Hammel Profile picture

Stay in touch and get notified when new unrolls are available from this author!

Read all threads

This Thread may be Removed Anytime!

PDF

Twitter may remove this content at anytime! Save it as PDF for later use!

Try unrolling a thread yourself!

how to unroll video
  1. Follow @ThreadReaderApp to mention us!

  2. From a Twitter thread mention us with a keyword "unroll"
@threadreaderapp unroll

Practice here first or read more on our help page!

More from @MathisHammel

Oct 21
THREAD - Une immense avancée pour l'humanité : on a réussi à afficher Bad Apple et jouer à Doom sur une croix de pharmacie.

Petit complément technique à la vidéo de @Sylvqin pour vous montrer les coulisses du reverse-engineering de cet objet mystique. Image
Le socle de la croix de pharmacie est occupé par une carte de contrôle électronique relativement simple, entourée de composants d'électronique de puissance (mine de rien, 2560 LED ça consomme pas mal de watts) Image
Les composants ont résisté au temps et aux éléments, mais les gravures des puces ont quasiment disparu.

Heureusement, on peut les identifier grâce au circuit imprimé sur lequel figurent les références, et comprendre le layout de la carte : Image
Read 33 tweets
Jun 20
J'en ai trop marre de voir des smicards pleurer pour qu'on augmente pas les impôts des millionnaires.

J'ai gagné un peu plus de 7000€/mois pendant plusieurs années, si tu te considères pas riche à ce niveau c'est du déni ou de la malhonnêteté. Thread ⤵️
J'aime pas trop parler d'argent ici parce que ça peut passer pour de l'orgueil, mais on voit tellement de conneries se propager en ce moment que je me suis senti obligé de partager mon expérience (je supprimerai probablement le thread d'ici quelques jours)
Commençons par "tu t'offres un ou deux resto par mois" : je mange au resto quasiment chaque midi en semaine.

Le soir j'aime bien cuisiner, jamais aucune hésitation avant de prendre des produits bio ou premium.
Read 10 tweets
Mar 29
THREAD - les manipulations illicites du classement lors de la compétition la plus chère d'Europe.

Cette semaine, l'équipe de l'European Cyber Cup a trafiqué arbitrairement son scoreboard pour favoriser certaines équipes qui lui faisaient pression. 1/14 Image
Je suis coach de 10 étudiant·es de @Guardia_School qui ont participé cette semaine aux épreuves de l'European Cyber Cup.

Cette compétition est organisée par le forum InCyber (anciennement FIC, qui a discrètement changé de nom à cause de quelques casseroles).
Le prix d'inscription n'est pas rendu public, mais en 2021 je me souviens que mon entreprise avait dû payer 10.000€ pour nous inscrire, sachant que le cash prize pour l'équipe gagnante est de 5.000€ 🙃 Image
Read 16 tweets
Feb 8
J'ai besoin de votre aide pour m'aider à entraîner l'équipe de France de cybersécurité !

Quelques détails ci-dessous, merci de partager au maximum 🙏
En janvier, j'ai eu l'honneur d'être nommé entraîneur des compétiteurs français en cybersécurité, pour la prestigieuse compétition WorldSkills qui opposera 65 pays cette année.
Contrairement aux excellentes compétitions FCSC/ECSC qui sont très orientées CTF, le challenge WorldSkills va plutôt demander des compétences métier, appliquées à des environnements réalistes.
Read 6 tweets
Nov 8, 2023
THREAD

Je me suis intéressé à la cybersécurité de Crush, l'appli de rencontres pour 10-21 ans qui fait beaucoup de bruit.

J'y ai découvert un réseau de sociétés fictives qui récolte activement les données de dizaines de milliers de mineur·es. Explications ⤵️ Capture de l'appli "Crush, trouve ton crush secret" sur le store iOS.
PARTIE 1. Le fonctionnement de l'appli

Crush est une appli dont le but est de découvrir ses admirateurs secrets au collège/lycée.

Elle a été renommée "Friendzy, sondages entre amis" depuis son bad buzz.
Le principe est simple : après avoir ajouté vos amis sur l'appli, vous répondez à des questions sur vos liens d'amitié.

Dans un autre onglet, vous pouvez savoir ce que les gens ont répondu sur vous, sans voir leur nom. Sondage "La discrétion est son superpouvoir" avec 4 choix parmi mes amis sur l'appli (j'ai masqué deux noms qui appartiennent potentiellement à des vraies personnes)
Read 24 tweets
Aug 1, 2023
Droite au Coeur est toujours en maintenance, mais j'ai réussi à contourner la fermeture de l'authentification donc j'ai toujours accès au site.

La vulnérabilité initiale a été corrigée, je peux donc légalement vous expliquer la pire faille de sécurité du monde. 🧵1/9
L'interface de Droite au Coeur, comme beaucoup de sites web depuis ~2010, est dynamique.

Cela signifie que votre navigateur va d'abord télécharger le code qui gère l'affichage de la page, mais sans télécharger le contenu.

Pendant qq secondes, la page ressemble en gros à ça : Image
C'est ce squelette de page qui va ensuite faire d'autres requêtes pour récupérer le contenu, ici les profils de dating.

Pour ça, l'interface va utiliser une API : c'est un programme hébergé côté serveur, qui va récupérer et formater les données qu'on lui demande. Image
Read 10 tweets

Did Thread Reader help you today?

Support us! We are indie developers!


This site is made by just two indie developers on a laptop doing marketing, support and development! Read more about the story.

Become a Premium Member ($3/month or $30/year) and get exclusive features!

Become Premium

Don't want to be a Premium member but still want to support us?

Make a small donation by buying us coffee ($5) or help with server cost ($10)

Donate via Paypal

Or Donate anonymously using crypto!

Ethereum

0xfe58350B80634f60Fa6Dc149a72b4DFbc17D341E copy

Bitcoin

3ATGMxNzCUFzxpMCHL5sWSt4DVtS8UqXpi copy

Thank you for your support!

Follow Us!

:(