Aider à mettre à jour cette page

🌏

Il existe une nouvelle version de cette page, mais seulement en anglais pour le moment. Aidez-nous à traduire la dernière version.

Cette page est incomplète. Si vous êtes un expert sur le sujet, veuillez éditer cette page et l'enrichir de votre sagesse.

Sécurité

Dernière modification: , Invalid DateTime
Edit page

Les contrats intelligents Ethereum sont extrêmement flexibles, capables à la fois de détenir de grandes quantités de jetons (souvent plus de 1 milliard de $) et d'exécuter une logique immuable sur du code intelligent précédemment déployé. Alors que cela a créé un écosystème dynamique et créatif de contrats intelligents interconnectés et trustless, c'est aussi l'écosystème parfait pour attirer les attaquants qui cherchent à en tirer profit en exploitant des vulnérabilités dans les contrats intelligents et des comportements inattendus dans Ethereum. Le code d'un contrat intelligent ne peut habituellement pas être modifié pour corriger les défauts de sécurité, les actifs volés à partir de contrats intelligents sont irrécupérables et extrêmement difficiles à tracer. Le montant total volé ou perdu en raison de problèmes sur les contrats intelligents atteint facilement le milliard de dollars. Voici quelques-uns des problèmes les plus importants dus à des erreurs de codage de contrat intelligent :

Prérequis

Cette page couvre la sécurité des contrats intelligents, donc assurez-vous d'être familiarisé avec les contrats intelligents avant d'aborder la sécurité.

Comment écrire un code de contrats intelligents plus sécurisé

Avant de lancer un code sur le réseau principal, il est important de prendre des précautions suffisantes pour protéger tout ce qui a de la valeur et qui est confié à votre contrat intelligent. Dans cet article, nous allons discuter de quelques attaques spécifiques, fournir des ressources pour en savoir plus sur les types d'attaques, et vous donner quelques outils et bonnes pratiques de base pour assurer le bon fonctionnement et la sécurité de vos contrats.

Les audits ne sont pas une solution miracle

Il y a des années, les outils permettant d'écrire, de compiler, de tester et de déployer des contrats intelligents manquaient de maturité, ce qui a généré de nombreux projets désordonnés d'écriture de code Solidity, code qui était ensuite transmis à un expert pour examen afin de s'assurer qu'il fonctionnait de façon sécurisée et comme prévu. En 2020, les processus de développement et les outils qui prennent en charge l'écriture de Solidity sont nettement meilleurs. En exploiter les bonnes pratiques permet non seulement de garantir que votre projet est plus facile à gérer, mais cela constitue un élément essentiel de sa sécurité. Un audit en fin de rédaction de votre contrat intelligent ne suffit plus comme seule considération de sécurité. La sécurité commence bien avant d'écrire votre première ligne de code de contrat intelligent, elle commence par des processus de conception et de développement adéquats.

Processus de développement de contrats intelligents

Il faut au minimum que :

  • tout le code soit stocké dans un système de contrôle de version comme git ;
  • toutes les modifications de code soient effectuées via des PR (Pull Requests) ;
  • toutes les PR aient au moins un réviseur ; Si vous travaillez sur un projet en solo, envisagez de trouver un autre auteur solo pour négocier des révisions de code !
  • une seule commande compile, déploie et exécute une suite de tests sur votre code en utilisant un environnement de développement Ethereum (Voir : Truffle) ;
  • vous ayez exécuté votre code via des outils d'analyse basiques comme Mythril et Slither, idéalement avant que chaque PR soit fusionnée, en comparant les différences de sortie ;
  • Solidity n'émette aucune alerte à la compilation ;
  • votre code soit bien documenté.

Il y a bien d'autres choses à dire sur les processus de développement, mais ces éléments constituent un bon point de départ. Pour plus d'éléments et d'explications détaillées, consultez la liste de contrôle qualité des processus, fournie par DeFiSafety. DefiSafety est un service public non officiel qui publie des avis sur diverses DApps Ethereum publiques de grande taille. Une partie du système d'évaluation de DeFiSafety comprend la façon dont le projet adhère à cette liste de contrôle qualité des processus. En suivant ces processus :

  • vous produirez du code plus sécurisé, via des tests reproductibles et automatisés ;
  • les experts seront en mesure de vérifier votre projet plus efficacement ;
  • l'intégration de nouveaux développeurs sera plus simple ;
  • les développeurs pourront itérer, tester et obtenir des commentaires sur les modifications rapidement ;
  • votre projet enregistrera probablement moins de régressions.

