WEB3DEV

Cover image for O guia definitivo para auditar um Contrato Inteligente + Ataques mais perigosos em Solidity
Rafael Ojeda
Rafael Ojeda

Posted on • Atualizado em

O guia definitivo para auditar um Contrato Inteligente + Ataques mais perigosos em Solidity

Sumário
  1. Como auditar um Contrato Inteligente
  2. Auditoria do Cassino Ethereum
  3. Disclaimer
  4. Introdução
  5. Visão geral
  6. Características agradáveis
  7. Ataques feitos ao contrato
  8. Vulnerabilidades críticas encontradas no contrato
  9. Vulnerabilidades médias encontradas no contrato
  10. Vulnerabilidades de baixa severidade encontradas
  11. Comentários linha por linha
  12. Resumo da auditoria
  13. Conclusão

O guia definitivo para auditar um Contrato Inteligente + Ataques mais perigosos em Solidity

Keep calm and continue testing

Você já se perguntou como auditar um contrato inteligente para encontrar violações de segurança?

Você pode aprender sozinho ou pode usar este útil guia passo a passo para saber exatamente o que fazer, em que momento e auditar esses contratos.

Estive pesquisando várias auditorias de Contratos Inteligentes e aprendi os passos mais comuns que eles tomaram para extrair todas as informações essenciais de qualquer contrato.

Você aprenderá o seguinte:

  • Passos a tomar para auditar completamente um Contrato Inteligente para gerar um pdf com todas as conclusões.

  • Os tipos mais importantes de ataques que você precisa conhecer como um Auditor de Contrato Inteligente Ethereum.

  • O que procurar em um contrato e dicas úteis que você não encontrará em nenhum outro lugar a não ser aqui.

Vamos direto ao assunto e começar a auditar os contratos:

Como auditar um Contrato Inteligente

Para ensinar-lhe exatamente como fazer isso, eu vou auditar um de meus próprios contratos. Desta forma, você verá uma auditoria do mundo real que você pode aplicar por si mesmo.

Agora você pode perguntar: o que é exatamente uma auditoria de Contrato Inteligente?

Uma auditoria de Contrato Inteligente é o processo que investiga cuidadosamente um pedaço de código, neste caso um contrato de Solidity para encontrar bugs, vulnerabilidades e riscos, antes que o código seja implantado e usado na rede principal da Ethereum, onde ele não será modificável. É só para fins de discussão.

Note que uma auditoria não é um documento legal que verifica que o código é seguro. Ninguém pode garantir 100% que o código não terá futuros bugs ou vulnerabilidades. É uma garantia de que seu código foi revisado por um especialista e é seguro.

Para discutir possíveis melhorias e principalmente para encontrar bugs e vulnerabilidades que possam arriscar o Ether das pessoas.

Uma vez que isso esteja claro, vamos dar uma olhada na estrutura de uma Auditoria de Contrato Inteligente:

  1. Disclaimer: Aqui você dirá que a auditoria não é um documento juridicamente vinculativo e que não garante nada. Que se trata apenas de um documento de discussão.

  2. Visão geral da auditoria e características legais: Uma visão rápida do Contrato inteligente que será auditado e boas práticas encontradas.

  3. Ataques feitos ao contrato: Nesta seção você vai falar sobre os ataques feitos ao contrato e os resultados. Apenas para verificar se ele é, de fato, seguro.

  4. Vulnerabilidades críticas encontradas no contrato: Questões críticas que poderiam prejudicar fortemente a integridade do contrato. Alguns erros que permitiriam aos atacantes roubar Ether é uma questão crítica.

  5. Vulnerabilidades médias encontradas no contrato: aquelas vulnerabilidades que poderiam danificar o contrato, mas com algum tipo de limitação. Como um bug que permite que as pessoas modifiquem uma variável aleatória.

  6. Vulnerabilidades de baixa severidade encontradas: Essas são as questões que realmente não danificam o contrato e que poderiam existir na versão implantada do contrato.

  7. Comentários linha por linha: Nesta seção, você analisará as linhas mais importantes onde você vê potenciais melhorias.

  8. Resumo da auditoria: Sua opinião sobre o contrato e conclusões finais sobre a auditoria.

