Guia para a auditoria automática de contratos inteligentes. Parte 1: Preparando para uma auditoria

1. Introdução


Nossa empresa está envolvida na auditoria de segurança de contratos inteligentes, e a questão do uso de ferramentas automatizadas é muito aguda. Quanto eles podem ajudar a identificar locais suspeitos, quais devem ser usados, o que podem fazer e quais são as especificidades do trabalho nessa área? Esses e problemas relacionados são o assunto deste artigo. E o material será tentativas de trabalhar com contratos reais com a ajuda dos representantes e receitas mais interessantes para o lançamento deste software extremamente heterogêneo e extremamente interessante. No começo, eu queria criar um artigo, mas depois de algum tempo a quantidade de informações ficou muito grande, por isso foi decidido criar uma série de artigos, um para cada analisador automático. A lista da qual vamos pegar as ferramentas é apresentada, por exemplo, aqui , mas se outras ferramentas interessantes surgirem durante a escrita, eu as descreverei com prazer e as testarei.


Devo dizer que as tarefas de auditoria foram extremamente interessantes, porque até o momento, os desenvolvedores não prestaram muita atenção aos aspectos econômicos dos algoritmos e à otimização interna. E a auditoria de contratos inteligentes adicionou vários vetores de ataque interessantes que devem ser considerados ao procurar erros. Além disso, surgiram muitas ferramentas para testes automáticos: analisadores estáticos, analisadores de bytecode, fuzzers, analisadores e muitos outros bons softwares.


O objetivo do artigo: promover a distribuição de código de contrato seguro e permitir que os desenvolvedores se livrem rápida e facilmente de bugs bobos que geralmente são os mais irritantes. Quando o protocolo em si é completamente confiável e resolve um problema sério, a presença de um erro estúpido esquecido no estágio de teste pode arruinar seriamente a vida do projeto. Portanto, vamos aprender a usar, no mínimo, ferramentas que permitem que “pouco sangue” se livre de problemas conhecidos.


No futuro, devo dizer que os erros críticos mais comuns que encontramos nas auditorias ainda são problemas lógicos de implementação e não vulnerabilidades típicas, como direitos de acesso, excesso de números inteiros, reentrada. Uma auditoria grande e completa das soluções é impossível sem desenvolvedores experientes, capazes de auditar a lógica de alto nível dos contratos, seu ciclo de vida, aspectos de operação real e conformidade com a tarefa, e não apenas os padrões de ataque típicos. É a lógica de alto nível que frequentemente se torna uma fonte de bugs críticos.


Mas avisos, falhas e erros típicos deixados de lado por descuido que não devem ser esquecidos são o destino dos analisadores automáticos; eles devem lidar melhor com essas tarefas do que as pessoas. É esta tese que será testada.


Recursos da auditoria de código de contrato inteligente


A auditoria inteligente de código de contrato é uma área bastante específica. Apesar de seu tamanho pequeno, o contrato inteligente da Ethereum é um programa completo que pode organizar ramificações complexas, loops, árvores de decisão e até mesmo para automatizar transações aparentemente simples, que exigem a reflexão sobre todas as ramificações possíveis a cada passo. Deste ponto de vista, o desenvolvimento de blockchain é extremamente de baixo nível, exige muito recursos e lembra muito o desenvolvimento de sistemas e softwares incorporados em C / C ++ e linguagens assembler. É por isso que adoramos ver nas entrevistas os desenvolvedores de algoritmos de baixo nível, a pilha de rede, serviços altamente carregados, todos que lidam com otimização de baixo nível e auditoria de código.


Do ponto de vista do desenvolvedor, o Solidity também é bastante específico, embora seja fácil de ler por quase qualquer programador e nos primeiros passos e pareça extremamente simples. O código de solidez é bastante fácil de ler, é familiar a qualquer desenvolvedor que conheça a sintaxe C / C ++ e o OOP, como JavaScript.


Aqui, a simplicidade do código é a chave para a sobrevivência, nada funciona, então todo o arsenal de desenvolvimento de baixo nível é usado no trabalho - algoritmos que permitem o uso eficiente de recursos, economizam memória: árvores Merkle, filtros Bloom, carregamento de recursos “preguiçosos”, desenrolamento de loop, coleta manual de lixo e muito mais
Uma pequena quantidade de código-fonte e código de código resultante.