Attaques et vulnérabilités

Maintenant que vous écrivez du code Solidity en utilisant un processus de développement efficace, examinons quelques vulnérabilités courantes de Solidity pour voir ce qui peut mal tourner.

Réentrance

La réentrance est l'un des problèmes de sécurité les plus importants à considérer lors du développement de contrats intelligents. L'EVM ne pouvant pas exécuter plusieurs contrats en même temps, un contrat appelant un autre contrat met en pause l'exécution du contrat appelant et l'état de la mémoire jusqu'à ce que l'appel revienne, événement à partir duquel l'exécution reprend normalement. Cette pause et cette reprise peuvent créer une vulnérabilité connue sous le nom de "réentrance".

Voici une version simple d'un contrat vulnérable à la réentrance :

1// CE CONTRAT A UNE VULNÉRABILITÉ INTENTIONNELLE, NE PAS COPIER
2contract Victim {
3 mapping (address => uint256) public balances;
4
5 function deposit() external payable {
6 balances[msg.sender] += msg.value;
7 }
8
9 function withdraw() external {
10 uint256 amount = balances[msg.sender];
11 (bool success, ) = msg.sender.call.value(amount)("");
12 require(success);
13 balances[msg.sender] = 0;
14 }
15}
16
Afficher tout
📋 Copier

Pour permettre à un utilisateur de retirer l'ETH qu'il a précédemment stocké dans le contrat, cette fonction :

  1. lit le solde de l'utilisateur ;
  2. lui envoie le solde en ETH ;
  3. réinitialise le solde à 0, de sorte que l'utilisateur ne puisse pas retirer le solde de nouveau.

Si elle est appelée à partir d'un compte régulier (comme votre propre compte Metamask), cette fonction est comme attendue : msg.sender.call.value() envoie simplement l'ETH vers votre compte. Toutefois, les contrats intelligents peuvent également effectuer des appels. Si un contrat personnalisé et malveillant appelle la fonction withdraw(), msg.sender.call.value() n'enverra pas le montant d'ETH (via amount), mais appellera aussi implicitement le contrat pour commencer à exécuter du code. Imaginez le contrat malveillant suivant :

1contract Attacker {
2 function beginAttack() external payable {
3 Victim(VICTIM_ADDRESS).deposit.value(1 ether)();
4 Victim(VICTIM_ADDRESS).withdraw();
5 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(VICTIM_ADDRESS).withdraw();
10 }
11 }
12}
13
Afficher tout
📋 Copier

Appeler Attacker.beginAttack() démarrera un cycle qui ressemble à quelque chose comme ça :

10.) Attacker's EOA calls Attacker.beginAttack() with 1 ETH
20.) Attacker.beginAttack() deposits 1 ETH into Victim
3
4 1.) Attacker -> Victim.withdraw()
5 1.) Victim reads balanceOf[msg.sender]
6 1.) Victim sends ETH to Attacker (which executes default function)
7 2.) Attacker -> Victim.withdraw()
8 2.) Victim reads balanceOf[msg.sender]
9 2.) Victim sends ETH to Attacker (which executes default function)
10 3.) Attacker -> Victim.withdraw()
11 3.) Victim reads balanceOf[msg.sender]
12 3.) Victim sends ETH to Attacker (which executes default function)
13 4.) Attacker no longer has enough gas, returns without calling again
14 3.) balances[msg.sender] = 0;
15 2.) balances[msg.sender] = 0; (it was already 0)
16 1.) balances[msg.sender] = 0; (it was already 0)
17
Afficher tout

Appeller Attacker.beginAttack avec 1 ETH génère une nouvelle attaque par réentrance contre la victime, retirant plus d'ETH qu'il n'en a été fourni (prélevé sur les soldes des autres utilisateurs, entraînant une sous-garantie du contrat de la victime)

Comment gérer la réentrance (de la mauvaise façon)

