Vai al contenuto principale

Come usare Slither per trovare bug nei contratti intelligenti

Solidity
contratti intelligenti
sicurezza
test
Avanzato
Trailofbits
9 giugno 2020
8 minuti di lettura

Come usare Slither

Lo scopo di questo tutorial è mostrare come usare Slither per trovare automaticamente bug nei contratti intelligenti.

Installazione

Slither richiede Python >= 3.6. Può essere installato tramite pip o usando docker.

Slither tramite pip:

pip3 install --user slither-analyzer

Slither tramite 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 tua directory corrente. Puoi modificare i file dal tuo host ed eseguire gli strumenti sui file dal docker

All'interno di 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 rispetto a script definiti dall'utente. Slither è fornito con un set di rilevatori predefiniti che trovano molti bug comuni. Chiamare Slither dalla riga di comando eseguirà tutti i rilevatori, senza che sia necessaria alcuna conoscenza dettagliata dell'analisi statica:

slither project_paths

Oltre ai rilevatori, Slither ha capacità di revisione del codice attraverso i suoi printer (opens in a new tab) e strumenti (opens in a new tab).

Usa crytic.io (opens in a new tab) per ottenere l'accesso a rilevatori privati e all'integrazione con GitHub.

Analisi statica

Le capacità e il design del framework di analisi statica Slither sono stati descritti in post del blog (1 (opens in a new tab), 2 (opens in a new tab)) e in un documento accademico (opens in a new tab).

L'analisi statica esiste in diverse varianti. 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, ma è 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 su metodi formali come Frama-C (opens in a new tab) e Polyspace (opens in a new tab).

Non esamineremo in modo esaustivo le tecniche di analisi statica e i ricercatori qui. Ci concentreremo invece su ciò che è necessario per comprendere come funziona Slither, in modo da poterlo usare più efficacemente per trovare bug e comprendere il codice.

Rappresentazione del codice

A differenza di un'analisi dinamica, che ragiona su un singolo percorso di esecuzione, l'analisi statica ragiona su tutti i percorsi contemporaneamente. Per farlo, si affida a una diversa rappresentazione del codice. Le due più comuni sono l'albero sintattico astratto (AST) e il grafo del flusso di controllo (CFG).

Alberi Sintattici Astratti (AST)

Gli AST vengono usati ogni volta che il compilatore analizza il codice. È probabilmente la struttura più basilare su cui può essere eseguita l'analisi statica.

In breve, un AST è un albero strutturato in cui, di solito, ogni foglia contiene una variabile o una costante e i nodi interni sono operandi o operazioni del flusso di controllo. Considera il seguente codice:

1function safeAdd(uint a, uint b) pure internal returns(uint){
2 if(a + b <= a){
3 revert();
4 }
5 return a + b;
6}

L'AST corrispondente è mostrato in:

AST

Slither usa l'AST esportato da solc.

Sebbene semplice da costruire, l'AST è una struttura annidata. A volte, non è la più semplice da analizzare. Ad esempio, per identificare le operazioni usate dall'espressione a + b <= a, devi prima analizzare <= e poi +. Un approccio comune è usare il cosiddetto pattern visitor, che naviga attraverso l'albero in modo ricorsivo. Slither contiene un visitor generico in ExpressionVisitor (opens in a new tab).

Il seguente codice usa ExpressionVisitor per rilevare se l'espressione contiene un'addizione:

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 è l'espressione da testare
14print(f'The expression {expression} has a addition: {visitor.result()}')
Mostra tutto

Grafo del Flusso di Controllo (CFG)

La seconda rappresentazione del codice più comune è il grafo del flusso di controllo (CFG). Come suggerisce il nome, è una rappresentazione basata su grafi che espone tutti i percorsi di esecuzione. Ogni nodo contiene una o più istruzioni. Gli archi nel grafo 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 si basa la maggior parte delle analisi.

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 che puoi eseguire con Slither sono le analisi sintattiche.

Analisi sintattica

Slither può navigare attraverso i diversi componenti del codice e la loro rappresentazione per trovare incongruenze e difetti usando un approccio simile al pattern matching.

Ad esempio, i seguenti rilevatori cercano problemi legati alla sintassi:

Analisi semantica

