Incidents associés
Analyse de l'exploit DAO
Phil Daian
Je suis donc sûr que tout le monde a entendu parler de la grande nouvelle entourant le DAO qui a été emporté à hauteur de 150 millions de dollars par un pirate informatique utilisant l'exploit d'envoi récursif d'Ethereum.
Ce message sera le premier de ce qui est potentiellement une série, déconstruisant et expliquant ce qui n'a pas fonctionné au niveau technique tout en fournissant une chronologie retraçant les actions de l'attaquant à travers la blockchain. Ce premier article se concentrera sur la manière exacte dont l'attaquant a volé tout l'argent du DAO.
Une attaque en plusieurs étapes Cet exploit dans le DAO n'est clairement pas anodin ; le modèle de programmation exact qui rendait le DAO vulnérable était non seulement connu, mais corrigé par les créateurs de DAO eux-mêmes dans une mise à jour prévue antérieure du code du framework. Ironiquement, alors qu'ils écrivaient leurs articles de blog et criaient victoire, le pirate préparait et déployait un exploit qui ciblait la même fonction qu'il venait de corriger pour vider le DAO de tous ses fonds. Passons à la vue d'ensemble de l'attaque. L'attaquant analysait DAO.sol et a remarqué que la fonction 'splitDAO' était vulnérable au modèle d'envoi récursif que nous avons décrit ci-dessus : cette fonction met à jour les soldes et les totaux des utilisateurs à la fin, donc si nous pouvons obtenir l'un des appels de fonction avant que cela n'arrive pour appeler à nouveau splitDAO, nous obtenons la récursivité infinie qui peut être utilisée pour déplacer autant de fonds que nous le voulons (les commentaires de code sont marqués avec XXXXX, vous devrez peut-être faire défiler pour les voir) : function splitDAO ( uint _proposalID , address _newCurator ) noEther onlyTokenholders renvoie ( bool _success ) { ... uint fundsToBeMoved = ( balances [ msg . sender ] * p . splitData [ 0 ]. splitBalance ) / p . splitData [ 0 ]. approvisionnement total ; if ( p . splitData [ 0 ]. newDAO . createTokenProxy . value ( fundsToBeMoved )( msg . sender ) == false ) throw ; ... Transférer ( msg . expéditeur , 0 , soldes [ msg . expéditeur ]); removeRewardFor ( msg . sender ); totalSupply -= soldes [ msg . expéditeur ] ; soldes [ msg . expéditeur ] = 0 ; payéOut [ msg . expéditeur ] = 0 ; retourne vrai ; } L'idée de base est celle-ci : proposer un split. Exécutez la scission. Lorsque le DAO va retirer votre récompense, appelez la fonction pour exécuter une scission avant la fin de ce retrait. La fonction commencera à s'exécuter sans mettre à jour votre solde, et la ligne que nous avons marquée ci-dessus comme "l'attaquant veut s'exécuter plus d'une fois" s'exécutera plus d'une fois. Qu'est-ce que ça fait? Eh bien, le code source est dans TokenCreation.sol, et il transfère les jetons du DAO parent au DAO enfant. Fondamentalement, l'attaquant l'utilise pour transférer plus de jetons qu'il ne devrait pouvoir le faire dans son DAO enfant. Comment le DAO décide-t-il du nombre de jetons à déplacer ? En utilisant le tableau balances bien sûr : uint fundsToBeMoved = ( balances [msg . sender ] * p . splitData [ 0 ]. splitBalance ) / p . splitData [ 0 ]. approvisionnement total ; Parce que p.splitData[0] va être le même à chaque fois que l'attaquant appelle cette fonction (c'est une propriété de la proposition p, pas l'état général du DAO), et parce que l'attaquant peut appeler cette fonction à partir de removeRewardFor avant le balances array est mis à jour, l'attaquant peut faire exécuter ce code arbitrairement plusieurs fois en utilisant l'attaque décrite, avec fundsToBeMoved sortant à la même valeur à chaque fois. La première chose que l'attaquant devait faire pour ouvrir la voie à son exploit réussi était d'exécuter réellement la fonction de retrait du DAO, qui était vulnérable à l'exploit d'envoi récursif critique. Regardons ce qui est nécessaire pour que cela se produise dans le code (à partir de DAO.sol) : function removeRewardFor ( address _account ) noEther interne renvoie ( bool _success ) { if (( balanceOf ( _account ) * rewardAccount . AccumulatedInput ()) / totalSupply <paidOut [ _compte ]) jeter ; uint récompense = ( balanceOf ( _account ) * rewardAccount . AccumulatedInput ()) / totalSupply -paidOut [ _account ] ; if ( ! recompenseAccount . payOut ( _compte , récompense )) jeter ; payéOut [ _account ] += récompense ; retourne vrai ; } Si le pirate pouvait obtenir que la première instruction if soit évaluée comme fausse, l'instruction marquée comme vulnérable s'exécuterait. Lorsque cette instruction s'exécute, un code semblable à celui-ci est appelé : function payOut ( address _recipient , uint _amount ) return ( bool ) { if ( msg . sender != owner || msg . value > 0 || ( payOwnerOnly && _recipient ! = propriétaire )) lancer ; if ( _recipient . call . value ( _amount )()) { PayOut ( _recipient , _amount ); retourne vrai ; } sinon { renvoie faux ; } Remarquez comment la ligne marquée est exactement le code vulnérable mentionné dans la description de l'exploit que nous avons lié ! Cette ligne enverrait alors un message du contrat du DAO à "_recipient" (l'attaquant). "_recipient" contiendrait bien sûr une fonction par défaut, qui appellerait à nouveau splitDAO avec les mêmes paramètres que l'appel initial de l'attaquant. N'oubliez pas que, comme tout se passe depuis l'intérieur de l'intérieur du retrait vers l'intérieur de splitDAO, le code mettant à jour les soldes dans splitDAO n'a pas été exécuté. Ainsi, la division enverra plus de jetons au DAO enfant, puis demandera la récompense