Sicurezza
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
- Problema 2 relativo a multi-sig Parity: bloccati 300 milioni di dollari
- The DAO hack, 3,6 milioni di ETH! Oltre un miliardo di dollari in base al prezzo attuale dell'ETH
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. DefiSafety è 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)2Copia
Anche questa però non è ancora una buona soluzione. Uno degli aspetti più entusiasmanti di Ethereum è la sua componibilità: gli Smart Contract si integrano e si costruiscono l'uno sull'altro. Utilizzando la riga sopra, limiti l'utilità del progetto.
Come gestire il codice rientrante (in modo corretto)
Semplicemente cambiando l'ordine dell'aggiornamento dello storage e della chiamata esterna si impedisce la condizione di codice rientrante che ha reso possibile l'attacco. Una nuova chiamata a withdraw, sempre se possibile, non andrà a beneficio dell'attaccante, dal momento che lo storage di balances
(il saldo) sarà già impostato a 0.
1contract NoLongerAVictim {2 function withdraw() external {3 uint256 amount = balances[msg.sender];4 balances[msg.sender] = 0;5 (bool success, ) = msg.sender.call.value(amount)("");6 require(success);7 }8}9Copia
Il codice qui sopra segue il modello di progettazione "controlli-effetti-interazioni", che aiuta a proteggere dal codice rientrante. Puoi approfondire controli-effetti-interazioni qui
Come gestire il codice rientrante (opzione a prova di bomba)
Ogni volta che invii ETH a un indirizzo non attendibile o interagisci con un contratto sconosciuto (chiamando transfer()
di un indirizzo token fornito dall'utente), ti apri alla possibilità di codice rientrante. Progettando contratti che non inviano ETH e non chiamano contratti non affidabili, si preclude ogni possibilità di codice rientrante!
Altri tipi di attacco
I tipi di attacco illustrati sopra coprono i problemi del codice di Smart Contract (codice rientrante) e alcune stranezze di Ethereum (codice in esecuzione all'interno di costruttori di contratto, prima che il codice sia disponibile all'indirizzo del contratto). Ci sono moltissimi altri tipi di attacco da evitare, ad esempio:
- Front-running
- Rifiuto di invio di ETH
- Overflow/underflow di numeri interi
Letture consigliate:
- Consensys Smart Contract Known Attacks - Una spiegazione molto leggibile delle vulnerabilità più significative, molte con codice di esempio.
- SWC Registry - Elenco curato di CWE che si applicano a Ethereum e Smart Contract
Strumenti per la sicurezza
Niente può sostituire la conoscenza dei principi di base della sicurezza di Ethereum e l'utilizzo di una società di auditing professionale che riveda il codice, però sono disponibili molti strumenti che aiutano a evidenziare potenziali problemi nel codice.
Sicurezza degli Smart Contract
Slither - Framework di analisi statica per Solidity scritto in Python 3
MythX - API per l'analisi della sicurezza degli Smart Contract Ethereum
Mythril - Strumento di analisi della sicurezza per bytecode dell'EVM.
SmartContract.Codes - Motore di ricerca per codice sorgente Solidity verificato
Manticore - Interfaccia da riga di comando che usa uno strumento di esecuzione simbolica su Smart Contract e file binari
Securify - Scanner di sicurezza per Smart Contract Ethereum
ERC20 Verifier - Strumento di verifica utilizzato per controllare se un contratto rispetta lo standard ERC20
Verifica formale
Informazioni sulla verifica formale
- How formal verification of smart-contacts works 20 luglio 2018 - Brian Marick
- How Formal Verification Can Ensure Flawless Smart Contract 29 gennaio 2018 - Bernard Mueller
Usare gli strumenti
Due degli strumenti più popolari per l'analisi della sicurezza degli Smart Contract sono:
Entrambi sono strumenti utili che analizzano il codice e segnalano problemi. Ognuno ha una versione hosted commerciale, ma sono disponibili anche gratuitamente da eseguire localmente. Il seguente è un rapido esempio di come eseguire Slither, che viene reso disponibile in una comoda immagine Docker trailofbits/eth-security-toolbox
. Dovrai installare Docker se non lo hai già installato.
$ mkdir test-slither$ curl https://gist.githubusercontent.com/epheph/460e6ff4f02c4ac582794a41e1f103bf/raw/9e761af793d4414c39370f063a46a3f71686b579/gistfile1.txt > bad-contract.sol$ docker run -v `pwd`:/share -it --rm trailofbits/eth-security-toolboxdocker$ cd /sharedocker$ solc-select 0.5.11docker$ slither bad-contract.sol
Genererà questo output:
ethsec@1435b241ca60:/share$ slither bad-contract.solINFO:Detectors:Reentrancy in Victim.withdraw() (bad-contract.sol#11-16):External calls:- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)State variables written after the call(s):- balances[msg.sender] = 0 (bad-contract.sol#15)Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilitiesINFO:Detectors:Low level call in Victim.withdraw() (bad-contract.sol#11-16):- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#low-level-callsINFO:Slither:bad-contract.sol analyzed (1 contracts with 46 detectors), 2 result(s) foundINFO:Slither:Use https://crytic.io/ to get access to additional detectors and Github integrationMostra tutto
Qui Slither ha identificato una potenzialità di codice rientrante, individuando le righe principali su cui potrebbe verificarsi il problema e ci fornisce un link per avere maggiori dettagli:
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities
In questo modo, si conoscono rapidamente i potenziali problemi del codice. Come tutti gli strumenti di test automatici, Slither non è perfetto, e a volte segnala troppo. Può mettere in guardia da un potenziale codice rientrante anche quando non è presente alcuna vulnerabilità sfruttabile. Spesso, rivedere le DIFFERENZE nell'output di Slither tra le modifiche al codice è estremamente illuminante e aiuta a scoprire le vulnerabilità che sono state introdotte molto prima che il codice del progetto sia completo.
Letture consigliate
Guida alle best practice per la sicurezza degli Smart Contract
- consensys.github.io/smart-contract-best-practices/
- GitHub
- Raccolta di raccomandazioni di sicurezza e best practice
Standard di verifica della sicurezza degli Smart Contract (SCSVS)
Conosci una risorsa della community che ti è stata utile? Modifica questa pagina e aggiungila!
Tutorial correlati
- Secure development workflow
- How to use Slither to find smart contract bugs
- How to use Manticore to find smart contract bugs
- Security guidelines
- Token security