In primo luogo, ti aiuterò a capire in modo semplice cos'è un attacco di rientro e come puoi prevenirlo, quindi approfondirò gli esempi di codice per mostrare dove sono le vulnerabilità, quale sarebbe il codice dell'attaccante e, cosa più importante, ti mostrerò gli ultimi metodi verificati per proteggere non solo uno ma tutti gli smart contract del tuo progetto.

Spoiler: se hai già sentito parlare del modificatore nonReentrant(), continua a leggere perché stai per scoprire alcune righe sotto il modificatore globalNonReentrant() e il pattern checks-effects-interactions.



Nell'immagine sopra abbiamo ContractA e ContractB. Ora, come sai, un contratto intelligente può interagire con un altro contratto intelligente, come in questo caso, ContrattoA può chiamare ContrattoB. Pertanto, l'idea di base del rientro è che ContrattoB è in grado di richiamare ContrattoA mentre ContrattoA è ancora in esecuzione.

Quindi, come può usarlo l’aggressore?

Sopra abbiamo ContrattoA che ha 10 Ether e vediamo che ContrattoB ha immagazzinato 1 Ether in ContrattoA. In questo caso, ContrattoB sarà in grado di utilizzare la funzione di prelievo da ContrattoA e rimandare Ether a se stesso non appena supera il controllo in cui il suo saldo è maggiore di 0, per poi modificare il suo saldo totale a 0.



Vediamo ora come può ContractB utilizzare la rientranza per sfruttare la funzione di prelievo e rubare tutti gli Ether da ContractA. Fondamentalmente, l'attaccante avrà bisogno di due funzioni: attack() e fallback().

In Solidity, una funzione di fallback è una funzione esterna priva di nome, parametri o valori restituiti. Chiunque può chiamare una funzione di fallback: chiamando una funzione che non esiste all’interno del contratto; Chiamare una funzione senza passare i dati richiesti; Invio di Ether senza alcun dato nel contratto.

Il modo in cui funziona la rientranza (seguiamo le frecce passo dopo passo) prevede che l'attaccante chiami la funzione attack() che all'interno chiama la funzione withdraw() da ContractA. All'interno della funzione verificherà se il saldo di ContractB è maggiore di 0 e in tal caso proseguirà l'esecuzione.



Poiché il saldo di ContractB è maggiore di 0, restituisce 1 Ether e attiva la funzione di fallback. Nota che in questo momento ContrattoA ha 9 Ether e ContrattoB ha già 1 Ether.



Successivamente, quando la funzione di fallback viene eseguita, attiva nuovamente la funzione di prelievo del Contratto A, controllando nuovamente se il saldo del Contratto B è maggiore di 0. Se controlli di nuovo l'immagine sopra noterai che il suo saldo è ancora 1 Ether.



Ciò significa che il controllo passa e invia un altro Ether a ContractB, che attiva la funzione di fallback. Si noti che poiché la riga in cui abbiamo "balance=0" non viene mai eseguita, ciò continuerà finché tutto l'Ether di ContractA non sarà esaurito.

___________

Diamo ora un'occhiata a uno smart contract in cui possiamo identificare la rientranza con il codice Solidity.



Nel contratto EtherStore abbiamo la funzione deposit() che memorizza e aggiorna i saldi del mittente e poi la funzione withdrawAll() che prenderà tutti i saldi archiviati in una volta. Da notare l'implementazione di withdrawAll() dove verifica prima con la richiesta che il saldo sia maggiore di 0 e subito dopo invia nuovamente gli Ether, lasciando per ultimo l'aggiornamento del saldo del mittente a 0.



Qui abbiamo il contratto Attack che utilizzerà la rientranza per prosciugare il contratto EtherStore. Analizziamo il suo codice:

  • Nel suo costruttore, l'aggressore passerà l'indirizzo EtherStore per creare un'istanza e poter così utilizzare le sue funzioni.

  • Lì vediamo la funzione fallback() che verrà chiamata quando EtherStore invierà Ether a questo contratto. Al suo interno chiamerà il ritiro da EtherStore purché il saldo sia uguale o superiore a 1.

  • E all'interno della funzione attack() abbiamo la logica che sfrutterà EtherStore. Come possiamo vedere, per prima cosa avvieremo l'attacco assicurandoci di avere abbastanza ether, poi depositeremo 1 ether in modo da avere un saldo maggiore di 0 in EtherStore e quindi superare i controlli prima di iniziare a prelevare.

