Attaques par réentrance dans les Smart Contracts : Comprendre la vulnérabilité et mettre en œuvre des stratégies de prévention

Les attaques par réentrance représentent l'une des vulnérabilités les plus notoires en matière de sécurité des contrats intelligents, responsables de millions de fonds volés à travers divers protocoles blockchain. Cet article explore les mécanismes derrière les vulnérabilités de réentrance et présente des techniques de prévention complètes que les développeurs soucieux de la sécurité devraient mettre en œuvre.

Qu'est-ce qu'une attaque par réentrance ?

Au cœur d'une attaque par réentrance, une fonction dans un contrat intelligent est appelée plusieurs fois avant que son exécution précédente ne soit terminée. La vulnérabilité fondamentale survient lorsqu'un contrat intelligent appelle un contrat externe avant de résoudre ses propres changements d'état, créant une opportunité d'exploitation.

Dans un scénario typique, le Contrat A interagit avec le Contrat B en appelant l'une de ses fonctions. La faille de sécurité critique émerge lorsque le Contrat B acquiert la capacité d'appeler à nouveau le Contrat A alors que le Contrat A exécute toujours sa fonction originale. Ce modèle d'interaction récursive crée la base d'une exploitation potentielle.

Comment fonctionnent les attaques de réentrance : une décomposition étape par étape

Considérez ce scénario : le Contrat A détient un total de 10 ETH, le Contrat B ayant déposé 1 ETH dans le Contrat A. La vulnérabilité devient exploitable lorsque le Contrat B tente de retirer ses fonds par la séquence suivante :

  1. Le contrat B appelle la fonction withdraw() dans le contrat A
  2. Le contrat A vérifie que le solde du contrat B est supérieur à 0 (passe le contrôle)
  3. Le contrat A envoie l'ETH au contrat B, déclenchant la fonction de fallback du contrat B
  4. Avant que le Contrat A puisse mettre à jour le solde du Contrat B à zéro, la fonction de repli dans le Contrat B appelle withdraw() à nouveau
  5. Le contrat A vérifie le solde du contrat B, qui affiche toujours 1 ETH (pas encore mis à jour)
  6. Le processus se répète jusqu'à ce que les fonds du Contrat A soient épuisés.

La vulnérabilité clé est que la mise à jour du solde se produit après le transfert d'ETH, permettant à l'attaquant d'exploiter le même solde plusieurs fois avant qu'il ne soit fixé à zéro.

Anatomie d'une attaque : Mise en œuvre technique

Examinons le modèle de code vulnérable :

solidité // Contrat EtherStore vulnérable contrat EtherStore { mapping(address => uint) soldes publics;

fonction deposit() public payable {
    balances[msg.sender] += msg.value;
}

fonction withdrawAll() publique {
    uint bal = balances[msg.sender];
    require(bal > 0);
    
    // Vulnérabilité : Appel externe avant la mise à jour de l'état
    (bool sent, ) = msg.sender.call{value: bal}("");
    require(sent, "Échec de l'envoi d'Ether");
    
    // La mise à jour de l'état se produit trop tard
    balances[msg.sender] = 0;
}

}

Maintenant, voyons comment un attaquant pourrait exploiter cette vulnérabilité :

solidité // Contrat d'attaque exploitant la vulnérabilité de réentrance contrat Attack { EtherStore public etherStore;

constructeur(adresse _etherStoreAddress) {
    etherStore = EtherStore(_adresseEtherStore);
}

// Fonction de repli qui est appelée lorsque EtherStore envoie de l'Ether
recevoir() externe payable {
    if(adresse(etherStore).solde >= 1 ether) {
        // Rentrer à nouveau dans la fonction withdrawAll
        etherStore.retirerTout();
    }
}

fonction attack() externe payable {
    require(msg.value >= 1 ether);
    
    // Déposer pour établir un solde dans EtherStore
    etherStore.deposit{value: 1 ether}();
    
    // Démarrer le processus de retrait, déclenchant l'attaque de réentrance
    etherStore.withdrawAll();
}

}

La séquence d'attaque commence lorsque l'attaquant appelle attack(). Cela dépose 1 ETH pour établir un solde, puis appelle withdrawAll(). Lorsque EtherStore renvoie ETH, cela déclenche la fonction receive() dans le contrat Attack, qui appelle à nouveau withdrawAll() avant que le solde ne soit mis à jour. Cette boucle continue jusqu'à ce que l'EtherStore soit vidé de ses fonds.

Techniques de prévention complètes

Les développeurs soucieux de la sécurité peuvent mettre en œuvre trois techniques robustes pour se protéger contre les vulnérabilités de réentrance :

1. Protection au niveau de la fonction : le modificateur nonReentrant

La protection la plus courante au niveau de la fonction individuelle consiste à mettre en œuvre un garde de réentrance :