On pourrait envisager de contrecarrer la réentrance en empêchant simplement les contrats intelligents d'interagir avec votre code. Vous recherchez stackoverflow, vous trouvez cet extrait de code avec des tonnes de votes positifs :

1function isContract(address addr) internal returns (bool) {
2 uint size;
3 assembly { size := extcodesize(addr) }
4 return size > 0;
5}
6
📋 Copier

Cela semble sensé : les contrats ont du code, si l'appelant a du code, cele ne l'autorise pas à déposer. Tentons de l'ajouter :

1// CE CONTRAT A UNE VULNÉRABILITÉ INTENTIONNELLE, NE PAS COPIER
2contract ContractCheckVictim {
3 mapping (address => uint256) public balances;
4
5 function isContract(address addr) internal returns (bool) {
6 uint size;
7 assembly { size := extcodesize(addr) }
8 return size > 0;
9 }
10
11 function deposit() external payable {
12 require(!isContract(msg.sender)); // <- NEW LINE
13 balances[msg.sender] += msg.value;
14 }
15
16 function withdraw() external {
17 uint256 amount = balances[msg.sender];
18 (bool success, ) = msg.sender.call.value(amount)("");
19 require(success);
20 balances[msg.sender] = 0;
21 }
22}
23
Afficher tout
📋 Copier

Maintenant pour déposer de l'ETH, vous ne devez pas avoir de code de contrat intelligent à votre adresse. Ceci est néanmoins facilement surmonté avec le contrat d'attaquant suivant :

1contract ContractCheckAttacker {
2 constructor() public payable {
3 ContractCheckVictim(VICTIM_ADDRESS).deposit(1 ether); // <- New line
4 }
5
6 function beginAttack() external payable {
7 ContractCheckVictim(VICTIM_ADDRESS).withdraw();
8 }
9
10 function() external payable {
11 if (gasleft() > 40000) {
12 Victim(VICTIM_ADDRESS).withdraw();
13 }
14 }
15}
16
Afficher tout
📋 Copier

Alors que la première attaque était une attaque sur la logique du contrat, il s'agit maintenant d'une attaque sur le comportement de déploiement du contrat Ethereum. Pendant la construction, un contrat n'a pas encore renvoyé le code à déployer à son adresse, mais conserve le contrôle complet de l'EVM DURANT ce processus.

Il est techniquement possible d'empêcher les contrats intelligents d'appeler votre code, en utilisant cette ligne :

1require(tx.origin == msg.sender)
2
📋 Copier

Cependant, ce n'est toujours pas une bonne solution. L'un des aspects les plus passionnants d'Ethereum est sa composabilité : ses contrats intelligents s'intègrent et se construisent les uns avec les autres. En utilisant la ligne ci-dessus, vous limitez l'utilité de votre projet.

Comment gérer la réentrance (de la bonne façon)

En changeant simplement l'ordre de la mise à jour de stockage et de l'appel externe, nous empêchons la condition de réentrance qui a permis l'attaque. Un rappel vers un retrait, bien que possible, ne profitera pas à l'attaquant puisque le stockage des soldes sera déjà défini à 0.

1contract NoLongerAVictim {
2 function withdraw() external {
3 uint256 amount = balances[msg.sender];
4 balances[msg.sender] = 0;
5 (bool success, ) = msg.sender.call.value(amount)("");
6 require(success);
7 }
8}
9
📋 Copier

Le code ci-dessus suit le modèle de conception "Checks-Effects-Interactions", qui aide à se protéger contre la réentrance. En savoir plus sur le modèle Checks-Effects-Interactions

Comment gérer la réentrance (l'option nucléaire)

Chaque fois que vous envoyez de l'ETH à une adresse non fiable ou que vous interagissez avec un contrat inconnu (comme appeler le "transfer()" d'une adresse de jeton fournie par l'utilisateur), vous vous exposez à un risque de réentrance. En concevant des contrats qui n'envoient pas d'ETH et n'appellent pas de contrats non fiables, vous empêchez la possiblité de réentrance !

Autres types d'attaques

Les types d'attaque ci-dessus couvrent les problèmes de codage de contrats intelligents (réentrance) et les anomalies Ethereum (exécution de code dans les constructeurs de contrats, avant que le code ne soit disponible à l'adresse du contrat). Il existe beaucoup, beaucoup d'autres types d'attaque à connaître, y compris :

  • Front running
  • Rejet d'envoi d'ETH
  • Dépassement/sous-flux d'entier