Um contrato inteligente separado é limitado em volume de bytecode, cada byte custa uma certa quantidade de gás e o máximo é limitado acima, para que você possa inserir cerca de 10 KB no blockchain (no momento), não funcionará mais. Aqui está um bom artigo sobre quanto custa um contrato e quanto custa gás . Portanto, muito não pode ser empurrado. Se você exagera, vários milhares de linhas de código "médio" são o máximo. Várias dezenas de métodos, a falta de agregação e a lógica geralmente complexa são extremamente características dos contratos. Tudo o que não se encaixa exige que você selecione o código em bibliotecas separadas, altere e complique o procedimento de upload para a rede. Os desenvolvedores de solidez podem ter prazer em inserir vários códigos em um contrato, mas eles simplesmente precisam organizar seus sistemas de contratos corretamente, criando bibliotecas de classes separadas com seu próprio armazenamento. E é conveniente decompor essas "classes" separadas em arquivos separados e, portanto, ler o código dos contratos é muito bom, tudo está bem estruturado desde o início - simplesmente não funcionará de outra maneira. Como exemplo, recomendo ver como o ERC721 é produzido com solidez openzeppelin .


Gás, gás, gás


O gás introduz uma camada adicional de lógica na execução do código do contrato, o que requer uma auditoria. Além disso, ao contrário do código tradicional, a mesma seção de código pode gastar quantidades diferentes de gás. Uma tabela de códigos de operação EVM e seu custo é útil para entender as restrições de gás.


Para demonstrar por que você precisa dedicar muito tempo à avaliação de gás, considere este pseudo-código (é claro, irrealista; disparar no loop com éter é uma má idéia):


//          function fixSomeAccountAction(uint _actionId) public onlyValidator { // … events[msg.sender].push(_actionId); } //   ,           function receivePaymentForSavedActions() { // ... for (uint256 i = 0; i < events[msg.sender].length; i++) { //  actionId   uint actionId = events[msg.sender][i]; //      action uint payment = getPriceByEventId(actionId); if (payment > 0) { paymentAccumulators[msg.sender] += payment; } emit LogEventPaymentForAction(msg.sender, actionId, payment); // … // delete “events[msg.sender][i]” from array } } 

o fato é que o ciclo no contrato é executado eventos [msg.sender]. tempos de duração, e cada iteração é uma entrada na blockchain (transfer () e emit ()). Se o comprimento da matriz for pequeno, o ciclo cumprirá dez vezes, distribuindo o pagamento para cada ação. Porém, se a matriz de eventos [msg.sender] for grande, haverá muitas iterações e o gás gasto será executado no limite máximo de gás codificado (~ 8.000.000). A transação cairá e agora nunca funcionará, pois não há como reduzir a duração da matriz de eventos [msg.sender] no contrato. Se o ciclo não apenas calcular um valor unitário, mas gravar no blockchain (por exemplo, algumas comissões são pagas, pagamentos por ações), o número permitido de iterações é bastante limitado. Julgue por si mesmo - limite: 8.000.000, registrando um novo valor de 256 bits: 20.000. você pode salvar ou atualizar os metadados apenas para algumas centenas de endereços de 256 bits com alguns metadados.Outra parte divertida é escrever um novo valor: 20.000 e uma atualização de um existente: 5.000, mesmo com o mesmo ambiente exato do seu contrato ao fazer uma transferência tokens para um endereço que já possui tokens, você gasta 4 vezes menos gás (5.000 vs 20.000) em um registro.


Portanto, não se surpreenda que a questão do gás nos contratos inteligentes esteja tão intimamente relacionada à segurança dos contratos, porque a situação em que os fundos permanecem permanentemente presos ao contrato do ponto de vista prático difere pouco da situação em que foram roubados. O fato de a instrução ADD custar 3 gases e SSTORE (salvar no armazenamento): 20.000 significa que o recurso mais caro da blockchain é o armazenamento, e as tarefas de otimizar o código do contrato têm muito em comum com as tarefas de desenvolvimento de baixo nível em C e ASM para sistemas embarcados. sistemas, onde o armazenamento também é um recurso muito limitado.


Blockchain bonito