Ho spiegato passo dopo passo nell'esempio di ContractA e ContractB come verrà eseguito il codice, quindi ora facciamo un riepilogo di come sarà. Prima di tutto l’attaccante chiamerà attack(), che all’interno chiamerà ritiraAll() da EtherStore, che poi invierà la funzione di fallback del contratto Ether to Attack. E lì inizierà il rientro e prosciugherà il saldo dell’EtherStore.

Quindi, come possiamo proteggere i nostri contratti dagli attacchi di rientro?

Ti mostrerò tre tecniche di prevenzione per proteggerli completamente. Tratterò come prevenire il rientro in una singola funzione, il rientro in funzioni incrociate e il rientro in contratto incrociato.



La prima tecnica per proteggere una singola funzione è utilizzare un modificatore chiamato noReentrant.

Un modificatore è un tipo speciale di funzione che utilizzi per modificare il comportamento di altre funzioni. I modificatori consentono di aggiungere condizioni o funzionalità aggiuntive a una funzione senza dover riscrivere l'intera funzione.

Ciò che facciamo qui è bloccare il contratto mentre la funzione viene eseguita. In questo modo non sarà in grado di rientrare nella singola funzione poiché dovrà esaminare il codice della funzione e quindi modificare la variabile di stato bloccato in false per superare nuovamente il controllo effettuato nella richiesta.

___________



La seconda tecnica consiste nell'utilizzare il modello Checks-Effects-Interactions che proteggerà i nostri contratti dal rientro tra funzioni incrociate. Riesci a individuare nel contratto EtherStore aggiornato sopra cosa è cambiato?

Per approfondire il pattern Check-Effects-Interaction, consiglio di leggere https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html





Sopra vediamo il confronto tra il codice vulnerabile dell'immagine a sinistra dove è stato aggiornato il saldo dopo l'invio degli Ether, che come visto sopra potenzialmente non potrebbe mai essere raggiunto, e a destra ciò che è stato fatto è spostare i saldi[ msg.sender] = 0 (o effect) subito dopo require(bal > 0) (controllo) ma prima di inviare ether (interazione).

In questo modo ci assicureremo che anche se un'altra funzione accede a WithdrawAll(), questo contratto sarà protetto dall'aggressore perché il saldo verrà sempre aggiornato prima di inviare l'Ether.

Modello creato da https://twitter.com/GMX_IO

La terza tecnica che ti mostrerò è la creazione del contratto GlobalReentrancyGuard per proteggerti dal rientro tra contratti incrociati. È importante capire che questo è applicabile a progetti con più contratti che interagiscono tra loro.

L'idea qui è la stessa del modificatore noReentrant che ho spiegato nella prima tecnica, inserisce il modificatore, aggiorna una variabile per bloccare il contratto e non lo sblocca finché non termina il codice. La grande differenza qui è che stiamo utilizzando una variabile memorizzata in un contratto separato che viene utilizzata come luogo per verificare se la funzione è stata inserita o meno.







Ho creato qui un esempio senza codice vero e proprio e solo con nomi di funzioni come riferimento per comprendere l'idea poiché, in base alla mia esperienza, può aiutare a visualizzare la situazione più che scriverla semplicemente a parole.

In questo caso, l'attaccante chiamerebbe la funzione nel contratto ScheduledTransfer che, dopo aver soddisfatto le condizioni, invierebbe gli Ether specificati al contratto AttackTransfer che, quindi, entrerebbe nella funzione di fallback e quindi "annullerebbe" la transazione dal punto di partenza del contratto ScheduledTransfer. vedere e tuttavia ricevere l'Etere. E in questo modo inizierebbe uno sguardo fino a prosciugare tutti gli Ether da ScheduledTransfer.

Bene, utilizzando GlobalReentrancyGuard che ho menzionato sopra si eviterà uno scenario di attacco del genere.

__________________

Twitter @TheBlockChainer per trovare ulteriori aggiornamenti giornalieri su contratti intelligenti, sicurezza Web3, solidità, controllo dei contratti intelligenti e altro ancora.

__________________