Vai al contenuto principale

Come usare Slither per trovare i bug dello Smart Contract

SoliditySmart Contractsicurezzatestanalisi statica
Argomenti avanzati
Trailofbits
Creare contratti sicuri(opens in a new tab)
9 giugno 2020
7 minuti letti minute read

Come usare Slither

L'obiettivo di questo tutorial è mostrare come usare Slither per trovare automaticamente bug negli Smart Contract.

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-toolbox
docker 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.11
cd /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:

AST

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 ExpressionVisitor
2from slither.core.expressions.binary_operation import BinaryOperationType
3
4class HasAddition(ExpressionVisitor):
5
6 def result(self):
7 return self._result
8
9 def _post_binary_operation(self, expression):
10 if expression.type == BinaryOperationType.ADDITION:
11 self._result = True
12
13visitor = HasAddition(expression) # expression is the expression to be tested
14print(f'The expression {expression} has a addition: {visitor.result()}')
Mostra tutto
Copia

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 è:

CFG

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:

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 += 1
3}
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 Slither
2slither = Slither('/path/to/project')
3
Copia

Esplorare contratti e funzioni

Un oggetto Slither ha:

  • contracts (list(Contract): elenco di contratti
  • contracts_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 contratto
  • functions (list(Function)): elenco di funzioni
  • modifiers (list(Modifier)): elenco di funzioni
  • all_functions_called (list(Function/Modifier)): elenco di tutte le funzioni interne raggiungibili dal contratto
  • inheritance (list(Contract)): elenco di contratti ereditati
  • get_function_from_signature (str): restituisce una funzione dalla firma
  • get_modifier_from_signature (str): restituisce un modificatore dalla firma
  • get_state_variable_from_name (str): restituisce una variabile di stato dal nome

Un oggetto Function o Modifier ha:

  • name (str): nome della funzione
  • contract (contract): il contratto in cui è dichiarata la funzione
  • nodes (list(Node)): elenco dei nodi che compongono il CFG della funzione o del modificatore
  • entry_point (Node): punto di ingresso del CFG
  • variables_read (list(Variable)): elenco delle variabili lette
  • variables_written (list(Variable)): elenco delle variabili scritte
  • state_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: @Herbie_23(opens in a new tab), 15 novembre 2023

Questo tutorial è stato utile?