Tout d'abord, je vais vous aider à comprendre de manière simple ce qu'est une attaque de réentrance et comment vous pouvez l'empêcher, puis j'approfondirai les exemples de code afin de montrer où sont les vulnérabilités, quel serait le code de l'attaquant. et surtout, je vais vous montrer les dernières méthodes vérifiées pour protéger non seulement un mais tous les contrats intelligents de votre projet.

Spoiler : Si vous avez déjà entendu parler du modificateur nonReentrant(), continuez à lire car vous êtes sur le point de découvrir quelques lignes sous le modificateur globalNonReentrant() et le modèle contrôles-effets-interactions.



Dans l'image ci-dessus, nous avons ContractA et ContractB. Désormais, comme vous le savez, un contrat intelligent peut interagir avec un autre contrat intelligent, comme dans ce cas, ContractA peut appeler ContractB. Ainsi, l'idée de base de la réentrée est que ContractB est capable de rappeler ContractA pendant que ContractA est toujours en cours d'exécution.

Alors, comment l’attaquant peut-il utiliser cela ?

Ci-dessus nous avons ContractA qui a 10 Ethers et nous voyons que ContractB a stocké 1 Ether dans ContractA. Dans ce cas, ContractB pourra utiliser la fonction de retrait de ContractA et se renvoyer de l'Ether au fur et à mesure qu'il passe le contrôle où son solde est supérieur à 0, pour ensuite voir son solde total modifié à 0.



Voyons maintenant comment ContractB peut utiliser la réentrance pour exploiter la fonction de retrait et voler tous les Ethers de ContractA. Fondamentalement, l'attaquant aura besoin de deux fonctions : attack() et fallback().

Dans Solidity, une fonction de secours est une fonction externe sans nom, sans paramètres ni valeurs de retour. N'importe qui peut appeler une fonction de secours en : Appelant une fonction qui n'existe pas dans le contrat ; Appeler une fonction sans transmettre les données requises ; Envoi d'Ether sans aucune donnée au contrat.

La façon dont fonctionne la réentrée (suivons les flèches étape par étape) est que l'attaquant appelle la fonction attack() qui, à l'intérieur, appelle la fonction withdraw() de ContractA. À l’intérieur de la fonction, il vérifiera si le solde de ContractB est supérieur à 0 et si c’est le cas, il poursuivra l’exécution.



Étant donné que le solde de ContractB est supérieur à 0, il renvoie cet Ether et déclenche la fonction de secours. Notez qu'à l'heure actuelle, ContractA a 9 Ethers et ContractB a déjà 1 Ether.



Ensuite, lorsque la fonction de secours est exécutée, elle déclenche à nouveau la fonction de retrait de ContractA, vérifiant à nouveau si le solde de ContractB est supérieur à 0. Si vous vérifiez à nouveau l'image ci-dessus, vous remarquerez que son solde est toujours de 1 Ether.



Cela signifie que la vérification réussit et envoie un autre Ether à ContractB, ce qui déclenche la fonction de repli. Notez que puisque la ligne où nous avons « balance=0 » n'est jamais exécutée, cela continuera jusqu'à ce que tout l'Ether de ContractA ait disparu.

___________

Jetons maintenant un œil à un contrat intelligent où nous pouvons identifier la réentrance avec le code Solidity.



Dans le contrat EtherStore, nous avons la fonction deposit() qui stocke et met à jour les soldes de l'expéditeur, puis la fonction withdrawAll() qui prendra tous les soldes stockés en même temps. Veuillez noter l'implémentation de withdrawAll() où il vérifie d'abord avec l'exigence que le solde est supérieur à 0 et juste après envoie l'Ether, encore une fois, laissant pour la fin la mise à jour du solde de l'expéditeur à 0.



Nous avons ici le contrat Attack qui va utiliser la réentrée pour drainer le contrat EtherStore. Analysons son code :

  • Dans son constructeur, l'attaquant va transmettre l'adresse EtherStore afin de créer une instance et ainsi pouvoir utiliser ses fonctions.

  • Nous y voyons la fonction fallback() qui sera appelée lorsqu'EtherStore enverra Ether à ce contrat. À l'intérieur, il appellera retrait d'EtherStore tant que le solde est égal ou supérieur à 1.

  • Et à l'intérieur de la fonction attack() nous avons la logique qui exploitera EtherStore. Comme nous pouvons le voir, nous allons d'abord lancer l'attaque en nous assurant d'avoir suffisamment d'éther, puis déposer 1 ether afin d'avoir un solde supérieur à 0 dans EtherStore et ainsi passer les contrôles avant de commencer à retirer.

