Guia para a auditoria automática de contratos inteligentes. Parte 3: Mitrilo

Advertência


Este artigo não é uma classificação da eficácia dos analisadores automáticos. Eu os aplico aos meus próprios contratos, intencionalmente sintetizando erros e estudo as reações. Esse estudo não pode ser a base para determinar “melhor ou pior”; para isso, faz sentido realizar um estudo cego em uma grande amostra de contratos, o que, dada a natureza caprichosa desse tipo de software, é extremamente difícil. É bem possível que um pequeno erro no contrato possa desabilitar uma grande parte da lógica do analisador, e um simples sinal heurístico pode adicionar uma quantidade enorme de pontos ao analisador, encontrando um bug generalizado que os concorrentes simplesmente não conseguiram adicionar. Erros na preparação e compilação de contratos também podem desempenhar um papel. Todo o software em questão é bastante jovem e está sendo constantemente desenvolvido, portanto, não tome comentários críticos como problemas irreparáveis.


O objetivo do artigo é fornecer ao leitor uma compreensão de como os métodos de análise de código em diferentes analisadores funcionam e a capacidade de usá-los corretamente, em vez de "fazer uma escolha". Uma escolha razoável é usar várias ferramentas ao mesmo tempo, com foco nas mais adequadas para o contrato analisado.


Configuração e preparação para o lançamento


O Mythril usa vários tipos de análise de uma só vez, eis alguns bons artigos sobre o assunto: o mais importante , isto ou aquilo. Antes de continuar, faz sentido lê-los.


Primeiro, vamos criar nossa própria imagem do Mythril no Docker (não importa o que queremos mudar?):


git clone https://github.com/ConsenSys/mythril-classic.git cd mythril-classic docker build -t myth . 

Agora tente executá-lo em nossos contracts/flattened.sol (eu uso o mesmo contrato discutido na introdução ), no qual existem dois contratos principais, Ownable pelo Zeppelin e nosso Booking . Ainda temos um problema com a versão do compilador, eu a corrigi da mesma maneira que no artigo anterior, adicionando linhas ao Dockerfile que substituirá a versão do compilador:


 COPY --from=ethereum/solc:0.4.20 /usr/bin/solc /usr/bin 

Após reconstruir a imagem, você pode tentar executar a análise de contrato. Imediatamente vamos usar os sinalizadores -v4 e --verbose-report para ver todos os avisos. Vamos lá:


 docker run -v $(pwd):/tmp \ -w /tmp myth:latest \ -v4 \ --verbose-report \ -x contracts/flattened.sol 

Aqui trabalhamos com um contrato achatado sem dependências. Para analisar um contrato Booking.sol separado e fazer com que o Mythril selecione todas as dependências, você pode usar algo como isto:


 docker run -v $(pwd):/tmp \ -w /tmp myth:latest \ --solc-args="--allow-paths /tmp/node_modules/zeppelin-solidity/ zeppelin-solidity=/tmp/node_modules/zeppelin-solidity" \ -v4 \ --verbose-report \ -x contracts/Booking.sol 

Prefiro trabalhar com a opção achatada, pois modificaremos muito no código. Mas o Mythril também possui um modo extremamente conveniente - --truffle , que simplesmente --truffle tudo o que a truffle , verificando todo o projeto quanto a vulnerabilidades. Outra característica importante é a capacidade de especificar o nome do contrato a ser analisado por dois pontos; caso contrário, o Mythril analisará todos os contratos que encontrar. Acreditamos que o Ownable do OpenZeppelin é um contrato seguro e só vamos analisar o Booking , portanto a linha final a ser executada é:


 docker run -v $(pwd):/tmp -w /tmp myth:latest -x contracts/flattened.sol:Booking -v4 --verbose-report 

Iniciar e implantar contrato


Iniciamos o analisador com a linha acima, examinamos a saída e obtemos, entre outras coisas, esta linha:


 mythril.laser.ethereum.svm [WARNING]: No contract was created during the execution of contract creation Increase the resources for creation execution (--max-depth or --create-timeout) The analysis was completed successfully. No issues were detected. 

Acontece que nosso contrato não foi criado e "corrigido" no emulador. É por isso que recomendo usar o sinalizador -v4 para todos os tipos de análise para ver todas as mensagens e não perder uma única importante. Vamos descobrir o que há de errado. A solução para esse problema prático é bastante importante para entender como usar o Mythril corretamente.


