Vai al contenuto principale

Aiuta ad aggiornare questa pagina

🌏

C'è una nuova versione di questa pagina, ma al momento è solo in inglese. Aiutaci a tradurre l'ultima versione.

Traduci la pagina
Visualizza in inglese

Nessun bug qui!🐛

Questa pagina non è stata tradotta. Per il momento, è stata intenzionalmente lasciata in inglese.

Sicurezza degli smart contract

Ultima modifica: , Invalid DateTime
Modifica la pagina

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:

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 degli 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 COPIARE
2contract Victim {
3 mapping (address => uint256) public balances;
4
5 function deposit() external payable {
6 balances[msg.sender] += msg.value;
7 }
8
9 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}
16
Mostra tutto
📋 Copia

Per consentire a un utente di prelevare gli ETH precedentemente archiviati nel contratto, questa funzione

  1. Legge il saldo dell'utente
  2. Gli invia l'importo del saldo in ETH
  3. 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 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(VICTIM_ADDRESS).withdraw();
10 }
11 }
12}
13
Mostra tutto
📋 Copia

Chiamando Attacker.beginAttack() si avvierà un ciclo del tipo:

10.) EOA di Attacker chiama Attacker.beginAttack() con 1 ETH
20.) Attacker.beginAttack() deposita 1 ETH in Victim
3
4 1.) Attacker -> Victim.withdraw()
5 1.) Victim reads balances[msg.sender]
6 1.) Victim invia ETH a Attacker (che esegue la funzione default)
7 2.) Attacker -> Victim.withdraw()
8 2.) Victim reads balances[msg.sender]
9 2.) Victim invia ETH a Attacker (che esegue la funzione default)
10 3.) Attacker -> Victim.withdraw()
11 3.) Victim reads balances[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 nuovo
14 3.) balances[msg.sender] = 0;
15 2.) balances[msg.sender] = 0; (era già 0)
16 1.) balances[msg.sender] = 0; (era già 0)
17
Mostra 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}
6
📋 Copia

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 COPIARE
2contract ContractCheckVictim {
3 mapping (address => uint256) public balances;
4
5 function isContract(address addr) internal returns (bool) {
6 uint size;
7 assembly { size := extcodesize(addr) }
8 return size > 0;
9 }
10
11 function deposit() external payable {
12 require(!isContract(msg.sender)); // <- NUOVA LINEA
13 balances[msg.sender] += msg.value;
14 }
15
16 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}
23
Mostra tutto
📋 Copia

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); // <- New line
4 }
5
6 function beginAttack() external payable {
7 ContractCheckVictim(VICTIM_ADDRESS).withdraw();
8 }
9
10 function() external payable {
11 if (gasleft() > 40000) {
12 Victim(VICTIM_ADDRESS).withdraw();
13 }
14 }
15}
16
Mostra tutto
📋 Copia

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
📋 Copia

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}
9
📋 Copia

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:

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.

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

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 [commercial], 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-toolbox
docker$ cd /share
docker$ solc-select 0.5.11
docker$ slither bad-contract.sol

Genererà questo output:

ethsec@1435b241ca60:/share$ slither bad-contract.sol
INFO: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-vulnerabilities
INFO: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-calls
INFO:Slither:bad-contract.sol analyzed (1 contracts with 46 detectors), 2 result(s) found
INFO:Slither:Use https://crytic.io/ to get access to additional detectors and GitHub integration
Mostra 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 è possibile venire a conoscenza rapidamente di 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

Guide alle migliori prassi di sicurezza dei contratti intelligenti

Standard di verifica di sicurezza degli smart contract (SCSVS)

Conosci una risorsa pubblica che ti è stata utile? Modifica questa pagina e aggiungila!

Questo articolo è stato utile?

👈

Indietro

Aggiornare gli smart contract

Avanti

Verifica formale dello smart contract
👉