J'ai expliqué ci-dessus dans les exemples de ContractA et ContractB, étape par étape, comment le code s'exécutera, alors maintenant, faisons un résumé de comment cela se passera. Tout d’abord, l’attaquant appellera Attack(), qui appellera RemoveAll() depuis EtherStore, qui enverra ensuite Ether à la fonction de secours du contrat Attack. Et là, il commencera la réentrée et drainera le solde de l’EtherStore.

Alors, comment pouvons-nous protéger nos contrats des attaques de réentrée ?

Je vais vous montrer trois techniques de prévention pour les protéger pleinement. Je vais expliquer comment empêcher la réentrance dans une seule fonction, la réentrance entre fonctions et la réentrance entre contrats.



La première technique pour protéger une seule fonction consiste à utiliser un modificateur appelé noReentrant.

Un modificateur est un type spécial de fonction que vous utilisez pour modifier le comportement d'autres fonctions. Les modificateurs vous permettent d'ajouter des conditions ou des fonctionnalités supplémentaires à une fonction sans avoir à réécrire l'intégralité de la fonction.

Ce que nous faisons ici, c'est verrouiller le contrat pendant que la fonction est exécutée. De cette façon, il ne pourra pas réintégrer la fonction unique car il devra parcourir le code de la fonction, puis changer la variable d'état verrouillée en false afin de repasser la vérification effectuée dans le require.

___________



La deuxième technique consiste à utiliser le modèle Contrôles-Effets-Interactions qui protégera nos contrats de la réentrance interfonctionnelle. Pouvez-vous repérer dans le contrat EtherStore mis à jour ci-dessus ce qui a changé ?

Pour approfondir le modèle Check-Effects-Interaction, je vous recommande de lire https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html





Ci-dessus, nous voyons la comparaison entre le code vulnérable de l'image de gauche où le solde a été mis à jour après l'envoi de l'Ether, qui, comme vu ci-dessus, ne pourrait potentiellement jamais être atteint, et à droite ce qui a été fait est de déplacer les soldes[ msg.sender] = 0 (ou effet) juste après require(bal > 0) (check) mais avant d'envoyer de l'éther (interaction).

De cette façon, nous nous assurerons que même si une autre fonction accède à withdrawAll(), ce contrat sera protégé de l'attaquant car le solde sera toujours mis à jour avant d'envoyer l'Ether.

Modèle créé par https://twitter.com/GMX_IO

La troisième technique que je vais vous montrer consiste à créer le contrat GlobalReentrancyGuard pour vous protéger contre la réentrance entre contrats. Il est important de comprendre que cela s’applique aux projets comportant plusieurs contrats interagissant les uns avec les autres.

L'idée ici est la même que dans le modificateur noReentrant que j'ai expliqué dans la première technique, il entre dans le modificateur, met à jour une variable pour verrouiller le contrat et ne le déverrouille pas tant qu'il n'a pas terminé le code. La grande différence ici est que nous utilisons une variable stockée dans un contrat séparé qui sert à vérifier si la fonction a été saisie ou non.







J'ai créé ici un exemple sans code réel et juste avec des noms de fonctions pour référence pour comprendre l'idée car, d'après mon expérience, cela peut aider à visualiser la situation plus que simplement l'écrire avec des mots.

Ici, l'attaquant appellerait la fonction du contrat ScheduledTransfer qui, après avoir rempli les conditions, enverrait l'Ether spécifié au contrat AttackTransfer qui, par conséquent, entrerait dans la fonction de secours et « annulerait » donc la transaction à partir du point de contact du contrat ScheduledTransfer. voir et pourtant recevoir l’Éther. Et de cette façon, il commencerait un examen jusqu'à vider tous les Ethers de ScheduledTransfer.

Eh bien, en utilisant le GlobalReentrancyGuard que j'ai mentionné ci-dessus, cela évitera un tel scénario d'attaque.

__________________

Twitter @TheBlockChainer pour trouver plus de mises à jour quotidiennes sur les contrats intelligents, la sécurité Web3, la solidité, l'audit des contrats intelligents, et plus encore.

__________________