Pular para o conteúdo principal

Como utilizar o Slither para encontrar bugs nos contratos inteligentes

soliditysmart contractssegurançatestandoanálise estática
Avançado
Trailofbits
Construindo contratos seguros(opens in a new tab)
9 de junho de 2020
8 minutos de leitura minute read

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 pelo pip ou usando o docker.

Slither via pip:

pip3 install --user slither-analyzer

Slither através de docker:

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

O último comando roda a 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 através 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 na linha de comando irá executar todos os detectores. Nenhum conhecimento detalhado da análise estática é necessária:

slither project_paths

Além de detectadores, o Slither possui recursos de revisão de código através de suas printers(opens in a new tab) e ferramentas(opens in a new tab).

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

Análise estática

Os recursos e design do framework estático de análise do Slither foram descritos nos posts de blog (1(opens in a new tab)), 2(opens in a new tab)) e em um documento acadêmico(opens in a new tab).

A análise estática existe em diferentes "flavors". Você provavelmente percebe que compiladores como clang(opens in a new tab) e gcc(opens in a new tab) dependem destas técnicas de pesquisa, mas também sustenta (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ós não analisaremos exaustivamente técnicas de análise estática e pesquisador aqui. Em vez disso, vamos focar no que é necessário para entender como o Slither funciona para que você possa usá-lo de forma mais eficiente para encontrar bugs e entender códigos.

Representação de código

Em contraste com uma análise dinâmica, que justifica um único caminho de execução, razões de análise estática sobre todos os caminhos ao mesmo tempo. Para isso, ele depende de uma representação diferente do código. As duas mais comuns são a árvore de sintaxe abstrata (AST) e o gráfico de fluxo de controle (CFG).

Árvores de sintaxe abstratas (AST)

AST é usado toda vez que o compilador analisa o código. É provavelmente a estrutura mais básica sobre a qual se pode efetuar a análise estática.

Em poucas palavras, a AST é uma árvore estruturada onde, normalmente, cada folha contém uma variável ou uma constante e os nós internos são operações 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}
Copiar

O AST correspondente é mostrado em:

AST

O Slither usa o AST exportado pelo solc.

Enquanto for simples construir, o AST é uma estrutura aninhada. Por vezes, esta não é a mais simples de analisar. Por exemplo, para identificar as operações usadas pela expressão a + b <= a,, primeiro você deve analisar <= e, em seguida, +. Uma abordagem comum é usar o chamado padrão de visitantes, 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 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
Copiar

Controlar Gráfico de Fluxos (CFG)

A segunda representação de código mais comum é o gráfico de fluxo de controle (CFG). Como seu nome sugere, é uma representação baseada em gráficos que expõe todos os caminhos de execução. Cada nó contém uma ou várias instruções. Bordas no gráfico representam as operações de fluxo de controle (se/então/outra vez, loop, etc). O nosso exemplo anterior é o do CFG:

CFG

O CFG é a representação que está por cima da qual se constrói a maioria das análises.

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

Análise

O tipo mais simples de análises que você pode realizar com o Slither são análises sintáticas.

Análises de sintaxe

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

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 vai aprofundar e analisar o "significado" do código. Esta família inclui vários tipos de análises. Conduzem a resultados mais poderosos e úteis, mas são também mais complexos de escrever.

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

Análise de dependência de dados

Uma variável variable_a diz ser dependente de dados variable_b se houver um caminho para o qual o valor de variable_a seja influenciado pela variable_b.

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

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

O Slither vem com capacidades embutidas 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 em "dangerous strict equality detector"(opens in a new tab). Aqui o Slither procurará por uma comparação rigorosa de igualdade com um valor perigoso (incorrect_strict_equality. y#L86-L87(opens in a new tab)), e informará o usuário que deve usar >= ou <= ao invés de ==para evitar um invasor para prender o contrato. Entre outros, o detector considerará como perigoso o valor de retorno de uma chamada para o balanceOf(endereço) (incorrect_strict_equality. y#L63-L64(opens in a new tab)), e usará o mecanismo de dependência de dados para rastrear seu uso.

Cálculo de ponto fixo

Se a sua análise navegar através do CFG e seguir as bordas, é provável que você veja os nós já visitados. Por exemplo, se um loop é apresentado como mostrado abaixo:

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

A sua análise terá de saber quando parar. Existem duas estratégias principais aqui: (1) iterar em cada nó um número finito de vezes, (2) calcular um chamado fixpoint. Um ponto de acesso basicamente significa que a análise deste nó não fornece nenhuma informação significativa.

Um exemplo de fixpoint usado pode ser encontrado nos detectadores de reentrância: Slither explora os nós, e procurar por chamadas externas, escrever e ler para armazenar. Uma vez que chegou a um ponto de correção ("fixpoint") (reentrancy.py#L125-L131(opens in a new tab)), interrompe a exploração e analisa os resultados para ver se uma reentrância está presente, através de diferentes padrões de reentrada (reentrancy_benign. y(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 um cálculo de ponto fixo eficiente requer um bom entendimento de como a análise propaga sua informação.

Representação intermediária

Uma representação intermediária (IR) é uma linguagem que deve ser mais acessível à análise estática do que a original. Slither traduz Solidity para seu próprio IR: SlithIR(opens in a new tab).

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

API Básica

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

Carregando um codebase:

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

Explorando contratos e funções

Um objeto Slither contém:

  • contractscontracts (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 de seu nome

Um objeto Slither contém:

  • name (str): Nome do contrato
  • functions (list(Function)): Lista de funções
  • modifiers (list(Modifier)): Lista de funções
  • all_functions_called (list(Função/Modificador)): Lista de todas as funções internas acessíveis pelo contrato
  • herança (lista(contrato)): Lista de contratos herdados
  • get_function_from_signature (str): Retorna uma função a partir de sua assinatura
  • get_function_from_signature (str): Retorna uma função a partir de sua assinatura
  • get_contract_from_name (str): Retorna um contrato a partir de seu nome

Um objeto Function ou Modifier têm:

  • 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(variável)): Lista de variáveis lidas
  • variables_written (list(variável)): Lista de variáveis escritas
  • state_variables_read (list(StateVariable)): Lista de variáveis de estado lidas (subconjunto de variáveis lidas)
  • state_variables_written (list(StateVariable)): Lista de variáveis de estado escritas (subconjunto de variáveis escritas)

Última edição: @guilhermevendramini(opens in a new tab), 19 de janeiro de 2024

Este tutorial foi útil?