Sicurezza
Ultima modifica: , Invalid DateTime
Gli Smart Contract Ethereum sono estremamente flessibili, in grado di contenere grandi quantità di token (spesso per importi superiori a 1 miliardo di USD) e di eseguire una logica immutabile basata su codice di Smart Contract distribuito precedentemente. Sebbene questa situazione abbia creato un ecosistema vibrante e creativo di Smart Contract affidabili e interconnessi, è anche l'ecosistema perfetto per attrarre malintenzionati che cercano di trarre profitto sfruttando le vulnerabilità degli Smart Contract e il comportamento imprevisto di Ethereum. Il codice degli Smart Contract di solito non può essere modificato per correggere falle di sicurezza, le risorse rubate dagli Smart Contract sono irrecuperabili e anche estremamente difficili da tracciare. L'importo totale del valore rubato o perso a causa di problemi degli Smart Contract si aggira facilmente attorno al miliardo di dollari. Alcuni dei maggiori errori dovuti a errori di codifica degli Smart Contract includono:
- Problema 1 relativo a multi-sig Parity: persi 30 milioni di dollari(opens in a new tab)
- Problema 2 relativo a multi-sig Parity: bloccati 300 milioni di dollari(opens in a new tab)
- The DAO hack, 3,6 milioni di ETH! Oltre un miliardo di dollari in base al prezzo attuale dell'ETH(opens in a new tab)
Prerequisiti
Parleremo della sicurezza degli Smart Contract quindi assicurati di avere familiarità con gli Smart Contract prima di affrontare questo argomento.
Come scrivere codice più sicuro per gli Smart Contract
Prima di eseguire codice sulla rete principale, è importante prendere sufficienti precauzioni per proteggere le risorse di valore associate allo Smart Contract. In questo articolo parleremo di alcuni attacchi specifici, suggeriremo risorse per saperne di più su altri tipi di attacco e indicheremo alcuni strumenti di base e le best practice per garantire il funzionamento corretto e sicuro dei contratti.
Gli audit non sono infallibili
Anni fa, gli strumenti per scrivere, compilare, testare e distribuire Smart Contract erano molto immaturi, e di conseguenza molti progetti includevano codice Solidity scritto a caso che veniva poi passato a un auditor che lo esaminava per garantire che funzionasse in modo sicuro e come previsto. Nel 2020, i processi di sviluppo e gli strumenti che supportano la scrittura di codice Solidity sono decisamente migliori; queste best practice non solo assicurano che un progetto sia più facile da gestire, ma sono una parte fondamentale della sicurezza del progetto. Un audit al termine della scrittura dello Smart Contract non è più sufficiente come unico strumento per garantire la sicurezza del progetto. La sicurezza inizia ancor prima di scrivere la prima riga di codice dello Smart Contract, la sicurezza inizia da processi di progettazione e sviluppo adeguati.
Processo di sviluppo di Smart Contract
Requisiti minimi:
- Tutto il codice memorizzato in un sistema di controllo delle versioni, come git
- Tutte le modifiche al codice effettuate tramite richieste pull
- Tutte le richieste pull hanno almeno un revisore. Se sei l'unico sviluppatore nel progetto, prendi in considerazione la possibilità di trovare un altro sviluppatore che lavori da solo per revisionarvi i progetti a vicenda
- Un singolo comando compila, distribuisce ed esegue una serie di test sul codice utilizzando un ambiente di sviluppo per Ethereum (vedi: Truffle)
- Hai verificato il codice con strumenti di base di analisi del codice come Mythril e Slither, idealmente prima dell'unione di ogni richiesta pull, e confrontato le differenze nell'output
- Solidity non produce NESSUN avviso in fase di compilazione
- Il codice è ben documentato
C'è molto altro da dire sul processo di sviluppo, ma questo è un buon punto di partenza. Per ulteriori punti e spiegazioni dettagliate, vedi la checklist per la qualità del processo stilata da DeFiSafety(opens in a new tab). DefiSafety(opens in a new tab) è un servizio pubblico non ufficiale che pubblica recensioni di varie dapp Ethereum pubbliche. Parte del sistema di valutazione di DeFiSafety indica in che misura il progetto aderisce a questa checklist della qualità del processo. Seguendo questi processi:
- Produrrai codice più sicuro, tramite test automatici riproducibili
- Gli auditor saranno in grado di rivedere il tuo progetto in modo più efficace
- Sarà più facile aggiungere nuovi sviluppatori
- Gli sviluppatori potranno iterare, testare e ottenere feedback velocemente sulle modifiche
- È meno probabile che il progetto subisca regressioni
Attacchi e vulnerabilità
Una volta assicurato che il codice Solidity sia scritto utilizzando un processo di sviluppo efficiente, diamo un'occhiata ad alcune vulnerabilità comuni di Solidity, per capire cosa può andare storto.
Codice rientrante
Il codice rientrante è uno dei più comuni e più importanti problemi di sicurezza da valutare quando si sviluppano Smart Contract. Mentre l'EVM non può eseguire più contratti allo stesso tempo, un contratto che chiama un altro contratto interrompe l'esecuzione e lo stato di memoria del contratto chiamante fino a quando la chiamata restituisce un risultato, dopo di che l'esecuzione procede normalmente. Questo momento di pausa e riavvio può creare una vulnerabilità conosciuta come "re-entrancy" o codice rientrante.
Ecco una semplice versione di un contratto vulnerabile al codice rientrante:
1// QUESTO CONTRATTO CONTIENE VULNERABILITA' INTENZIONALI, NON COPIARE2contract Victim {3 mapping (address => uint256) public balances;45 function deposit() external payable {6 balances[msg.sender] += msg.value;7 }89 function withdraw() external {10 uint256 amount = balances[msg.sender];11 (bool success, ) = msg.sender.call.value(amount)("");12 require(success);13 balances[msg.sender] = 0;14 }15}16Mostra tuttoCopia
Per consentire a un utente di prelevare gli ETH precedentemente archiviati nel contratto, questa funzione
- Legge il saldo dell'utente
- Gli invia l'importo del saldo in ETH
- Imposta il saldo a 0, in modo che non sia possibile prelevarlo nuovamente.
Se chiamato da un account standard (ad esempio un account MetaMask), questo codice funziona come previsto: msg.sender.call.value() invia semplicemente il saldo ETH. Però anche gli Smart Contract possono effettuare chiamate. Se quindi è un contratto maligno modificato a chiamare withdraw()
, msg.sender.call.value() non invierà solo amount
(l'importo) di ETH, ma chiamerà anche implicitamente il contratto per iniziare l'esecuzione del codice. Immagina che a chiamare sia questo contratto maligno:
1contract Attacker {2 function beginAttack() external payable {3 Victim(VICTIM_ADDRESS).deposit.value(1 ether)();4 Victim(VICTIM_ADDRESS).withdraw();5 }67 function() external payable {8 if (gasleft() > 40000) {9 Victim(VICTIM_ADDRESS).withdraw();10 }11 }12}13Mostra tuttoCopia
Chiamando Attacker.beginAttack() si avvierà un ciclo del tipo:
10.) EOA di Attacker chiama Attacker.beginAttack() con 1 ETH20.) Attacker.beginAttack() deposita 1 ETH in Victim34 1.) Attacker -> Victim.withdraw()5 1.) Victim legge balanceOf[msg.sender]6 1.) Victim invia ETH a Attacker (che esegue la funzione default)7 2.) Attacker -> Victim.withdraw()8 2.) Victim legge balanceOf[msg.sender]9 2.) Victim invia ETH a Attacker (che esegue la funzione default)10 3.) Attacker -> Victim.withdraw()11 3.) Victim legge balanceOf[msg.sender]12 3.) Victim invia ETH a Attacker (che esegue la funzione default)13 4.) Attacker non ha abbastanza carburante, restituisce il risultato senza chiamare di nuovo14 3.) balances[msg.sender] = 0;15 2.) balances[msg.sender] = 0; (era già 0)16 1.) balances[msg.sender] = 0; (era già 0)17Mostra tutto
Chiamando Attacker.beginAttack con 1 ETH si attacca Victim con codice rientrante, prelevando più ETH rispetto alla disponibilità (prendendoli dai saldi di altri utenti e rendendo il contratto Victim non collateralizzato)
Come gestire il codice rientrante (in modo sbagliato)
Si potrebbe pensare di difendersi dal codice rientrante semplicemente impedendo a qualsiasi Smart Contract di interagire con il proprio codice. Se cerchi stackoverflow, trovi questo frammento di codice con tantissimi voti a favore:
1function isContract(address addr) internal returns (bool) {2 uint size;3 assembly { size := extcodesize(addr) }4 return size > 0;5}6Copia
Sembra avere senso: i contratti hanno codice, se il chiamante ha un codice, non si consente di depositare. Aggiungiamolo:
1// QUESTO CONTRATTO CONTIENE VULNERABILITA' INTENZIONALI, NON COPIARE2contract ContractCheckVictim {3 mapping (address => uint256) public balances;45 function isContract(address addr) internal returns (bool) {6 uint size;7 assembly { size := extcodesize(addr) }8 return size > 0;9 }1011 function deposit() external payable {12 require(!isContract(msg.sender)); // <- NUOVA LINEA13 balances[msg.sender] += msg.value;14 }1516 function withdraw() external {17 uint256 amount = balances[msg.sender];18 (bool success, ) = msg.sender.call.value(amount)("");19 require(success);20 balances[msg.sender] = 0;21 }22}23Mostra tuttoCopia
Per depositare ETH, non bisogna avere codice di Smart Contract al proprio indirizzo. A questo però si può facilmente ovviare con il seguente contratto di Attacker:
1contract ContractCheckAttacker {2 constructor() public payable {3 ContractCheckVictim(VICTIM_ADDRESS).deposit(1 ether); // <- Nuova linea4 }56 function beginAttack() external payable {7 ContractCheckVictim(VICTIM_ADDRESS).withdraw();8 }910 function() external payable {11 if (gasleft() > 40000) {12 Victim(VICTIM_ADDRESS).withdraw();13 }14 }15}16Mostra tuttoCopia
Mentre il primo attacco era un attacco alla logica del contratto, questo è un attacco al comportamento di distribuzione dei contratti Ethereum. Durante la costruzione, un contratto non ha ancora restituito il suo codice da distribuire al proprio indirizzo, ma mantiene il pieno controllo dell'EVM DURANTE questo processo.
È tecnicamente possibile impedire che gli Smart Contract chiamino il proprio codice utilizzando questa riga:
1require(tx.origin == msg.sender)2