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 :
Le contrat B appelle la fonction withdraw() dans le contrat A
Le contrat A vérifie que le solde du contrat B est supérieur à 0 (passe le contrôle)
Le contrat A envoie l'ETH au contrat B, déclenchant la fonction de fallback du contrat B
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
Le contrat A vérifie le solde du contrat B, qui affiche toujours 1 ETH (pas encore mis à jour)
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.
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 :
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
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 :
// 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 :
Utilisez toujours la dernière version de Solidity qui offre des fonctionnalités de sécurité améliorées
Les contrats de sujet font l'objet d'audits de sécurité approfondis par des entreprises réputées
Mettre en œuvre une couverture de test complète y compris des tests unitaires pour les cas limites
Commencez avec une exposition minimale à l'ETH lors du déploiement de nouveaux contrats en production
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.
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.
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 :
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;
}
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;
}
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;
}
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);
}
// MISE EN ŒUVRE SÉCURISÉE fonction withdrawAll() publique { uint bal = balances[msg.sender]; require(bal > 0); // Vérifie
}
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;
}
// 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 :
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.