Complément d'information:

  • Consensys Smart Contract Known Attacks - Une explication très lisible des vulnérabilités les plus importantes, avec un exemple de code pour la plupart.
  • Registre SWC - Liste conservée des CWE qui s'appliquent à Ethereum et aux contrats intelligents

Outils de sécurité

Bien qu'il n'y ait pas de substitut à la compréhension des bases de sécurité d'Ethereum et à l'engagement d'un expert pour revoir votre code, il existe de nombreux outils disponibles pour aider à mettre en évidence les problèmes potentiels présents dans votre code.

Sécurité des contrats intelligents

Slither - Framework d'analyse statique Solidity rédigé en Python 3

MythX - API d'analyse de sécurité pour les contrats intelligents Ethereum

Mythril - Outil d'analyse de sécurité pour le bytecode de l'EVM

SmartContract.Codes - Moteur de recherche pour les codes sources Solidity vérifiés

Manticore - Interface en ligne de commande qui utilise un outil d'exécution symbolique sur les contrats intelligents et les fichiers binaires

Securify - Scanner de sécurité pour les contrats intelligents Ethereum

ERC20 Verifier - Outil utilisé pour vérifier qu'un contrat est conforme à la norme ERC20

Vérification formelle

Informations sur la vérification formelle

Utilisation d'outils

Deux des outils les plus populaires pour l'analyse de sécurité des contrats intelligents sont :

Tous deux sont des outils utiles qui analysent votre code et signalent les problèmes. Chacun possède une version hébergée par [commercial], mais est également disponible gratuitement pour fonctionner localement. Voici un bref exemple de la façon d'exécuter Slither, disponible dans une image Docker pratique trailofbits/eth-security-toolbox. Vous devrez installer Node.js si vous ne l’avez pas déjà fait.

$ mkdir test-slither
$ curl https://gist.githubusercontent.com/epheph/460e6ff4f02c4ac582794a41e1f103bf/raw/9e761af793d4414c39370f063a46a3f71686b579/gistfile1.txt > bad-contract.sol
$ docker run -v `pwd`:/share -it --rm trailofbits/eth-security-toolbox
docker$ cd /share
docker$ solc-select 0.5.11
docker$ slither bad-contract.sol

génèrera cette sortie :

ethsec@1435b241ca60:/share$ slither bad-contract.sol
INFO:Detectors:
Reentrancy in Victim.withdraw() (bad-contract.sol#11-16):
External calls:
- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)
State variables written after the call(s):
- balances[msg.sender] = 0 (bad-contract.sol#15)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities
INFO:Detectors:
Low level call in Victim.withdraw() (bad-contract.sol#11-16):
- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#low-level-calls
INFO:Slither:bad-contract.sol analyzed (1 contracts with 46 detectors), 2 result(s) found
INFO:Slither:Use https://crytic.io/ to get access to additional detectors and GitHub integration
Afficher tout

Slither a identifié le potentiel de réentrance, ainsi que les lignes clés où le problème pourrait se produire, et nous donne un lien pour plus de détails sur le problème ici :

Reference : https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities

Ceci vous permet de connaître rapidement les problèmes potentiels dans votre code. Comme tous les outils de test automatisés, Slither n'est pas parfait, et peut signaler trop d'erreurs dans le rapport. Il peut avertir d'une éventuelle réentrance, même si aucune vulnérabilité exploitable n'existe. Souvent, l'examen des DIFFERENCES entre les changements de code répertoriées dans le rapport Slither est extrêmement utile, car il aide à découvrir les vulnérabilités introduites beaucoup plus tôt que si vous attendez la fin de votre projet.

Complément d'information

Guide des bonnes pratiques de sécurité pour les contrat intelligents

Norme de vérification de la sécurité des contrats intelligents (SCSVS)

Une ressource communautaire vous a aidé ? Modifiez cette page et ajoutez-la !