Como utilizar o Slither para encontrar bugs nos contratos inteligentes
Como usar o Slither
O objetivo deste tutorial é mostrar como usar o Slither para localizar automaticamente bugs 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 Python
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-toolboxdocker 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.11cd /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:
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 ExpressionVisitor2from slither.core.expressions.binary_operation import BinaryOperationType34class HasAddition(ExpressionVisitor):56 def result(self):7 return self._result89 def _post_binary_operation(self, expression):10 if expression.type == BinaryOperationType.ADDITION:11 self._result = True1213visitor = HasAddition(expression) # expression is the expression to be tested14print(f'The expression {expression} has a addition: {visitor.result()}')Exibir tudoCopiar
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:
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:
State variable shadowing(opens in a new tab): itera sobre todas as variáveis de estado e verifica se tem alguma variável "shadow" de um contrato herdado (state.py#L51-L62(opens in a new tab))
Interface ERC20 incorreta(opens in a new tab): procurar por assinaturas de função ERC20 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 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 += 13}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 Slither2slither = Slither('/path/to/project')3Copiar
Explorando contratos e funções
Um objeto Slither
contém:
- contracts
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 de seu nome
Um objeto Slither
contém:
name (str)
: Nome do contratofunctions (list(Function))
: Lista de funçõesmodifiers (list(Modifier))
: Lista de funçõesall_functions_called (list(Função/Modificador))
: Lista de todas as funções internas acessíveis pelo contratoherança (lista(contrato))
: Lista de contratos herdadosget_function_from_signature (str)
: Retorna uma função a partir de sua assinaturaget_function_from_signature (str)
: Retorna uma função a partir de sua assinaturaget_contract_from_name (str)
: Retorna um contrato a partir de seu nome
Um objeto Function
ou Modifier
têm:
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(variável))
: Lista de variáveis lidasvariables_written (list(variável))
: Lista de variáveis escritasstate_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: @nhsz(opens in a new tab), 15 de agosto de 2023