Pular para o conteúdo principal

Como usar o Slither para encontrar bugs em contratos inteligentes

Solidity
smart contracts
segurança
testando
Avançado
Trailofbits
9 de junho de 2020
8 minutos de leitura

Como usar o Slither

O objetivo deste tutorial é mostrar como usar o Slither para localizar automaticamente bugs em contratos inteligentes.

Instalação

O Slither requer a versão 3.6 do Python ou superior. Pode ser instalado via pip ou usando o docker.

Slither via pip:

pip3 install --user slither-analyzer

Slither via docker:

docker pull trailofbits/eth-security-toolbox
docker run -it -v "$PWD":/home/trufflecon trailofbits/eth-security-toolbox

O último comando executa o eth-security-toolbox em um docker que tem acesso ao seu diretório atual. Você pode alterar os arquivos do seu host e executar as ferramentas nos arquivos do docker

Dentro do docker, execute:

solc-select 0.5.11
cd /home/trufflecon/

Executando um script

Para executar um script python com python 3:

python3 script.py

Linha de comando

Linha de comando versus scripts definidos pelo usuário. O Slither vem com um conjunto de detectores predefinidos que encontram muitos bugs comuns. Chamar o Slither pela linha de comando executará todos os detectores, não é necessário conhecimento detalhado de análise estática:

slither project_paths

Além dos detectores, o Slither tem recursos de revisão de código por meio de seus printers (opens in a new tab) e ferramentas (opens in a new tab).

Use o crytic.io (opens in a new tab) para obter acesso a detectores privados e integração com o GitHub.

Análise estática

As capacidades e o design da estrutura de análise estática Slither foram descritos em posts de blog (1 (opens in a new tab), 2 (opens in a new tab)) e em um artigo acadêmico (opens in a new tab).

A análise estática existe em diferentes "sabores". Você provavelmente percebe que compiladores como clang (opens in a new tab) e gcc (opens in a new tab) dependem dessas técnicas de pesquisa, mas elas também são a base para (Infer (opens in a new tab), CodeClimate (opens in a new tab), FindBugs (opens in a new tab)) e ferramentas baseadas em métodos formais como Frama-C (opens in a new tab) e Polyspace (opens in a new tab).

Não faremos uma revisão exaustiva das técnicas de análise estática e dos pesquisadores aqui. Em vez disso, vamos nos concentrar no que é necessário para entender como o Slither funciona para que você possa usá-lo com mais eficácia para encontrar bugs e entender o código.

Representação de código

Em contraste com uma análise dinâmica, que analisa um único caminho de execução, a análise estática analisa todos os caminhos de uma só vez. Para isso, ela se baseia em uma representação de código diferente. As duas mais comuns são a árvore de sintaxe abstrata (AST) e o grafo de fluxo de controle (CFG).

Árvores de Sintaxe Abstrata (AST)

As ASTs são usadas sempre que o compilador analisa o código. É provavelmente a estrutura mais básica sobre a qual a análise estática pode ser realizada.

Em resumo, uma AST é uma árvore estruturada onde, geralmente, cada folha contém uma variável ou uma constante, e os nós internos são operandos ou operações de fluxo de controle. Considere o seguinte código:

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

A AST correspondente é mostrada em:

AST

O Slither usa a AST exportada pelo solc.

Embora seja simples de construir, a AST é uma estrutura aninhada. Às vezes, essa não é a forma mais direta de analisar. Por exemplo, para identificar as operações usadas pela expressão a + b <= a, você deve primeiro analisar <= e depois +. Uma abordagem comum é usar o chamado padrão de visitante, que navega pela árvore recursivamente. O Slither contém um visitante genérico em ExpressionVisitor (opens in a new tab).

O código a seguir usa o ExpressionVisitor para detectar se a expressão contém uma adição:

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()}')
Exibir tudo

Grafo de Fluxo de Controle (CFG)

A segunda representação de código mais comum é o grafo de fluxo de controle (CFG). Como o nome sugere, é uma representação baseada em grafo que expõe todos os caminhos de execução. Cada nó contém uma ou várias instruções. As arestas no grafo representam as operações de fluxo de controle (if/then/else, loop, etc). O CFG do nosso exemplo anterior é:

CFG

O CFG é a representação sobre a qual a maioria das análises é construída.

Existem muitas outras representações de código. Cada representação tem vantagens e desvantagens, dependendo da análise que você deseja realizar.

Análise

O tipo mais simples de análise que você pode realizar com o Slither é a análise sintática.

Análise de sintaxe

O Slither pode navegar pelos diferentes componentes do código e sua representação para encontrar inconsistências e falhas usando uma abordagem do tipo correspondência de padrões.