Este é um parágrafo muito positivo sobre por que o blockchain é tão bom do ponto de vista de segurança apenas para o auditor. O determinismo da execução do código do contrato é a chave para a depuração e reprodução bem-sucedidas de bugs e vulnerabilidades. Tecnicamente, qualquer chamada para um código de contrato pode ser reproduzida em qualquer plataforma com um pouco de precisão, isso permite que os testes funcionem em qualquer lugar e sejam extremamente fáceis de oferecer suporte, e a investigação de incidentes é confiável e inegável. Agora sempre sabemos quem quando qual função foi chamada, com quais parâmetros, qual código a processou e qual foi o resultado. Tudo isso é completamente determinado, ou seja, reproduz em qualquer lugar, mesmo em JS em uma página da web. Se falamos sobre o Ethereum, qualquer caso de teste é extremamente fácil de escrever em JavaScript conveniente, incluindo parâmetros confusos, e funciona muito bem onde quer que haja Node.js.


Todas essas palavras bonitas, no entanto, não devem relaxar os desenvolvedores, porque, como mencionado acima, os erros mais graves são lógicos e, para eles, o determinismo da execução é uma propriedade ortogonal.


O ambiente para a montagem do contrato


Para escrever o artigo, aceitei um antigo contrato experimental para reservar uma casa do designer Smartz: https://github.com/smartzplatform/constructor-eth-booking . O contrato permite que você crie um registro do objeto (apartamento ou quarto de hotel), defina o preço e as datas de entrega, após as quais o contrato aguarda pagamento e, se recebido, corrige o ato de reserva, mantendo os fundos na balança até o hóspede entrar no quarto e não confirmará a entrada. Nesse ponto, o proprietário do quarto recebe o pagamento. O contrato é essencialmente uma máquina de estados, cujos estados e transições podem ser visualizados no Booking.sol. Fizemos isso rapidamente, alteramos durante o processo de desenvolvimento e não conseguimos fazer um grande número de testes, pois está longe de ser uma nova versão do compilador e de uma lógica interna mais ou menos rica. Então, vamos ver como os analisadores lidam com isso, quais erros eles encontrarão e, se necessário, adicionamos os nossos.


Trabalhar com diferentes versões do solc


Analisadores diferentes terão que ser usados ​​de maneiras diferentes - alguns são lançados a partir do docker, outros usam bytecode compilado e o próprio auditor também precisa lidar não com alguns, mas com dezenas de contratos anteriores com versões diferentes do compilador. Portanto, você precisa " solc " diferentes versões do solc de solc diferente, tanto no sistema host quanto na imagem do docker e na trufa, para que eu lhe dê algumas opções de hack sujas:


1 maneira: dentro da trufa


Para isso, não são necessários truques, porque a partir da versão 5.0.0 do truffle, é possível especificar a versão do compilador diretamente no truffle.js, como neste diff .


Agora, a trufa fará o download do compilador necessário e o executará. Muito obrigado à equipe por isso, o Solidity é um idioma jovem, há sérias mudanças no idioma e a mudança de versão para versão para o auditor é inaceitável - dessa forma, você pode introduzir novos erros e mascarar os antigos.


Método 2: substituindo / usr / bin / solc no contêiner do docker do analisador
Se o analisador estiver distribuído na forma de um Dockerfile, você poderá substituí-lo ao montar a imagem do Dockerfile adicionando uma linha ao Dockerfile que obtém a versão necessária solc diretamente da imagem, que a extrai da rede e substitui / usr / bin / solc:


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

3 vias: substituindo / usr / bin / solc


A maneira mais suja na testa, se não houver saída, você poderá substituir o binário / usr / bin / solc com um script como este (não se esqueça de salvar o arquivo original):


 #!/bin/bash # run Solidity compiler of given version, pass all parameters # you can run “SOLC_DOCKER_VERSION=0.4.20 solc --version” SOLC_DOCKER_VERSION="${SOLC_DOCKER_VERSION:-0.4.24}" docker run \ --entrypoint "" \ --tmpfs /tmp \ -v $(pwd):/project \ -v $(pwd)/node_modules:/project/node_modules \ -w /project \ ethereum/solc:$SOLC_DOCKER_VERSION \ /usr/bin/solc \ "$@" 

Ele baixa e armazena em cache a imagem do docker com a versão correta do solc , solc o diretório atual e executa /usr/bin/solc com os parâmetros passados. Não é uma maneira muito boa, mas talvez para algumas tarefas seja adequado.


Código de nivelamento