Portanto, estamos lendo sobre o Mythril: It uses concolic analysis, taint analysis and control flow checking to detect a variety of security vulnerabilities . Se você não conhece muito esses termos, recomendo o wiki sobre testes concólicos aqui , mas aqui está uma boa apresentação sobre a verificação de contaminação do x86. Resumindo: Mythril emula a execução de um contrato, corrige as ramificações pelas quais a execução pode ir e tenta alcançar um estado "quebrado" do contrato, classificando várias combinações de parâmetros e tentando contornar todos os caminhos possíveis. Aqui está um exemplo de diagrama de ação do artigo acima:


 1.      .   symbolic-,        . 2.      ,     ,   trace .    ,      ,    . 3.     . 4.       trace-. 5.  symbolic execution   trace,   symbolic ,    ,     ,     . 6.     ,          .    , . 7.   :   ,   ,   input-,     ,      .   input-   ,   .6    . 8.   .4 

Se você simplificá-lo bastante, o Mythril, tendo encontrado uma ramificação no código, pode entender sob quais conjuntos de variáveis ​​é possível entrar em uma e na outra ramificação. Em cada ramo, o Mythril sabe se leva a assert , transfer , selfdestruct e outros códigos de operação relevantes à segurança. Portanto, o Mythril analisa quais conjuntos de parâmetros e transações podem levar a uma violação de segurança. E a maneira como o Mythril corta ramos que nunca obtêm controle e analisa o fluxo de controle é seu truque principal. Mais detalhes sobre o intestino Mythril e a caminhada nos galhos estão escritos aqui .


Devido à natureza determinística da execução inteligente de contratos, a mesma sequência de instruções sempre leva estritamente a um conjunto de alterações de estado, independentemente da plataforma, arquitetura ou ambiente. Além disso, as funções nos contratos inteligentes são bastante curtas e os recursos são extremamente limitados; portanto, analisadores como o Mythril, combinando execução simbólica e nativa, podem funcionar de maneira extremamente eficiente para contratos inteligentes.


No processo, o Mythril usa o conceito de "estado" - este é o código do contrato, seu ambiente, um ponteiro para o comando atual, armazenamento do contrato e o estado da pilha. Aqui está a documentação:


 The machine state μ is defined as the tuple (g, pc, m, i, s) which are the gas available, the program counter pc ∈ P256, the memory contents, the active number of words in memory (counting continuously from position 0), and the stack contents. The memory contents μm are a series of zeroes of size 256. 

O gráfico de transição entre estados é o principal objeto de estudo. No caso de uma inicialização bem-sucedida da análise, as informações sobre este gráfico são exibidas no log de análise. Além disso, o Mythril pode criar esse gráfico de forma legível por humanos, usando a opção --graph .


Agora, entendendo mais ou menos o que o Mythril fará, continuaremos a entender por que o contrato não é analisado e de onde veio [WARNING]: No contract was created during the execution of contract creation . Para começar, torci as --create-timeout e --max-depth (conforme recomendado) e, não obtendo o resultado, pensei que o construtor fosse o culpado - algo nele não funcionava. Aqui está o código dele:


 function Booking( string _description, string _fileUrl, bytes32 _fileHash, uint256 _price, uint256 _cancellationFee, uint256 _rentDateStart, uint256 _rentDateEnd, uint256 _noCancelPeriod, uint256 _acceptObjectPeriod ) public payable { require(_price > 0); require(_price > _cancellationFee); require(_rentDateStart > getCurrentTime()); require(_rentDateEnd > _rentDateStart); require(_rentDateStart+_acceptObjectPeriod < _rentDateEnd); require(_rentDateStart > _noCancelPeriod); m_description = _description; m_fileUrl = _fileUrl; m_fileHash = _fileHash; m_price = _price; m_cancellationFee = _cancellationFee; m_rentDateStart = _rentDateStart; m_rentDateEnd = _rentDateEnd; m_noCancelPeriod = _noCancelPeriod; m_acceptObjectPeriod = _acceptObjectPeriod; } 

Lembre-se do algoritmo de ação do Mythril. Para executar o rastreio, é necessário chamar o construtor do contrato, porque toda a execução subsequente dependerá de quais parâmetros o construtor foi chamado. Por exemplo, se você chamar o construtor com _price == 0 , o construtor lançará uma exceção ao require(_price > 0) . Mesmo que o Mythril itere sobre os muitos valores de _price , o construtor ainda será interrompido se, por exemplo, _price <= _cancellationFee . Neste contrato, há uma dúzia de parâmetros associados a restrições estritas, e o Mythril, é claro, não consegue adivinhar as combinações válidas de parâmetros. Ele tenta ir para o próximo ramo de execução, classificando os parâmetros do construtor, mas ele praticamente não tem chance de adivinhar - há muitas combinações de parâmetros. Portanto, o cálculo do contrato não dá certo - todas as formas require(...) de algum tipo de require(...) , e obtemos o problema acima.


