Come usare Slither per trovare i bug dello Smart Contract
Come usare Slither
L'obiettivo di questo tutorial è mostrare come usare Slither per trovare automaticamente bug negli Smart Contract.
- Installazione
- Uso dalla riga di comando
- Introduzione all'analisi statica: breve introduzione all'analisi statica
- API: descrizione dell'API Python
Installazione
Slither richiede Python >=3.6. Può essere installato tramite pip o usando docker.
Slither con pip:
pip3 install --user slither-analyzer
Slither con docker:
docker pull trailofbits/eth-security-toolboxdocker run -it -v "$PWD":/home/trufflecon trailofbits/eth-security-toolbox
L'ultimo comando esegue eth-security-toolbox in un docker che ha accesso alla directory corrente. Puoi cambiare i file dall'host ed eseguire gli strumenti sui file dal docker
In docker, esegui:
solc-select 0.5.11cd /home/trufflecon/
Eseguire uno script
Per eseguire uno script Python con Python 3:
python3 script.py
Riga di comando
Riga di comando e script definiti dall'utente. Slither comprende una serie di rilevatori predefiniti che trovano molti bug comuni. Chiamare Slither dalla riga di comando eseguirà tutti i rilevatori, non è necessaria alcuna conoscenza dettagliata dell'analisi statica:
slither project_paths
Oltre ai rilevatori, Slither ha capacità di revisione del codice tramite le sue stampanti(opens in a new tab) e i suoi strumenti(opens in a new tab).
Usa crytic.io(opens in a new tab) per ottenere accesso ai rilevatori privati e integrazione con GitHub.
Analisi statica
Le capacità e il design del framework di analisi statica di Slither sono stati descritti in post di blog (1(opens in a new tab), 2(opens in a new tab)) e in un paper accademico(opens in a new tab).
L'analisi statica esiste in diversi tipi. Molto probabilmente ti renderai conto che compilatori come clang(opens in a new tab) e gcc(opens in a new tab) dipendono da queste tecniche di ricerca, che sono anche alla base di Infer(opens in a new tab), CodeClimate(opens in a new tab), FindBugs(opens in a new tab) e strumenti basati sui metodi formali come Frama-C(opens in a new tab) e Polyspace(opens in a new tab).
Qui non esamineremo in modo esaustivo le tecniche di analisi statica e il ricercatore. Ci concentreremo invece su ciò che serve per capire come funziona Slither così da poterlo usare più efficacemente per trovare bug e comprendere il codice.
Rappresentazione del codice
A differenza dell'analisi dinamica, che ragiona su un percorso di esecuzione singolo, l'analisi statica ragiona su tutti i percorsi contemporaneamente. Per farlo, si basa su una diversa rappresentazione del codice. Le due tipologie più comuni sono l'albero di sintassi astratta (AST) e il grafico del flusso di controllo (CFG).
Alberi di sintassi astratta (AST)
Gli AST sono usati ogni volta che il compilatore analizza il codice. Sono probabilmente la struttura più basilare su cui è eseguibile l'analisi statica.
In pillole, un AST è un albero strutturato dove, di solito, ogni foglia contiene una variabile o una costante e i nodi interni sono operandi o controllano le operazioni del flusso. Considera il codice seguente:
1function safeAdd(uint a, uint b) pure internal returns(uint){2 if(a + b <= a){3 revert();4 }5 return a + b;6}Copia
L'AST corrispondente è mostrato in:
Slither usa l'AST esportato da solc.
Sebbene semplice da costruire, l'AST è una struttura nidificata. A volte, non è la più semplice da analizzare. Per esempio, per identificare le operazioni usate dall'espressione a + b <= a
, devi prima analizzare <=
e poi +
. Un approccio comune è usare il cosiddetto schema dei visitatori, che naviga l'albero in modo ricorsivo. Slither contiene un visitatore generico in ExpressionVisitor
(opens in a new tab).
Il codice seguente usa ExpressionVisitor
per rilevare se l'espressione contiene una somma:
1from slither.visitors.expression.expression import ExpressionVisitor2from slither.core.expressions.binary_operation import BinaryOperationType34class HasAddition(ExpressionVisitor):56 def result(self):7 return self._result89 def _post_binary_operation(self, expression):10 if expression.type == BinaryOperationType.ADDITION:11 self._result = True1213visitor = HasAddition(expression) # expression is the expression to be tested14print(f'The expression {expression} has a addition: {visitor.result()}')Mostra tuttoCopia
Grafico del flusso di controllo (CFG)
La seconda rappresentazione più comune del codice è il grafico del flusso di controllo (CFG). Come suggerisce il nome, è una rappresentazione basata su un grafico, che espone tutti i percorsi d'esecuzione. Ogni nodo contiene una o più istruzioni. I bordi nel grafico rappresentano le operazioni del flusso di controllo (if/then/else, loop, ecc). Il CFG del nostro esempio precedente è:
Il CFG è la rappresentazione su cui gran parte delle analisi sono costruite.
Esistono molte altre rappresentazioni del codice. Ogni rappresentazione ha vantaggi e svantaggi a seconda dell'analisi che si desidera eseguire.
Analisi
Il tipo più semplice di analisi eseguibile con Slither è l'analisi sintattica.
Analisi della sintassi
Slither può navigare attraverso diversi componenti del codice e la loro rappresentazione per trovare incoerenze e difetti usando un approccio simile all'abbinamento a schemi.
Per esempio i seguenti rilevatori cercano problemi correlati alla sintassi:
Shadowing della variabile di stato(opens in a new tab): esegue iterazioni su tutte le variabili di stato e controlla se qualcuna esegue lo shadowing di una variabile da un contratto ereditato (state.py#L51-L62(opens in a new tab))
Interfaccia errata di ERC20(opens in a new tab): cerca firme della funzione ERC20 errate (incorrect_erc20_interface.py#L34-L55(opens in a new tab))
Analisi semantica
A differenza dell'analisi di sintassi, un'analisi semantica va più in profondità e analizza il "significato" del codice. Questa famiglia include alcuni tipi generici di analisi. Conducono a risultati più potenti e utili, ma anche più complessi da scrivere.
Le analisi semantiche sono usate per i rilevamenti più avanzati delle vulnerabilità.
Analisi della dipendenza dei dati
Una variabile variable_a
si dice dipendente dai dati di variable_b
se esiste un percorso per cui il valore di variable_a
è influenzato da variable_b
.
Nel codice seguente, variable_a
dipende da variable_b
:
1// ...2variable_a = variable_b + 1;Copia
Slither è dotato di capacità integrate di dipendenza dai dati(opens in a new tab), grazie alla sua rappresentazione intermedia (discussa in una sezione successiva).
Un esempio di uso della dipendenza dei dati si può trovare nel rilevatore di uguaglianze rigorose pericolose(opens in a new tab). In questo caso Slither cercherà confronti tra uguaglianze rigorose a un valore pericoloso (incorrect_strict_equality.py#L86-L87(opens in a new tab)), e informerà l'utente che dovrebbe usare >=
o <=
anziché ==
, per impedire a un malintenzionato di bloccare il contratto. Tra gli altri, il rilevatore considererà come pericoloso il valore restituito da una chiamata di balanceOf(address)
(incorrect_strict_equality.py#L63-L64(opens in a new tab)), e userà il motore delle dipendenze dei dati per monitorarne l'uso.
Calcolo del punto fisso
Se l'analisi naviga attraverso il CFG e segue i bordi, potresti vedere nodi già visitati. Per esempio, se un ciclo viene presentato come mostrato sotto:
1for(uint i; i < range; ++){2 variable_a += 13}Copia
L'analisi dovrà sapere quando interrompersi. Qui esistono due strategie principali: (1) itera su ogni nodo un numero finito di volte, (2) calcola un cosiddetto fixpoint (punto fisso). Un punto fisso indica fondamentalmente che analizzare il nodo non fornisce alcuna informazione utile.
Un esempio di punto fisso usato si può trovare nei rilevatori di rientranza: Slither esplora i nodi e cerca chiamate esterne, scrive e legge lo storage. Una volta raggiunto un punto fisso (reentrancy.py#L125-L131(opens in a new tab)), interrompe l'esplorazione e analizza i risultati per vedere se è presente una rientranza, tramite diversi schemi di rientranza (reentrancy_benign.py(opens in a new tab), reentrancy_read_before_write.py(opens in a new tab), reentrancy_eth.py(opens in a new tab)).
La scrittura dell'analisi tramite un calcolo efficiente dei punti fissi richiede una buona comprensione di come l'analisi propaga le informazioni.
Rappresentazione intermedia
Una rappresentazione intermedia (IR) è un linguaggio pensato per essere più adatto all'analisi statica che a quella originale. Slither traduce Solidity nella propria rappresentazione intermedia: SlithIR(opens in a new tab).
Comprendere SlithIR non è necessario per scrivere controlli di base. È invece utile se pensi di scrivere analisi semantiche avanzate. Le stampanti SlithIR(opens in a new tab) e SSA(opens in a new tab) aiuteranno a comprendere come è tradotto il codice.
Fondamenti delle API
Slither ha un'API che consente di esplorare gli attributi di base del contratto e le sue funzioni.
Per caricare una base di codice:
1from slither import Slither2slither = Slither('/path/to/project')3Copia
Esplorare contratti e funzioni
Un oggetto Slither
ha:
contracts (list(Contract)
: elenco di contratticontracts_derived (list(Contract)
: elenco dei contratti che non vengono ereditati da un altro contratto (sottoinsieme di contratti)get_contract_from_name (str)
: restituisce un contratto dal nome
Un oggetto Contract
ha:
name (str)
: nome del contrattofunctions (list(Function))
: elenco di funzionimodifiers (list(Modifier))
: elenco di funzioniall_functions_called (list(Function/Modifier))
: elenco di tutte le funzioni interne raggiungibili dal contrattoinheritance (list(Contract))
: elenco di contratti ereditatiget_function_from_signature (str)
: restituisce una funzione dalla firmaget_modifier_from_signature (str)
: restituisce un modificatore dalla firmaget_state_variable_from_name (str)
: restituisce una variabile di stato dal nome
Un oggetto Function
o Modifier
ha:
name (str)
: nome della funzionecontract (contract)
: il contratto in cui è dichiarata la funzionenodes (list(Node))
: elenco dei nodi che compongono il CFG della funzione o del modificatoreentry_point (Node)
: punto di ingresso del CFGvariables_read (list(Variable))
: elenco delle variabili lettevariables_written (list(Variable))
: elenco delle variabili scrittestate_variables_read (list(StateVariable))
: elenco delle variabili di stato lette (sottoinsieme di variables`read)state_variables_written (list(StateVariable))
: elenco delle variabili di stato scritte (sottoinsieme di variables`written)
Ultima modifica: @nhsz(opens in a new tab), 15 agosto 2023