Agora vamos descobrir a fonte. Obviamente, em teoria, os analisadores automáticos (especialmente para análise de fonte estática) devem coletar um contrato, retirar todas as dependências, combinar tudo em um monólito e analisá-lo. Mas, como eu já disse, as alterações de versão para versão podem ser sérias, e constantemente me deparei com a necessidade de colocar um diretório adicional na janela de encaixe, configurá-lo dentro do caminho e tudo isso para que ele puxe corretamente as importações necessárias. Alguns analisadores entendem tudo, o segundo não é, portanto, uma opção universal; para não sofrer a ativação de diretórios adicionais, é mais conveniente para os analisadores que comem um único arquivo mesclar tudo em um arquivo e analisá-lo apenas.


Para fazer isso, use o achatador de trufas comum .


Este é um módulo npm padrão, é usado de maneira muito simples:


 truffle-flattener contracts/Booking.sol > contracts/flattened.sol 

: https://github.com/trailofbits/slither
Se você precisar personalizar de alguma forma o nivelamento, pode escrever seu próprio nivelador, por exemplo, antes de usarmos a opção baseada em python: https://github.com/mixbytes/solidity-flattener


Vamos começar a análise.


No exemplo do mesmo velho https://github.com/smartzplatform/constructor-eth-booking, continuamos a análise. O contrato indica a versão antiga do compilador "0.4.20", e eu intencionalmente aceitei o contrato antigo para resolver problemas com o compilador. A situação é agravada pelo fato de um analisador automático, por exemplo, estudar bytecode, poder depender dessa versão do solc, e aqui as diferenças nas versões podem afetar muito os resultados ou até quebrar tudo. portanto, mesmo se você estiver fazendo tudo o que puder, usando as versões mais recentes, ainda poderá executar um analisador que foi ajustado para a versão anterior do compilador.
Compilando e executando testes


Para começar, basta retirar o projeto do github e tentar compilar.


 git clone https://github.com/smartzplatform/constructor-eth-booking.git cd constructor-eth-booking npm install truffle compile 

Certamente você tem problemas com a versão do compilador. Além disso, os analisadores automáticos também têm esses problemas, portanto, use qualquer meio para obter o compilador 0.4.20 e criar o projeto. Acabei de registrar a versão necessária do compilador no truffle.js e tudo foi montado conforme descrito acima.


Também execute


 truffle-flattener contracts/Booking.sol > contracts/flattened.sol 

conforme indicado no parágrafo sobre achatamento, são contracts/flattened.sol forneceremos para análise para diferentes analisadores
Conclusão para a introdução


Agora, com flattened.sol e a capacidade de usar solc uma versão arbitrária, você pode começar a analisar. Eu omitirei os problemas com a execução de trufas e testes, há muita documentação sobre esse assunto, resolva você mesmo. Obviamente, os testes devem ser executados e executados com êxito. Além disso, para verificar a lógica, o auditor geralmente precisa adicionar seus próprios testes, verificando locais com potencial de vazamento, por exemplo, verificando a funcionalidade do contrato nos limites das matrizes, cobrindo todas as variáveis ​​com testes, mesmo aquelas destinadas estritamente ao armazenamento de dados, etc. Existem muitas recomendações, além de ser apenas o produto que nossa empresa fornece ao mercado, portanto o estudo da lógica é uma tarefa puramente humana.


Iremos a analisadores que são interessantes do nosso ponto de vista, tentaremos incluir nosso contrato e introduziremos artificialmente vulnerabilidades para avaliar como os analisadores automáticos reagirão a eles. O próximo artigo será dedicado ao analisador Slither e, em geral, o plano de ação é aproximadamente o seguinte:


Parte 1. Introdução. Compilação, nivelamento, versões do Solidity (este artigo)
Parte 2. Slither
Parte 3. Mythril
Parte 4. Manticore
Parte 5. Equidna
Parte 6. Ferramenta desconhecida 1
Parte 7. Ferramenta desconhecida 2


Esse conjunto de analisadores foi obtido porque é importante que o auditor possa usar diferentes tipos de análise - estática e dinâmica, e eles exigem abordagens completamente diferentes. Nossa tarefa é aprender a usar as ferramentas básicas em cada tipo de análise e entender qual usar quando.


Talvez no processo de um estudo detalhado, novos candidatos apareçam para consideração ou a ordem dos artigos mude; portanto, fique atento. Para ir para a próxima parte, clique aqui

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


All Articles