Guarde essa estrutura em algum lugar seguro porque é tudo o que você precisa para realmente auditar um Contrato Inteligente de forma segura. Isso realmente o ajudará a encontrar aquelas vulnerabilidades difíceis de serem encontradas.

Recomendo que você comece pelo ponto 8 "Comentários linha por linha" porque ao analisar o contrato linha por linha, você encontrará as questões mais importantes e verá o que está faltando. O que poderia ser alterado ou melhorado.

Eu lhe mostrarei um Disclaimer que você pode usar como este para o primeiro passo da auditoria. Você pode ir ao ponto 1 e descer dali até que a auditoria esteja concluída.

Em seguida, mostrarei minha auditoria pessoal que fiz para um de meus contratos usando essa estrutura com essas etapas. Você também verá uma descrição dos ataques mais importantes que podem ser feitos a um Contrato Inteligente na etapa 4.

Auditoria do Cassino Ethereum

Você pode ver o código sendo auditado em meu github: https://github.com/merlox/casino-ethereum/blob/master/contracts/Casino.sol

Esta é a auditoria do meu contrato Cassino.sol:

Introdução

Nesta auditoria de Contrato Inteligente, cobrimos os seguintes tópicos:

  1. Disclaimer
  2. Visão geral
  3. Características agradáveis
  4. Ataques feitos ao contrato
  5. Vulnerabilidades críticas encontradas no contrato
  6. Vulnerabilidades médias encontradas no contrato
  7. Vulnerabilidades de baixa severidade encontradas
  8. Comentários linha por linha
  9. Resumo da auditoria

Disclaimer

A auditoria não faz declarações ou garantias sobre a utilidade do código, segurança do código, adequação do modelo comercial, regime regulatório do modelo comercial, ou quaisquer outras declarações sobre a adequação dos contratos à finalidade, ou seu status livre de erros. A documentação de auditoria é apenas para fins de discussão.

Visão geral

O projeto tem apenas um arquivo, o arquivo Casino.sol, que contém 142 linhas de código de Solidity. Todas as funções e variáveis de estado são bem comentadas utilizando a documentação natspec para as funções, o que é bom para entender rapidamente como tudo deve funcionar.

O projeto implementa o Oraclize API para gerar números verdadeiramente aleatórios na blockchain usando um serviço centralizado.

A geração de números aleatórios na blockchain é um tópico bastante difícil porque um dos valores centrais do Ethereum é a previsibilidade, cujo objetivo é não ter valores indefinidos.

Portanto, o uso da geração de números confiáveis do Oraclize é considerado uma boa prática, uma vez que eles geram números aleatórios fora da blockchain. Ele implementa modificadores e uma função de callback que verifica que as informações vêm de uma entidade confiável.

O objetivo deste Contrato Inteligente é participar de uma loteria aleatória onde as pessoas apostam por um número entre 1 e 9. Quando 10 pessoas fazem suas apostas, o prêmio é automaticamente distribuído entre os vencedores. Há também um valor mínimo de aposta para cada usuário.

Cada jogador só pode apostar uma vez durante cada jogo e o número vencedor só é gerado quando o limite das apostas for atingido.

Características agradáveis

O contrato oferece um bom conjunto de funcionalidades que serão úteis para todo o contrato:

Geração de números aleatórios seguros com Oraclize e verificação de prova na callback.

Modificadores para verificar o jogo final, bloqueando as funções críticas até que as recompensas sejam distribuídas.

Uma boa quantidade de verificação para verificar se a função de aposta é utilizada corretamente.

Geração segura do número vencedor somente quando o máximo de apostas tiver sido alcançado.