A differenza dell'analisi sintattica, un'analisi semantica andrà più a fondo e analizzerà il "significato" del codice. Questa famiglia include alcuni ampi tipi di analisi. Portano a risultati più potenti e utili, ma sono anche più complessi da scrivere.

Le analisi semantiche vengono usate per i rilevamenti di vulnerabilità più avanzati.

Analisi delle dipendenze dei dati

Si dice che una variabile variable_a è dipendente dai dati di variable_b se esiste un percorso per il quale il valore di variable_a è influenzato da variable_b.

Nel seguente codice, variable_a è dipendente da variable_b:

1// ...
2variable_a = variable_b + 1;

Slither è dotato di capacità integrate di dipendenza dei dati (opens in a new tab), grazie alla sua rappresentazione intermedia (discussa in una sezione successiva).

Un esempio di utilizzo della dipendenza dei dati può essere trovato nel rilevatore di uguaglianza stretta pericolosa (opens in a new tab). Qui Slither cercherà un confronto di uguaglianza stretta con un valore pericoloso (incorrect_strict_equality.py#L86-L87 (opens in a new tab)) e informerà l'utente che dovrebbe usare >= o <= piuttosto che ==, per impedire a un utente malintenzionato di intrappolare il contratto. Tra le altre cose, il rilevatore considererà come pericoloso il valore di ritorno di una chiamata a balanceOf(address) (incorrect_strict_equality.py#L63-L64 (opens in a new tab)) e userà il motore di dipendenza dei dati per tracciarne l'utilizzo.

Calcolo del punto fisso

Se la tua analisi naviga attraverso il CFG e segue gli archi, è probabile che tu veda nodi già visitati. Ad esempio, se è presente un ciclo come mostrato di seguito:

1for(uint i; i < range; ++){
2 variable_a += 1
3}

La tua analisi dovrà sapere quando fermarsi. Ci sono due strategie principali qui: (1) iterare su ogni nodo un numero finito di volte, (2) calcolare un cosiddetto punto fisso (fixpoint). Un punto fisso significa fondamentalmente che l'analisi di questo nodo non fornisce alcuna informazione significativa.

Un esempio di punto fisso utilizzato può essere trovato nei rilevatori di rientranza: Slither esplora i nodi e cerca chiamate esterne, scritture e letture nello spazio di archiviazione. 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, attraverso diversi pattern 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)).

Scrivere analisi usando un calcolo efficiente del punto fisso richiede una buona comprensione di come l'analisi propaga le sue informazioni.

Rappresentazione intermedia

Una rappresentazione intermedia (IR) è un linguaggio concepito per essere più adatto all'analisi statica rispetto a quello originale. Slither traduce Solidity nella propria IR: SlithIR (opens in a new tab).

Comprendere SlithIR non è necessario se si desidera solo scrivere controlli di base. Tuttavia, tornerà utile se si prevede di scrivere analisi semantiche avanzate. I printer SlithIR (opens in a new tab) e SSA (opens in a new tab) ti aiuteranno a capire come viene tradotto il codice.

Basi dell'API

Slither ha un'API che ti consente di esplorare gli attributi di base del contratto e delle sue funzioni.

Per caricare una base di codice:

1from slither import Slither
2slither = Slither('/path/to/project')
3

Esplorare contratti e funzioni

Un oggetto Slither ha:

  • contracts (list(Contract): elenco dei contratti
  • contracts_derived (list(Contract): elenco dei contratti che non sono ereditati da un altro contratto (sottoinsieme di contratti)
  • get_contract_from_name (str): Restituisce un contratto dal suo nome

Un oggetto Contract ha:

  • name (str): Nome del contratto
  • functions (list(Function)): Elenco delle funzioni
  • modifiers (list(Modifier)): Elenco delle funzioni
  • all_functions_called (list(Function/Modifier)): Elenco di tutte le funzioni interne raggiungibili dal contratto
  • inheritance (list(Contract)): Elenco dei contratti ereditati
  • get_function_from_signature (str): Restituisce una Function dalla sua firma
  • get_modifier_from_signature (str): Restituisce un Modifier dalla sua firma
  • get_state_variable_from_name (str): Restituisce una StateVariable dal suo 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/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)

Ultimo aggiornamento della pagina: 3 febbraio 2025

Questo tutorial è stato utile?