Attaque des 51% sur une blockchain : démonstration sur Ethereum
Le 8 janvier dernier, une attaque des 51% sur la blockchain Ethereum Classic (un fork d’Ethereum) a permis à un attaquant de contrôler entre 500 000 et 1 million de dollars. Cette attaque permet d’empêcher la validation de certaines transactions (censure) et de faire des doubles dépenses. Elle est réalisable quand un acteur ou un cartel dispose de plus de 50% de la puissance de minage de la blockchain visée, d’où les 50% + 1%.
Celle-ci est loin d’être un cas isolé et d’autres attaquants ont également profité de la faible sécurité de certaines blockchains qui utilisent le consensus de type Proof-of-Work. Nous ne détaillerons pas dans notre article le fonctionnement de cet algorithme de consensus, mais il est expliqué ici si vous souhaitez en savoir davantage.
La popularité de cette attaque et l’appât du gain, dans le cadre de la double dépense, ont d’ailleurs poussé la communauté à mettre en place des sites permettant d’estimer combien coûterait une attaque d’une heure sur différentes blockchain, en supposant que l’on loue les machines de minage.
On remarque que pour Ethereum Classic, louer cette puissance de calcul revient à environ 6000 $ par heure, ce qui semble peu comparé aux bénéfices potentiels.
Ferme de minage
Beaucoup d’articles concernant ces attaques de 51% (aka “double spend”) circulent. Comment les attaquants procèdent-ils pour mener cette attaque ? Pour cela, nous allons la reproduire. Nous utiliserons un réseau Ethereum privé dans cette démonstration, car le but n’est pas ici d’utiliser l’attaque mais simplement d’en montrer la simplicité avec laquelle elle peut être construite. En outre, cela nous permet également d’éviter de fournir plus de 136 Terahash/s (136,000,000,000,000 hashes par seconde).
Setup
Nous utiliserons le client Ethereum Geth. Geth dispose en effet d’un mineur CPU intégré et d’un CLI qui nous facilitera la tâche.
Commençons par créer un réseau privé Ethereum ; nous aurons besoin d’un mineur honnête, honestMiner, d’un mineur malhonnête, maliciousMiner, qui dispose de plus de puissance de calcul que honestMiner, ainsi que d’une personne malhonnête, maliciousPerson****, en relation avec maliciousMiner. Il est important de noter que dans notre cas, honestMiner représente un seul noeud Ethereum afin de simplifier la démonstration ; mais il représente en réalité un large panel de mineurs. Le plus important est en effet le nombre de hash par seconde (hashrate) qu’ils peuvent effectuer.
Rangeons dans network/ le bloc genesis de notre blockchain :
Nos dossiers
Chaque mineur dispose de sa clé, qu’on trouve dans le dossier keystore et de sa blockchain, qu’on va ranger dans le dossier data.
Notre bloc genesis, très simple, nous permet de générer un montant d’ether initial à notre utilisateur maliciousPerson qu’il utilisera par la suite dans l’attaque.
Dès lors, initions nos deux noeuds mineurs :
@@ARTICLE_CONTENT@@gt; brew tap ethereum/ethereum @@ARTICLE_CONTENT@@gt; brew install ethereum @@ARTICLE_CONTENT@@gt; geth --datadir honestMiner/data/ init network/genesis.json @@ARTICLE_CONTENT@@gt; geth --datadir maliciousMiner/data/ init network/genesis.json
Initialisation du maliciousMiner
Création du compte de honestMiner
Créons des comptes/adresses Ethereum pour chacun de nos personas :
@@ARTICLE_CONTENT@@gt; geth account new --datadir maliciousClient/ @@ARTICLE_CONTENT@@gt; geth account new --datadir maliciousMiner/ @@ARTICLE_CONTENT@@gt; geth account new --datadir honestMiner/
Lançons nos deux mineurs dans leur consoles respectives :
@@ARTICLE_CONTENT@@gt; geth --datadir honestMiner/data --keystore honestMiner/keystore/ --networkid 666 --nodiscover --port 30303 console @@ARTICLE_CONTENT@@gt; geth --datadir maliciousMiner/data --keystore maliciousMiner/keystore/ --networkid 666 --nodiscover --port 30304 console
Bien qu’ils soient dans le même réseau privé, ils ne se connaissent pas.
Nous devons les rattacher en récupérant leurs informations de connexion, aussi appelées “enode” : enode://<NODE_ID>@<IP>:<LISTENING_PORT>?discport=<UDP_DISCOVERY_PORT>)
Allons dans la console geth de l’un des mineurs et récupérons l’enode de notre noeud, puis ajoutons le depuis la console de l’autre mineur : @@ARTICLE_CONTENT@@gt; admin.nodeInfo @@ARTICLE_CONTENT@@gt; admin.addPeer(<ENODE>)
Nos deux noeuds peuvent désormais communiquer :
On retrouve un peer depuis la console, c’est l’autre mineur !
Grâce à ces étapes, nous avons un réseau privé Ethereum très simple, qui nous permet par la suite de simuler un comportement multi-mineurs sur le réseau public Ethereum.
Le double spend
Faisons tout d’abord travailler nos deux mineurs depuis leurs consoles respectives, sur 1 thread : @@ARTICLE_CONTENT@@gt; miner.start(1)
honestMiner et maliciousMiner en plein travail
Au moment opportun, maliciousMiner va commencer son attaque. Il existe plusieurs approches pour mener à bien cette attaque :
- Se couper totalement du réseau et miner sa chaîne contenant uniquement des blocs vides (notre approche).
- Continuer de recevoir les transactions du réseau mais ne pas inclure les transactions de maliciousPerson dans nos blocs, autrement l’attaque ne fonctionnerait pas.
Afin que l’attaque soit réussie, la chaîne forkée malicieuse doit être plus “lourde” que l’autre chaîne, visualisable comme ceci :
Pour Bitcoin, cela signifie seulement qu’elle est plus longue, alors que pour Ethereum elle doit également contenir plus d’oncles (plus d’informations ici), comme on peut le voir graphiquement ci-dessous :
Nous décidons, par simplicité d’exécution de notre attaque, d’isoler maliciousMiner du réseau en changeant son adresse de contact. Pour rappel, maliciousMiner dispose de plus de puissance de calcul que honestMiner et donc plus de la moitié de la puissance de calcul de cette blockchain.
Relançons maliciousMiner, sur un autre port : @@ARTICLE_CONTENT@@gt; geth --datadir maliciousMiner/data --keystore maliciousMiner/keystore/ --networkid 666 --nodiscover --port 30305 console
Avec deux threads, son hashrate est plus important que celui d’honestMiner: @@ARTICLE_CONTENT@@gt; miner.start(2)
On peut le vérifier : @@ARTICLE_CONTENT@@gt; eth.hashrate
C’est le moment parfait pour commencer l’attaque ! MaliciousPerson décide d’acheter un nouvel SSD qui coûte 1 ether (~140 $) à un marchand online acceptant la crypto-monnaie d’Ethereum, l’ether.
Il effectue donc le paiement avec une transaction Ethereum, confirmée quelques secondes plus tard par honestMiner.
Envoi de la transaction : @@ARTICLE_CONTENT@@gt; personal.unlockAccount("0x4e02712e277521952a568678e1863dbeb57a3ee2", “<PASSWORD>”) @@ARTICLE_CONTENT@@gt; web3.eth.sendTransaction({from: "0x4e02712e277521952a568678e1863dbeb57a3ee2", to: "0xe82e809d5f9574b54a4b8ead7e195b163b39662f", value: 1000000000000000000})
L’envoi nous retourne l’id de transaction ; on vérifie son reçu en tapant : @@ARTICLE_CONTENT@@gt; web3.eth.getTransactionReceipt("<TRANSACTION_ID>")
Le reçu de la transaction d’honestMiner
Sa transaction, qui se trouve dans le block 38 (blockNumber) comme on peut le constater ci-dessus, est approuvée sur la chaîne honnête, le marchand décide donc d’attendre quelques blocs avant d’envoyer la commande, pour être certain que cette transaction perdure.
Après 28 blocs d’attente (environ 7 minutes), donc au bloc 66, le marchand décide enfin d’envoyer le colis.
Au moment où maliciousPerson reçoit la confirmation d’envoi du marchand, il demande à son ami maliciousMiner de relâcher ses blocs minés dans le réseau Ethereum, pour ce faire nous connectons simplement les deux noeuds avec admin.addPeer().
Or, il se trouve que maliciousMiner est actuellement à 67 blocs minés, car il utilise deux threads : il à un hashrate de 5520 alors qu’honestMiner a un hashrate de 4715. MaliciousMiner a donc 1 bloc de plus qu’****honestMiner : sa chaîne est plus lourde !
Le noeud d’honestMiner se met automatiquement à jour quand ils se mettent en communication, il reçoit un “new chain segment” : c’est la réorganisation de la chaîne.
réorganisation de la chaîne de honestMiner
Il semblerait donc que l’ancienne chaîne d’honestMiner soit effacée et que l’attaque soit un succès : la transaction a été annulée. Vérifions le en demandant le reçu de cette transaction d’achat :
reçu de la transaction d’achat
Le reçu n’existe plus car la transaction est tout simplement inexistante. Aussi, allons voir les transactions de ce fameux bloc 67 :
Le bloc ne contient lui aussi aucune transaction.
On retient que :
- La transaction a bien été annulée et ne figure plus dans aucun bloc.
- Cette annulation est une feature, et non pas un bug !
Conclusion
La finalité probabiliste des blockchain avec un consensus de type Proof-of-Work rend la sécurité de ces dernières dépendante de la puissance de hachage des mineurs. Par exemple, Bitcoin dispose d’un hashrate très important alors que celui d’Ethereum Classic est bien plus faible et propice à la réorganisation des chaînes.
La réorganisation des chaînes est vitale dans le consensus PoW, c’est pourquoi d’autres consensus ayant une finalité transactionnelle, comme le Proof-of-Stake et le Proof-of-Authority, émergent.
Pour pallier ce problème, les échanges essaient d’attendre un nombre de blocs suffisant avant de valider une transaction ; ils ont d’ailleurs augmenté ce laps de validation suite aux dernières attaques des 51%.