Nous avons presque tous utilisé Google Sheets ou Microsoft Excel pour saisir des données pour certains calculs. Disons que vous souhaitez entrer les noms des employés, leurs numéros de téléphone, leurs titres et le salaire qu'ils gagnent.
Dans sa forme la plus simple, voici à quoi ressemblerait un enregistrement ou un cas, dans Sheets ou Excel :
Comme vous pouvez le constater, le nom et le titre de l'employé sont constitués de texte, tandis que le numéro de téléphone et le salaire sont constitués d'une séquence de chiffres.
Ainsi, d'un point de vue sémantique, nous, en tant qu'êtres humains, comprenons ce que ces champs signifient dans le monde réel et pouvons les différencier.
De toute évidence, même si vous n'avez pas besoin d'un diplôme en informatique pour faire la différence, comment un compilateur ou un interpréteur traite-t-il ces données ?
C'est là qu'interviennent les types de données, et c'est quelque chose que les programmeurs prennent le temps de spécifier ou non, selon le langage de programmation dans lequel ils codent.
En d'autres termes, les points de données sous le nom de l'employé et le titre sont appelés chaînes. Bien sûr, le salaire est clairement un nombre entier, du fait qu'il n'a pas de point décimal. En termes simples, ce sont des types de données qui doivent être déclarés comme tels lorsque vous codez, afin que seules les bonnes opérations associées à ce type de données soient effectuées.
Voici comment nous déclarons un type de données entier dans Solidity :
Cela dit, le champ Numéro de téléphone dans la feuille de calcul ci-dessus contient un point de données qui sera utilisé comme une chaîne unique, mais cette discussion est pour un autre jour. Pour l'instant, nous nous concentrerons sur le type de données primitif avec lequel nous avons tous effectué des opérations arithmétiques de base.
Oui, nous parlons du type de données entier qui, tout en étant important pour les opérations arithmétiques clés, a une plage limitée pour tout calcul.
Probablement, l'exemple le plus populaire de débordement d'entier dans le monde réel se produit sur les véhicules. Autrement connus sous le nom d'odomètre, ces appareils suivent généralement le nombre de kilomètres parcourus par un véhicule.
Alors, que se passe-t-il une fois que la valeur des miles parcourus atteint la valeur entière non signée de 999999 dans un odomètre à six chiffres ?
Idéalement, une fois un autre mile ajouté, cette valeur devrait atteindre 1000000, n'est-ce pas ? Mais cela ne se produit pas car il existe une disposition pour un septième chiffre.
Au lieu de cela, la valeur des miles parcourus est réinitialisée à 000000, comme indiqué ci-dessous :
Par définition, étant donné que le septième chiffre n'est pas disponible, cela entraîne un « débordement » car la valeur exacte n'est pas représentée.
Vous obtenez l'image, non?
A l'inverse, l'inverse peut aussi se produire même si ce n'est pas si courant. En d'autres termes, lorsque la valeur enregistrée est inférieure à la plus petite valeur disponible dans la plage et que l'on appelle autrement 'underflow'.
Comme nous le savons tous, les ordinateurs stockent les nombres entiers en mémoire comme leur équivalent binaire. Maintenant, par souci de simplicité, disons que vous utilisez un registre 8 bits.
= 2⁸*1 + 2⁷*1 + 2⁶*1 + 2⁵*1 + 2⁴*1 + 2³*1 + 2²*1 + 2¹*1 + 2⁰*1
= 256 + 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1
= 111111111
Où chaque bit vaut 1, et comme vous pouvez le constater, vous ne pouvez pas stocker une valeur supérieure.
D'autre part, si vous souhaitez stocker le nombre 0 dans le registre 8 bits, voici à quoi cela ressemblerait :
= 2⁸*0 + 2⁷*0 + 2⁶*0 + 2⁵*0 + 2⁴*0 + 2³*0 + 2²*0 + 2¹*0 + 2⁰*0
= 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
= 000000000
Où chaque bit est 0, ce qui devrait vous dire que vous ne pouvez pas stocker une valeur inférieure.
En d'autres termes, la plage d'entiers autorisés pour un tel registre 8 bits est de 0 à 511. Alors, est-il possible de stocker l'entier 512 ou -1 dans un tel registre ?
Bien sûr que non. Par conséquent, vous stockerez une valeur qui ressemble à la valeur de réinitialisation des miles parcourus dans l'exemple de l'odomètre, mais sous forme de valeurs binaires.
De toute évidence, vous auriez besoin de registres avec quelques bits supplémentaires pour accueillir confortablement un tel nombre. Ou bien, risquez à nouveau la situation de débordement.
Dans le cas des entiers signés, nous stockons également les entiers négatifs. Ainsi, lorsque nous essayons de stocker un nombre inférieur à la plage acceptée, ou inférieur à zéro, comme indiqué ci-dessus, un sous-dépassement se produit.
Encore une fois, puisque le but de tout calcul est d'obtenir des résultats déterministes, cela peut au mieux être ennuyeux mais au pire causer la perte de millions. En particulier, lorsque ces erreurs de débordement ou de sous-dépassement d'entier se produisent dans des contrats intelligents.
Alors que le débordement et le sous-dépassement d'entiers existent depuis des décennies, leur existence en tant que bogue dans un contrat intelligent a fait monter les enchères. Lorsque les attaquants utilisent de telles erreurs, ils peuvent vider le contrat intelligent de grandes quantités de jetons.
La première fois que ce type de bogue s'est produit, c'est probablement avec le bloc 74638, qui a créé des milliards de Bitcoin pour trois adresses. Il faudrait des heures pour résoudre cette erreur au moyen d'un soft fork et qui a rejeté le bloc, rendant ainsi la transaction invalide.
D'une part, les transactions d'une valeur supérieure à 21 millions de bitcoins ont été rejetées. Ce n'était pas différent pour les transactions de débordement, tout comme celle qui a envoyé tant d'argent aux trois comptes susmentionnés.
Cependant, les contrats intelligents Ethereum ont également connu un débordement et un sous-dépassement d'entiers, la BeautyChain étant également un exemple frappant.
Dans ce cas, le contrat intelligent contenait une ligne de code défectueuse :
En conséquence, les attaquants pouvaient théoriquement recevoir un nombre illimité de jetons BEC, ce qui pouvait théoriquement représenter une valeur de (2²⁵⁶)-1.
Examinons maintenant un autre exemple de contrat intelligent dans lequel un sous-dépassement/débordement d'entier se produit.
A première vue, il y a deux contrats qui interagissent dans cet exemple, et qui démontre ce qui se passe dans le cas d'un débordement d'entier.
Comme vous pouvez le voir ci-dessous, le contrat TimeLock, vous permet de déposer et de retirer des fonds mais avec une différence : vous ne pouvez effectuer ce dernier qu'après un certain laps de temps. Dans ce cas, vous ne pouvez retirer vos fonds que dans une semaine.
Cependant, une fois que vous appelez la fonction d'attaque dans le contrat d'attaque, le verrouillage du temps en place n'est plus effectif et c'est pourquoi l'attaquant peut retirer le montant du solde immédiatement.
En d'autres termes, en raison d'un débordement d'entier avec l'instruction type(uint).max+1-timeLock.locktime(address(this)), le verrouillage temporel est éliminé.
Par exemple, une fois que vous avez déployé les deux contrats intelligents à l'aide du code ci-dessus, vous pouvez tester si le timelock tient en appelant les fonctions de dépôt et de retrait dans le contrat TimeLock, comme indiqué ci-dessous :
Comme vous pouvez le voir, en sélectionnant un montant de 2 Ether, nous obtenons le solde du contrat intelligent de 2 Ether indiqué ci-dessus :
Plus précisément, l'adresse spécifique qui contient le solde de 2 Ether peut être vérifiée en ajoutant l'adresse dans le champ de la fonction soldes et en cliquant sur le bouton soldes :
Cependant, comme mentionné ci-dessus, vous ne pouvez pas encore retirer ces fonds, en raison du blocage du temps en place. Lorsque vous regardez la console après avoir appuyé sur la fonction de retrait, vous trouverez une erreur indiquée par le symbole 'x' rouge. Comme vous pouvez le voir ci-dessous, la raison de cette erreur est fournie par le contrat est "Le temps de verrouillage n'a pas expiré":
Examinons maintenant le contrat d'attaque déployé, comme indiqué ci-dessous :
Maintenant, pour invoquer la fonction d'attaque, vous devez déposer une valeur de 1 Ether ou plus. Ainsi, dans ce cas, nous avons sélectionné 2 Ether, comme indiqué ci-dessous :
Après cela, appuyez sur "attaquer". Vous constaterez que les 2 Ether que vous avez déposés seront retirés immédiatement et ajoutés au contrat d'attaque comme en témoigne le solde de 2 Ether ci-dessous :
De toute évidence, cela n'est pas censé se produire car le verrouillage de longue durée doit prendre effet dès que vous effectuez le dépôt. Bien sûr, comme nous le savons, l'instruction type(uint).max+1-timeLock.locktime(address(this)) réduit le temps de verrouillage en utilisant la fonction raiseLockTime. C'est précisément pourquoi nous sommes en mesure de retirer immédiatement le solde d'Ether.
Ce qui nous amène à la question évidente : existe-t-il des moyens de corriger la vulnérabilité de débordement et de sous-dépassement d'entier ?
Reconnaissant que la vulnérabilité de débordement/sous-dépassement d'entier peut être dévastatrice, quelques correctifs à ce bogue ont été déployés. Examinons ces deux correctifs et comment ils fonctionnent autour d'une telle erreur :
Open Zeppelin, en tant qu'organisation, offre beaucoup en matière de technologie et de services de cybersécurité, la bibliothèque SafeMath faisant partie de son référentiel de développement de contrats intelligents. Ce référentiel contient des contrats qui peuvent être importés dans votre code de contrat intelligent, la bibliothèque SafeMath étant l'un d'entre eux.
Voyons comment l'une des fonctions de SafeMath.sol vérifie le dépassement d'entier :
Maintenant, une fois que le calcul de a+b a eu lieu, une vérification pour voir si c<a a lieu. Bien sûr, cela ne serait vrai que dans le cas d'un débordement d'entier.
Avec la version du compilateur de Solidity atteignant 0.8.0 et au-dessus, les contrôles de débordement et de sous-dépassement d'entiers sont désormais intégrés. Ainsi, on peut toujours utiliser cette bibliothèque pour vérifier cette vulnérabilité, à la fois lors de l'utilisation du langage et de cette bibliothèque. Bien sûr, si votre contrat intelligent nécessite une version de compilateur inférieure à 0.8.+, vous devez utiliser cette bibliothèque pour éviter le débordement ou le sous-dépassement.
Maintenant, comme mentionné précédemment, si pour votre contrat intelligent, vous utilisez une version de compilateur 0.8.0 et supérieure, cette version dispose d'un vérificateur intégré pour une telle vulnérabilité.
En fait, juste pour vérifier s'il fonctionne avec le contrat intelligent ci-dessus, lors du changement de la version du compilateur en « ^0.8.0 » et de son redéploiement, l'erreur « revert » suivante est reçue :
Bien entendu, aucun dépôt des 2 Ethers n'est effectué, ce qui est dû au contrôle du dépassement de la valeur du time lock. En conséquence, aucun retrait n'est possible car aucun fonds n'a été déposé en premier lieu.
Sans aucun doute, l'appel de la fonction Attack.attack() n'a pas fonctionné ici, donc tout va bien !
S'il y a quelque chose que vous devriez retenir de ce long article de blog, c'est qu'ignorer cette vulnérabilité, comme depuis l'attaque BEC, peut s'avérer coûteux. Comme vous pouvez également le constater, si rien n'est fait, il est facile que des erreurs non malveillantes se produisent. Ou tout aussi simple pour les pirates d'exploiter cette vulnérabilité.
En parlant de cela, et en utilisant notre compréhension de la façon dont l'attaque BEC s'est déroulée, la reconnaissance de cette vulnérabilité peut grandement contribuer à prévenir toute attaque lors de la rédaction de vos contrats intelligents, grâce aux correctifs proposés. Même s'il existe plusieurs autres vulnérabilités de contrats intelligents qui attendent de vous faire trébucher.