Ataques feitos ao contrato

A fim de verificar a segurança do contrato, testamos vários ataques a fim de garantir que o contrato é seguro e segue as melhores práticas.

Ataque de reentrância

Este ataque consiste em chamar recursivamente o método call.value() em um token ERC20 para extrair o ether armazenado no contrato se o usuário não estiver atualizando o balance do remetente antes de enviar o ether.

Quando você chama uma função para enviar o ether para um contrato, você pode usar a função fallback para executar novamente essa função até que o ether do contrato seja extraído.

Como este contrato usa transfer() em vez de call.value(), não há risco de ataques de reentrância uma vez que a função de transferência só permite usar 23.000 gas que você só pode usar para um evento para registrar dados e lançar em caso de falha.

Dessa forma, você não poderá chamar novamente a função de remetente, evitando assim o ataque de reentrância.

A função transfer é chamada apenas quando se distribuem as recompensas aos vencedores, o que acontece uma vez por jogo, quando o jogo termina. Portanto, não deve haver nenhum problema com os ataques de reentrância.

Note que a condição para chamar esta função é que o número de apostas seja maior ou igual ao limite de 10 apostas, mas esta condição não é atualizada até o final da função distributePrizes() que é arriscada porque alguém poderia teoricamente ser capaz de chamar esta função e executar toda a lógica antes de atualizar o estado.

Portanto, minha recomendação é atualizar a condição quando a função começar e ajustar o número de apostas para 0 para evitar chamar o distributePrizes() mais vezes do que o previsto.

Over e Underflows

Um overflow acontece quando o limite do variável tipo uint256 , 2**256, é excedido. O que acontece é que o valor é retornado a zero em vez de aumentar mais.

Por exemplo, se eu quiser atribuir um valor a um uint maior que 2**256, ele simplesmente irá para 0 - isto é perigoso.

Por outro lado, um underflow acontece quando se tenta subtrair de 0 um número maior que 0.

Por exemplo, se você subtrair 0 -1, o resultado será = 2**256 ao invés de -1.

Isto é bastante perigoso quando se trata de ether. No entanto, neste contrato não há subtração em nenhum lugar, portanto não há risco de underflow.

O único momento que um overflow pode acontecer é quando se bet() ( aposta ) por um número e a quantidade da variável TotalBet é aumentada:

totalBet += msg.value;

Alguém poderia enviar uma quantidade enorme de ether que excederia o limite de 2**256 e, portanto, fazer a aposta total 0. Isto é improvável, mas o risco está lá.

Portanto, recomendo o uso de uma biblioteca como a OpenZeppelin’s SafeMath.sol.

Ela o ajudará a fazer cálculos seguros sem o risco de under ou overflow.

A maneira como você a usa é importando a biblioteca, ativando-a para uint256 e depois usando as funções .mul(), .add(), sub() e .div(). Por exemplo, a função .mul(), .add(), sub() e .div():

import './SafeMath.sol';
contract Casino {
using SafeMath for uint256;
function example(uint256 _value) {
uint number = msg.value.add(_value);
}
}

Ataque de Repetição

O ataque de repetição consiste em fazer uma transação em uma blockchain como a blockchain original da Ethereum e depois repeti-la em outra blockchain como a clássica blockchain da Ethereum.

O ether é transferido como uma transação normal de uma blockchain para outra.

Embora não seja mais um problema porque desde a versão 1.5.3 de Geth e 1.4.4 de Parity ambas implementam a proteção de ataque EIP 155 de Vitalik Buterin: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md

Portanto, as pessoas que irão utilizar o contrato dependem de sua própria capacidade de se manterem atualizadas com esses programas para permanecerem seguras.

Ataque de Reordenação

Este ataque consiste em que um minerador ou outra parte tenta "competir" com um participante de um Contrato Inteligente, inserindo suas próprias informações em uma lista ou mapeamento para que o atacante possa ter sorte em obter suas próprias informações armazenadas no contrato.