solidité contrat ReentrancyGuard { bool privé verrouillé = false;

modificateur nonReentrant() {
    require(!locked, "Appel réentrant");
    verrouillé = vrai;
    _;
    verrouillé = faux;
}

// Appliquez ce modificateur aux fonctions vulnérables
fonction retirerDesFonds() publique nonReentrant {
    // Code de fonction protégé contre la réentrance
}

}

Cette approche verrouille efficacement le contrat pendant l'exécution de la fonction, empêchant ainsi tout appel récursif jusqu'à ce que la fonction se termine et déverrouille l'état.

2. Protection inter-fonctionnelle : modèle des vérifications, effets et interactions

Ce modèle de sécurité fondamental restructure le code pour éliminer les vulnérabilités dans plusieurs fonctions :

solidité // MISE EN ŒUVRE VULNÉRABLE function retirerTout() public { uint bal = balances[msg.sender]; require(bal > 0);

(bool envoyé, ) = msg.sender.call{value: bal}("");
require(sent, "Échec de l'envoi d'Ether");

balances[msg.sender] = 0; // Mise à jour de l'état après l'interaction

}

// MISE EN ŒUVRE SÉCURISÉE fonction withdrawAll() publique { uint bal = balances[msg.sender]; require(bal > 0); // Vérifie

balances[msg.sender] = 0; // Effets (changements d'état)

(bool sent, ) = msg.sender.call{value: bal}(""); // Interactions
require(sent, "Échec de l'envoi d'Ether");

}

En suivant le modèle Checks-Effects-Interactions, le contrat met à jour son état avant toute interaction externe, garantissant que même si un attaquant tente de réentrer, il rencontrera l'état mis à jour (solde zéro).

3. Protection au niveau du projet : garde de réentrance globale

Pour des projets complexes avec plusieurs contrats interagissant, la mise en œuvre d'un garde-fou de réentrance global fournit une protection à l'échelle du système :

solidité contrat GlobalReentrancyGuard { bool privé _notEntered = vrai;

modificateur globalNonReentrant() {
    require(_notEntered, "ReentrancyGuard: reentrant call");
    _notEntered = false;
    _;
    _notEntered = true;
}

}

// Tous les contrats du projet héritent de GlobalReentrancyGuard contrat SecureContract est GlobalReentrancyGuard { fonction vulnerableOperation() public globalNonReentrant { // Protégé contre la réentrance sur l'ensemble du projet } }

Cette approche est particulièrement précieuse pour se protéger contre les attaques de réentrance inter-contrats dans les protocoles DeFi où plusieurs contrats interagissent les uns avec les autres.

Impact réel des vulnérabilités de réentrance

Les vulnérabilités de réentrance ont conduit à certains des exploits les plus dévastateurs de l'histoire de la blockchain. Le célèbre piratage du DAO en 2016 a entraîné le vol d'environ 3,6 millions d'ETH ( d'une valeur d'environ $50 millions à l'époque) et a finalement conduit au hard fork d'Ethereum qui a créé Ethereum Classic.

Plus récemment, en 2020, le protocole Lendf.Me a perdu environ $25 millions à une attaque par réentrance, soulignant que malgré une sensibilisation accrue, ces vulnérabilités continuent de représenter des risques significatifs pour la sécurité des contrats intelligents.

Meilleures pratiques de sécurité

Au-delà des techniques spécifiques mentionnées, les développeurs devraient suivre ces pratiques de sécurité supplémentaires :

  1. Utilisez toujours la dernière version de Solidity qui offre des fonctionnalités de sécurité améliorées
  2. Les contrats de sujet font l'objet d'audits de sécurité approfondis par des entreprises réputées
  3. Mettre en œuvre une couverture de test complète y compris des tests unitaires pour les cas limites
  4. Commencez avec une exposition minimale à l'ETH lors du déploiement de nouveaux contrats en production
  5. Envisagez la vérification formelle pour les contrats de haute valeur

En mettant en œuvre ces techniques défensives et en suivant les meilleures pratiques de sécurité, les développeurs peuvent réduire considérablement le risque d'attaques de réentrance et créer des contrats intelligents plus sécurisés pour les applications blockchain.

ETH-0.14%
ETC-0.88%
Voir l'original
Cette page peut inclure du contenu de tiers fourni à des fins d'information uniquement. Gate ne garantit ni l'exactitude ni la validité de ces contenus, n’endosse pas les opinions exprimées, et ne fournit aucun conseil financier ou professionnel à travers ces informations. Voir la section Avertissement pour plus de détails.
  • Récompense
  • Commentaire
  • Reposter
  • Partager
Commentaire
0/400
Aucun commentaire
  • Épingler
Trader les cryptos partout et à tout moment
qrCode
Scan pour télécharger Gate app
Communauté
Français (Afrique)
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)