Por exemplo, os seguintes detectores procuram por problemas relacionados à sintaxe:

Análise semântica

Em contraste com a análise de sintaxe, uma análise semântica irá mais a fundo e analisará o "significado" do código. Esta família inclui alguns tipos abrangentes de análises. Elas levam a resultados mais poderosos e úteis, mas também são mais complexas de escrever.

As análises semânticas são usadas para as detecções de vulnerabilidades mais avançadas.

Análise de dependência de dados

Diz-se que uma variável variable_a tem dependência de dados de variable_b se houver um caminho no qual o valor de variable_a é influenciado por variable_b.

No código a seguir, variable_a depende de variable_b:

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

O Slither vem com recursos integrados de dependência de dados (opens in a new tab), graças à sua representação intermediária (discutida em uma seção posterior).

Um exemplo de uso de dependência de dados pode ser encontrado no detector de igualdades estritas perigosas (opens in a new tab). Aqui, o Slither procurará por uma comparação de igualdade estrita com um valor perigoso (incorrect_strict_equality.py#L86-L87 (opens in a new tab)), e informará ao usuário que deve usar >= ou <= em vez de ==, para evitar que um invasor prenda o contrato em uma armadilha. Entre outras coisas, o detector considerará perigoso o valor de retorno de uma chamada para balanceOf(address) (incorrect_strict_equality.py#L63-L64 (opens in a new tab)) e usará o mecanismo de dependência de dados para rastrear seu uso.

Computação de ponto fixo

Se sua análise navegar pelo CFG e seguir as arestas, você provavelmente verá nós já visitados. Por exemplo, se um loop for apresentado como mostrado abaixo:

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

Sua análise precisará saber quando parar. Existem duas estratégias principais aqui: (1) iterar em cada nó um número finito de vezes, (2) computar um chamado ponto fixo. Um ponto fixo basicamente significa que analisar este nó não fornece nenhuma informação significativa.

Um exemplo de uso de ponto fixo pode ser encontrado nos detectores de reentrância: o Slither explora os nós e procura por chamadas externas, escrita e leitura no armazenamento. Depois de atingir um ponto fixo (reentrancy.py#L125-L131 (opens in a new tab)), ele interrompe a exploração e analisa os resultados para ver se há uma reentrância, por meio de diferentes padrões de reentrância (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)).

Escrever análises usando computação de ponto fixo eficiente requer uma boa compreensão de como a análise propaga suas informações.

Representação intermediária

Uma representação intermediária (RI) é uma linguagem destinada a ser mais adequada à análise estática do que a original. O Slither traduz Solidity para sua própria RI: SlithIR (opens in a new tab).

Entender o SlithIR não é necessário se você quiser apenas escrever verificações básicas. No entanto, será útil se você planeja escrever análises semânticas avançadas. Os printers SlithIR (opens in a new tab) e SSA (opens in a new tab) ajudarão você a entender como o código é traduzido.

Conceitos básicos da API

O Slither tem uma API que permite explorar atributos básicos do contrato e suas funções.

Para carregar uma base de código:

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

Explorando contratos e funções

Um objeto Slither tem:

  • contracts (list(Contract): lista de contratos
  • contracts_derived (list(Contract): lista de contratos que não são herdados por outro contrato (subconjunto de contratos)
  • get_contract_from_name (str): retorna um contrato a partir do seu nome

Um objeto Contract tem:

  • name (str): nome do contrato
  • functions (list(Function)): lista de funções
  • modifiers (list(Modifier)): lista de modificadores
  • all_functions_called (list(Function/Modifier)): lista de todas as funções internas alcançáveis pelo contrato
  • inheritance (list(Contract)): lista de contratos herdados
  • get_function_from_signature (str): retorna uma função a partir de sua assinatura
  • get_modifier_from_signature (str): retorna um modificador a partir de sua assinatura
  • get_state_variable_from_name (str): retorna uma StateVariable a partir do seu nome

Um objeto Function ou Modifier tem:

  • name (str): nome da função
  • contract (contract): o contrato onde a função é declarada
  • nodes (list(Node)): lista dos nós que compõem o CFG da função/modificador
  • entry_point (Node): ponto de entrada do CFG
  • variables_read (list(Variable)): lista de variáveis lidas
  • variables_written (list(Variable)): lista de variáveis escritas
  • state_variables_read (list(StateVariable)): lista de variáveis de estado lidas (subconjunto de variables_read)
  • state_variables_written (list(StateVariable)): lista de variáveis de estado escritas (subconjunto de variables_written)

Última atualização da página: 3 de fevereiro de 2025

Este tutorial foi útil?