Quando um usuário coloca sua bet() e os dados são salvos na blockchain, qualquer pessoa será capaz de ver qual número foi apostado, simplesmente chamando o player de mapeamento playerBetsNumber .

Esse mapeamento mostra qual número foi selecionado por cada pessoa. Assim, nos dados da transação você pode ver facilmente a quantidade de ether que foi apostada.

Isto pode acontecer na função distributePrizes() porque é chamada quando a callback da geração do número aleatório é invocada.

Como a condição dessa função não é atualizada até o final, há o risco de um ataque de reordenação.

Consequentemente, minha recomendação é como eu disse antes: atualizar a condição do número de apostas no início da função distributePrizes() para evitar este tipo de comportamento imprevisto.

Ataque de endereço curto

Este ataque afeta os tokens ERC20, foi descoberto pela Golem team e consiste no seguinte:

  • Um usuário cria uma carteira de ethereum com um traling 0, o que não é difícil porque é apenas um dígito. Por exemplo: 0xiofa8d97756as7df5sd8f75g8675ds8gsdg0

  • Depois ele compra tokens, removendo o último zero:

Comprar 1000 tokens da conta 0xiofa8d97756as7df5sd8f75g8675ds8gsdg

  • Se o contrato de tokens tiver quantidade suficiente de tokens e a função de compra não verificar o comprimento do endereço do remetente, a máquina virtual do Ethereum apenas adicionará zeros à transação até que o endereço esteja completo.

  • A máquina virtual devolverá 256000 para cada 1000 tokens comprados. Este é um bug da máquina virtual que ainda não foi corrigido, portanto, sempre que você quiser comprar tokens, certifique-se de verificar o comprimento do endereço.

O contrato não é vulnerável a este ataque, já que não é um token ERC20.

Você pode ler mais sobre o ataque aqui: http://vessenes.com/the-erc20-short-address-attack-explained/

Vulnerabilidades críticas encontradas no contrato

Não há questões críticas no contrato inteligente auditado.

Vulnerabilidades médias encontradas no contrato

A função checkPlayerExists() não é constante quando deveria.

Logo, isto aumenta os custos do gas cada vez que a função é chamada, o que é um grande problema quando se lida com muitas chamadas.

Torne isso constante e evite execuções caras de gas.

Vulnerabilidades de baixa severidade encontradas

Você está usando assert() em vez de require() em todos os casos e no início das funções `call back()` e pay().

Assert e require comportam-se de forma quase idêntica, mas a função assert é utilizada para validar o estado do contrato após fazer alterações, enquanto que o require é normalmente utilizado no topo das funções para verificar a entrada da função.

  1. Você está definindo os players da variável no início do contrato, mas não está usando-a em nenhum lugar. Remova-a se você não vai usá-la.

Comentários linha por linha

Linha 1: Você está especificando uma versão pragma com o símbolo do acento circunflexo (^) na frente, que diz ao compilador para usar qualquer versão de solidity maior que 0,4,11 .

Esta não é uma boa prática, pois poderia haver grandes mudanças entre versões que tornariam seu código instável. É por isso que eu recomendo definir uma versão fixa sem o acento como 0.4.11.

Linha 14: Você está definindo a uint variável totalBet no singular, o que não é correto, pois ela armazena a soma de todas as apostas. Minha recomendação é mudá-la para plural, totalBets ao invés de totalBet.

Linha 24: Você está definindo a variável constante em caps que é uma boa prática para saber que é uma variável fixa, não modificada.

Linha 30: Como eu disse antes, você está definindo um array player não utilizado. Retire-o se você não vai usá-lo.

Linha 60: A função checkPlayerExists() deve ser constante, mas não é. Porque não modifica o estado do contrato, torna-o constante e economiza algum gas toda vez que ele é executado.

