Pular para o conteúdo principal

Como usar o Slither para encontrar bugs em contratos inteligentes

Solidity
contratos inteligentes
segurança
tes
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 encontrar bugs automaticamente em contratos inteligentes.

Instalação

O Slither requer Python >= 3.6. Ele pode ser instalado através do pip ou usando o Docker.

Slither através do pip:

pip3 install --user slither-analyzer

Slither através do 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 a partir 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 a partir da linha de comando executará todos os detectores, sem a necessidade de conhecimento detalhado de análise estática:

slither project_paths

Além dos detectores, o Slither possui recursos de revisão de código através 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

Os recursos e o design do framework de análise estática do Slither foram descritos em postagens 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 formas. 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 ela 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ão revisaremos exaustivamente as técnicas de análise estática e pesquisas aqui. Em vez disso, focaremos no que é necessário para entender como o Slither funciona, para que você possa usá-lo de forma mais eficaz para encontrar bugs e entender o código.

Representação de código

Em contraste com uma análise dinâmica, que raciocina sobre um único caminho de execução, a análise estática raciocina sobre todos os caminhos de uma só vez. Para fazer isso, ela depende de 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 toda vez 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:

function safeAdd(uint a, uint b) pure internal returns(uint){
    if(a + b <= a){
        revert();
    }
    return a + b;
}

A AST correspondente é mostrada em:

AST

O Slither usa a AST exportada pelo solc.

Embora simples de construir, a AST é uma estrutura aninhada. Às vezes, não é a 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 visitor (visitante), que navega pela árvore recursivamente. O Slither contém um visitor 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:

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 grafos 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.

Muitas outras representações de código existem. 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 as análises sintáticas.

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 semelhante à correspondência de padrões (pattern matching).

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

Análise semântica

Em contraste com a análise de sintaxe, uma análise semântica irá mais fundo e analisará o "significado" do código. Esta família inclui alguns tipos amplos 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 é dependente de dados de variable_b se houver um caminho para o qual o valor de variable_a seja influenciado por variable_b.

No código a seguir, variable_a é dependente de variable_b:

// ...
variable_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 igualdade estrita perigosa (opens in a new tab). Aqui, o Slither procurará por 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 ele deve usar >= ou <= em vez de ==, para evitar que um invasor prenda o contrato. Entre outros, o detector considerará como 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, é provável que você veja nós já visitados. Por exemplo, se um loop for apresentado conforme mostrado abaixo:

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

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) calcular o chamado ponto fixo (fixpoint). Um ponto fixo significa basicamente que analisar este nó não fornece nenhuma informação significativa.

Um exemplo de ponto fixo usado pode ser encontrado nos detectores de reentrada: o Slither explora os nós e procura por chamadas externas, gravação e leitura no armazenamento. Uma vez que atinge um ponto fixo (reentrancy.py#L125-L131 (opens in a new tab)), ele interrompe a exploração e analisa os resultados para ver se uma reentrada está presente, através de diferentes padrões de reentrada (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 um bom entendimento de como a análise propaga suas informações.

Representação intermediária

Uma representação intermediária (IR) é uma linguagem destinada a ser mais favorável à análise estática do que a original. O Slither traduz Solidity para sua própria IR: 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.

Noções básicas da API

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

Para carregar uma base de código:

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

Explorando contratos e funções

Um objeto Slither possui:

  • 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 possui:

  • name (str): Nome do contrato
  • functions (list(Function)): Lista de funções
  • modifiers (list(Modifier)): Lista de funções
  • 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 Function a partir de sua assinatura
  • get_modifier_from_signature (str): Retorna um Modifier a partir de sua assinatura
  • get_state_variable_from_name (str): Retorna uma StateVariable a partir de seu nome

Um objeto Function ou Modifier possui:

  • 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 gravadas
  • 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 gravadas (subconjunto de variables`written)