Agora, temos duas maneiras: a primeira é desativar todos os require no construtor, comentando-os. O Mythril poderá chamar o construtor com qualquer conjunto de parâmetros e tudo funcionará. Mas isso significa que, examinando um contrato com esses parâmetros, o Mythril encontrará erros possíveis com valores incorretos passados ​​ao construtor. Simplificando, se o Mythril encontrar um erro que surgir, se o criador do contrato especificar _cancellationFee um bilhão de vezes o preço do aluguel do _mprice , não haverá utilidade nesse erro - esse contrato nunca será bloqueado e os recursos para encontrar erros serão gastos. Implicamos que o contrato ainda está preso a parâmetros mais ou menos consistentes; portanto, para análises posteriores, faz sentido especificar parâmetros construtores mais realistas para que o Mythril não procure por erros que nunca ocorrerão se o contrato for fechado corretamente.


Passei muitas horas tentando entender exatamente onde a implantação é interrompida, incluindo e desativando várias partes do construtor. Além dos meus problemas, o construtor usa getCurrentTime() , que retorna a hora atual, e não está claro como essa chamada lida com o Mythril. Não vou descrever essas aventuras aqui, porque provavelmente com o uso regular, essas sutilezas serão conhecidas pelo auditor. Portanto, escolhi a segunda maneira: limitar os dados de entrada e simplesmente remover todos os parâmetros do construtor, mesmo getCurrentTime() , simplesmente codificou os parâmetros necessários diretamente no construtor (idealmente, esses parâmetros devem ser obtidos do cliente):


  function Booking( ) public payable { m_description = "My very long booking text about hotel and beautiful sea view!"; m_fileUrl = "https://ether-airbnb.bam/some-url/"; m_fileHash = 0x1628f3170cc16d40aad2e8fa1ab084f542fcb12e75ce1add62891dd75ba1ffd7; m_price = 1000000000000000000; // 1 ETH m_cancellationFee = 100000000000000000; // 0.1 ETH m_rentDateStart = 1550664800 + 3600 * 24; // current time + 1 day m_rentDateEnd = 1550664800 + 3600 * 24 * 4; // current time + 4 days m_acceptObjectPeriod = 3600 * 8; // 8 hours m_noCancelPeriod = 3600 * 24; // 1 day require(m_price > 0); require(m_price > m_cancellationFee); require(m_rentDateStart > 1550664800); require(m_rentDateEnd > m_rentDateStart); require((m_rentDateStart + m_acceptObjectPeriod) < m_rentDateEnd); require(m_rentDateStart > m_noCancelPeriod); } 

Além disso, para que tudo comece, você também deve definir o parâmetro max-depth . Funcionou para mim com esse construtor com --max-depth=34 na instância do AWS t2.medium. Ao mesmo tempo, no meu laptop, que é mais poderoso, tudo começa sem nenhuma max-depth . A julgar pelo uso desse parâmetro , é necessário construir ramificações para análise, e seu valor padrão é infinito ( código ). Portanto, gire este parâmetro com rotação, mas garanta que o contrato desejado seja analisado. Você pode entender isso por mensagens como:


 mythril.laser.ethereum.svm [INFO]: 248 nodes, 247 edges, 2510 total states mythril.laser.ethereum.svm [INFO]: Achieved 59.86% coverage for code: ............. 

A primeira linha descreve apenas o gráfico que será analisado, leia o resto das linhas você mesmo. Recursos computacionais sérios são necessários para analisar as várias ramificações que podem ser executadas; portanto, ao analisar contratos grandes, você terá que esperar até em um computador rápido.


Procure por erros


Agora vamos procurar erros e adicionar os nossos. O Mythril procura ramificações nas quais a transmissão, a autodestruição, a afirmação e outras ações importantes do ponto de vista da segurança ocorrem. Se uma das instruções acima for encontrada em algum lugar do código do contrato, o Mythril estuda as maneiras pelas quais é possível chegar a essa agência e, além disso, exibe a sequência de transações que levam a essa agência!


Primeiro, vamos ver o que o Mythril emitiu para o contrato de Booking longa duração. Primeiro aviso:


 ==== Dependence on predictable environment variable ==== SWC ID: 116 Severity: Low Contract: Booking Function name: fallback PC address: 566 Estimated Gas Usage: 17908 - 61696 Sending of Ether depends on a predictable variable. The contract sends Ether depending on the values of the following variables: - block.timestamp Note that the values of variables like coinbase, gaslimit, block number and timestamp are predictable and/or can be manipulated by a malicious miner. Don't use them for random number generation or to make critical decisions. -------------------- In file: contracts/flattened.sol:142 msg.sender.transfer(msg.value-m_price) 

e surge porque


 require(m_rentDateStart > getCurrentTime()); 

na função de fallback.


Observe que o Mythril percebeu que getCurrentTime() oculto em getCurrentTime() . Apesar do significado do contrato não ser um erro, o fato de o Mythril associar block.timestamp à transmissão é excelente! Nesse caso, o programador deve entender que a decisão é tomada com base no valor que o mineiro pode controlar. E, se no futuro surgir um leilão ou outro leilão de um serviço neste local do contrato, é preciso levar em consideração a possibilidade de ataques anteriores.


Vamos ver se o Mythril vê uma dependência do block.timestamp se escondermos a variável em uma chamada aninhada como esta:


 function getCurrentTime() public view returns (uint256) { - return now; + return getCurrentTimeInner(); } + function getCurrentTimeInner() internal returns (uint256) { + return now; + } 

E sim! O Mythril continua vendo a conexão entre block.timestamp e a transferência da transmissão, isso é extremamente importante para o auditor. A conexão entre a variável controlada pelo invasor e a decisão tomada após várias alterações no estado do contrato pode ser muito mascarada pela lógica, e o Mythril permite que você rastreie. Embora não valha a pena confiar no fato de que todas as conexões possíveis entre todas as variáveis ​​possíveis serão getCurrentTime() para você: se você continuar zombando da função getCurrentTime() e fizer uma profundidade de aninhamento tripla, o aviso desaparecerá. Cada chamada de função para o Mythril requer a criação de novas ramificações de estado, portanto, analisar níveis muito profundos de aninhamento exigirá recursos enormes.


É claro que há uma chance bastante séria de eu simplesmente usar incorretamente os parâmetros de análise ou o ponto de corte ocorrer em algum lugar nas profundezas do analisador. Como eu disse, o produto está em desenvolvimento ativo, no momento em que escrevo, vejo confirmações no repositório com a menção de max-depth ; portanto, não leve a sério os problemas atuais, já encontramos evidências suficientes de que o Mythril pode efetivamente procurar conexões implícitas entre variáveis.


Primeiro, adicione ao contrato uma função que transmita a transmissão a qualquer pessoa, mas somente após o cliente enviar a transmissão ao contrato. State.PAID que qualquer pessoa pegasse 1/5 do éter, mas somente quando o contrato estiver no estado State.PAID (ou seja, somente depois que o cliente pagou o número alugado com o éter). Aqui está a função:


 function collectTaxes() external onlyState(State.PAID) { msg.sender.transfer(address(this).balance / 5); } 

Mythril encontrou o problema:


 ==== Unprotected Ether Withdrawal ==== SWC ID: 105 Severity: High Contract: Booking Function name: collectTaxes() PC address: 2492 Estimated Gas Usage: 2135 - 2746 Anyone can withdraw ETH from the contract account. Arbitrary senders other than the contract creator can withdraw ETH from the contract account without previously having sent a equivalent amount of ETH to it. This is likely to be a vulnerability. -------------------- In file: contracts/flattened.sol:149 msg.sender.transfer(address(this).balance / 5) -------------------- -------------------- Transaction Sequence: { "2": { "calldata": "0x", "call_value": "0xde0b6b3a7640000", "caller": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" }, "3": { "calldata": "0x01b613a5", "call_value": "0x0", "caller": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" } } 

Ótimo, ou seja, Mythril chegou a realizar duas transações, que levam ao fato de que você pode retirar o éter do contrato. Agora altere o requisito State.RENT para State.RENT , desta forma:


 - function collectTaxes() external onlyState(State.PAID){ + function collectTaxes() external onlyState(State.RENT) { 

Agora collectTaxes() pode ser chamado apenas quando o contrato estiver no estado State.RENT e, neste momento, não há nada na balança, porque o contrato já enviou a transmissão inteira ao proprietário. E o importante aqui é que o Mythril desta vez NÃO gera o erro ==== Unprotected Ether Withdrawal ==== ! Sob a condição onlyState(State.RENT) , o analisador não chegou à ramificação de código que envia éter do contrato com um saldo diferente de zero. O Mythril passou por opções diferentes para os parâmetros, mas você pode entrar no State.RENT apenas enviando toda a transmissão ao locador. Portanto, é impossível chegar a esse ramo do código com um saldo diferente de zero, e o Mythril absolutamente não incomoda o auditor!


Da mesma forma, o Mythril encontrará selfdestruct e assert , mostrando ao auditor quais ações podem levar à destruição do contrato ou à quebra de uma função importante. Não darei esses exemplos, apenas tentarei criar uma função semelhante à acima, apenas chamando selfdestruct - selfdestruct e distorcer sua lógica.


Além disso, não esqueça que uma das partes do Mythril é a execução simbólica, e essa abordagem, por si só, sem emular a execução, pode determinar muitas vulnerabilidades. Por exemplo, qualquer uso de "+", "-" e outros operadores aritméticos pode ser considerado uma vulnerabilidade de "Estouro de número inteiro" se um dos operandos for de alguma forma controlado pelo invasor. Mas repito novamente, o recurso mais poderoso do Mythril é a combinação de execução simbólica e nativa e a determinação de valores de parâmetros que levam à ramificação lógica.


Conclusão


Obviamente, para mostrar toda a gama de possíveis problemas que o Mythril é capaz de detectar, serão necessários não apenas um, mas vários artigos. Para todo o resto, ele sabe fazer tudo isso em um blockchain real, encontrando os contratos e vulnerabilidades necessários por assinaturas, criando belos gráficos de chamadas, formatando relatórios. O Mythril também permite que você escreva seus próprios scripts de teste, fornecendo uma interface baseada em python ao contrato e testando funções individuais, corrigindo valores de parâmetros ou até implementando sua própria estratégia para trabalhar com código desmontado com um grau de flexibilidade arbitrário.


O Mythril ainda é um software bastante jovem, esse não é o IDA Pro e há muito pouca documentação, exceto alguns artigos. O valor de muitos parâmetros só pode ser lido no código Mythril, começando com cli.py. Espero que uma descrição completa e aprofundada da operação de cada parâmetro apareça na documentação.


Além disso, quando o contrato é mais ou menos grande, a saída de vários erros ocupa muito espaço, mas eu gostaria de receber informações compactadas sobre o erro encontrado, porque ao trabalhar com o Mythril, você deve definitivamente observar o caminho da análise, ver quais contratos foram testados da melhor maneira possível e poder desativar à força erros específicos que o auditor considera falso-positivos.


Mas, em geral, o Mythril é uma ferramenta excelente e extremamente poderosa para analisar contratos inteligentes e, no momento, deve estar no arsenal de qualquer auditor. Ele permite que você preste pelo menos atenção a partes críticas do código e detecte relacionamentos ocultos entre variáveis.


Para resumir, as recomendações para o uso do Mythril são:


  1. Limitar as condições iniciais do contrato em estudo Se, durante a análise, o Mythril gastar muitos recursos em ramificações que nunca serão implementadas na prática, ele perderá a capacidade de encontrar erros realmente importantes, portanto, você deve sempre tentar diminuir a área de ramificações em potencial.
  2. Verifique se a análise do contrato foi iniciada, não perca mensagens como mythril.laser.ethereum.svm [WARNING]: No contract was created during the execution of contract creation Increase the resources for creation execution (--max-depth or --create-timeout) , caso contrário, você pode considerar erroneamente que não há bugs.
  3. Você pode desabilitar arbitrariamente ramificações no código do contrato, dando ao Mythril menos variação na escolha de ramificações e economizando recursos. Tente fazer sem restrições max-depth , para não "cortar" a análise, mas tome cuidado para não mascarar o erro.
  4. Preste atenção a cada aviso, mesmo comentários leves às vezes valem a pena adicionar pelo menos um comentário ao código do contrato, facilitando para outros desenvolvedores.

No próximo artigo, trataremos do analisador Manticore, mas aqui está o índice dos artigos prontos ou planejados para a escrita:


Parte 1. Introdução. Compilação, nivelamento, versões do Solidity
Parte 2. Slither
Parte 3. Mythril (este artigo)
Parte 4. Manticore (durante a escrita)
Parte 5. Equidna (durante a escrita)

Source: https://habr.com/ru/post/pt442114/


All Articles