Também é uma boa prática especificar o tipo de visibilidade que a função tem mesmo que seja o valor padrão do público para evitar confusão. Para isso, adicione explicitamente o parâmetro de visibilidade pública à função.

Linha 61: Você não está verificando se o parâmetro player é enviado e bem formatado. Certifique-se de usar um require(player != endereço(0)); no topo dessa função para checar se um endereço inválido existe ou não. Verifique também o comprimento do endereço para proteger o código contra ataques de endereços curtos, por precaução.

Linha 69: Novamente, especifique a visibilidade da função bet() para evitar confusão e saber exatamente como deveria ser chamado.

Linha 72: Use require() ao invés de assert() para verificar se a entrada da função está bem formada.

Da mesma forma, no início das funções, require() é mais frequentemente utilizada. Mude todas as assert() no início para require().

Linha 90: Você está usando uma simples soma na variável msg.value. Isto poderia levar a overflows, uma vez que o valor poderia ficar bastante grande. É por isso que recomendo verificar a existência de overflows e underflows sempre que você estiver fazendo um cálculo.

Linha 98: A função generateNumberWinner() deve ser interna, já que você não quer que ninguém a execute fora do contrato.

Linha 103: Você está salvando o resultado de oraclize_newRandomDSQuery() em uma variável bytes32. Isto não é necessário para executar a função callback. Além disso, você não está usando essa variável em nenhum lugar. Portanto, recomendo não atribuir esse valor e apenas chamar a função.

Linha 110: A função ____callback()_ deve ser externa porque você só quer que ela seja chamada de fora.

Linha 117: Essa afirmação deve ser exigida pelas razões que expliquei acima.

Linha 119: Você está usando shae() que não é uma boa prática, pois o algoritmo usado não é exatamente shae3, mas keccak256. Minha recomendação é de mudá-lo para keccak256(), em vez disso, para fins de clareza.

Linha 125: A função distributePrizes() deve ser interna porque somente o contrato deve ser capaz de chamá-la.

Linha 129: Embora você esteja usando uma matriz de tamanho variável para um loop, não é tão ruim porque a quantidade de vencedores deve ser limitada a menos de 100.

Resumo da auditoria

Em geral, o código é bem comentado e claro sobre o que ele deve fazer para cada função.

O mecanismo para apostar e distribuir recompensas é bastante simples, portanto não deve trazer grandes problemas.

Minha recomendação final seria prestar mais atenção à visibilidade das funções, pois é bastante importante definir quem deve executar as funções e seguir as melhores práticas em relação ao uso de assert, require e keccak.

Este é um contrato seguro que armazenará com segurança os fundos enquanto estiver funcionando.

Conclusão

Essa foi toda a auditoria que eu mesmo fiz usando a estrutura explicada no início. Espero que você tenha aprendido algo e agora você seja capaz de fazer auditorias seguras a outros Contratos Inteligentes.

Continue aprendendo e melhorando seu conhecimento sobre segurança de contratos, melhores práticas e novas funcionalidades.

Se você gostou deste artigo, certifique-se disso:

  • Siga-me no twitter @merunas2
  • Aplauda este artigo, desde que seu coração sinta vontade
  • Compartilhe o artigo com seus colegas desenvolvedores do Ethereum
  • Comente suas perguntas, melhorias e erros de digitação que encontrar
  • Traga-me trabalho como um desenvolvedor Ethereum. Estou sempre disposto a aceitar mais trabalho!

Artigo escrito por Merunas Grincalaitis e traduzido para o português por Rafael ojeda
Você pode ler o artigo original aqui.


Abrace a oportunidade de elevar sua jornada de desenvolvimento para um nível superior. Auditar Contratos é apenas o começo; os builds incríveis da WEB3DEV representam a chave de entrada para o emocionante cenário web3. 🚀🧑‍💻

Não perca tempo, 👉inscreva-se👈 agora mesmo e comece a desbravar o universo Blockchain!
 
Seja também WEB3DEV!

Top comments (0)