Como usar o Slither para encontrar bugs em contratos inteligentes
Como usar o Slither
O objetivo deste tutorial é mostrar como usar o Slither para encontrar bugs automaticamente em contratos inteligentes.
- Instalação
- Uso da linha de comando
- Introdução à análise estática: Breve introdução à análise estática
- API: Descrição da API em Python
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:
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:
from slither.visitors.expression.expression import ExpressionVisitor
from slither.core.expressions.binary_operation import BinaryOperationType
class HasAddition(ExpressionVisitor):
def result(self):
return self._result
def _post_binary_operation(self, expression):
if expression.type == BinaryOperationType.ADDITION:
self._result = True
visitor = HasAddition(expression) # expression é a expressão a ser testada
print(f'The expression {expression} has a addition: {visitor.result()}')
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 é:
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:
-
Sombreamento de variável de estado (opens in a new tab): itera sobre todas as variáveis de estado e verifica se alguma sombreia uma variável de um contrato herdado (state.py#L51-L62 (opens in a new tab))
-
Interface ERC-20 incorreta (opens in a new tab): procura por assinaturas de função ERC-20 incorretas (incorrect_erc20_interface.py#L34-L55 (opens in a new tab))
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 contratoscontracts_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 contratofunctions (list(Function)): Lista de funçõesmodifiers (list(Modifier)): Lista de funçõesall_functions_called (list(Function/Modifier)): Lista de todas as funções internas alcançáveis pelo contratoinheritance (list(Contract)): Lista de contratos herdadosget_function_from_signature (str): Retorna uma Function a partir de sua assinaturaget_modifier_from_signature (str): Retorna um Modifier a partir de sua assinaturaget_state_variable_from_name (str): Retorna uma StateVariable a partir de seu nome
Um objeto Function ou Modifier possui:
name (str): Nome da funçãocontract (contract): o contrato onde a função é declaradanodes (list(Node)): Lista dos nós que compõem o CFG da função/modificadorentry_point (Node): Ponto de entrada do CFGvariables_read (list(Variable)): Lista de variáveis lidasvariables_written (list(Variable)): Lista de variáveis gravadasstate_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)

