WEB3DEV

Cover image for Introdução à Auditoria de Segurança de Contratos Inteligentes: DoS
Paulo Gio
Paulo Gio

Posted on

Introdução à Auditoria de Segurança de Contratos Inteligentes: DoS

Contexto

A negação de serviço (DoS) é um problema na segurança de contratos inteligentes que é semelhante à segurança de rede tradicional.

Conhecimento prévio

Negação de serviço (DoS) de segurança de rede tradicional: DoS é uma abreviação de Denial of Service, ou seja, negação de serviço. Uma negação de serviço ocorre quando uma interferência em um serviço reduz ou elimina sua disponibilidade. A seguir, exemplos de ataques comuns de negação de serviço contra protocolos de rede: Ataque SYN Flood (Inundação), IP Spoofing, UDP Flood, Ping Flood, Ataque Teardrop, Ataque LAND, Ataque Smurf, Ataque Fraggle e assim por diante.

Ataque de negação de serviço de contratos inteligentes: um problema de segurança que pode resultar em erros de lógica de código, problemas de compatibilidade ou profundidade de chamada excessiva (um recurso de máquinas virtuais de blockchain), fazendo com que os contratos inteligentes não funcionem corretamente. Os métodos de ataque de negação de serviço de contrato inteligente são relativamente simples, incluindo, mas não se limitando aos três seguintes:

  • Ataque de negação de serviço baseado na lógica do código: esse tipo de ataque de negação de serviço é normalmente causado pela imprecisão da lógica do código do contrato. O exemplo mais comum ocorre quando há uma lógica no contrato que percorre o mapeamento ou array de entrada. Quando não há limite de comprimento no mapeamento ou array de entrada, um invasor pode consumir uma grande quantidade de Gás ao percorrer um mapeamento ou array superlongo para o percurso do loop, causando o overflow de Gás da transação e, eventualmente, tornando o contrato inteligente inoperável.
  • Ataque de negação de serviço baseado em chamada externa: esse ataque de negação de serviço é causado pelo tratamento inadequado de chamadas externas pelo contrato. Por exemplo, em um contrato inteligente, existe um nó que altera o estado do contrato com base na execução de uma função externa, mas não lida com o fato de que a transação falhou. Com esse recurso, um invasor pode causar uma falha intencional de transação e o contrato inteligente continuará tentando processar os mesmos dados inválidos. Uma vez que o cartão lógico do contrato inteligente não pode mais ser usado neste ambiente, o contrato inteligente se torna temporariamente ou permanentemente ineficaz.
  • Ataque de negação de serviço baseado em gerenciamento de operação: em contratos inteligentes, por exemplo, a conta do proprietário (Owner) frequentemente atua como administrador, com privilégios extensos. Quando a função do proprietário falha ou a chave privada é perdida, a função de transferência, por exemplo, fica vulnerável a um ataque não subjetivo de negação de serviço, que pode resultar, por exemplo, na habilitação ou suspensão da função de transferência.

Exemplo de Vulnerabilidade

Na minha opinião, graças ao conhecimento comum, todos estão familiarizados com o conceito de ataque de negação de serviço. O ataque de negação de serviço de chamada externa é o tipo mais comum dos três tipos de ataques DoS. Para fornecer uma introdução completa, vamos orientá-lo através de um exemplo de código típico abaixo:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract KingOfEther {
   address public king;
   uint public balance;

   function claimThrone() external payable {
       require(msg.value > balance, "Precisa pagar mais para se tornar o rei");

       (bool sent, ) = king.call{value: balance}("");
       require(sent, "Falha ao enviar Ether");

       balance = msg.value;
       king = msg.sender;
   }
}
Enter fullscreen mode Exit fullscreen mode

Análise de Vulnerabilidade

Podemos ver no contrato acima que o objetivo é selecionar o “Rei do Ether” (King of Ether). O contrato ClaimThrone() permite que os usuários concorram pelo título de “Rei do Ether” inserindo qualquer quantidade de Ether maior que o usuário anterior. Se o valor da moeda for maior que o ETH do jogador anterior, o ETH permanecerá no contrato e o novo jogador será coroado “Rei do Ether”, enquanto o ETH do antigo jogador será devolvido a ele da mesma forma.

Podemos ver que a lógica de gerar o novo rei e retornar o antigo rei é concluída na mesma função, e o valor de retorno do reembolso também é verificado em ClaimThrone(). Vamos combinar esses recursos para completar o ataque.

Contrato de ataque

Observação: os cenários de ataque a seguir e a lógica do código de contrato são meramente exemplos e servem apenas para fins de demonstração.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Attack {
   KingOfEther kingOfEther;

   constructor(KingOfEther _kingOfEther) {
       kingOfEther = KingOfEther(_kingOfEther);
   }

   function attack() public payable {
       kingOfEther.claimThrone{value: msg.value}();
   }
}
Enter fullscreen mode Exit fullscreen mode

Vamos começar primeiro analisando o processo de ataque:

  1. Alice implanta o contrato KingOfEther.
  2. Alice chama KingOfEther.claimThrone() para enviar 1 ether para o contrato KingOfEther para se tornar o “Rei do Ether”.
  3. Bob chama KingOfEther.claimThrone() para enviar 2 ethers para o contrato KingOfEther para se tornar o novo rei.
  4. Alice recebe um reembolso de 1 ETH.
  5. Eve usa o endereço de KingOfEther para implantar o contrato de ataque.
  6. Eve chama Attack.attack() para enviar 3 ether ao contrato KingOfEther.
  7. O contrato de ataque se torna o novo rei.
  8. Bob está insatisfeito com o resultado, então ele chama KingOfEther.claimThrone() novamente, desta vez enviando 20 ethers para o contrato KingOfEther para demonstrar sua “competência financeira”.
  9. Bob descobre que suas transações foram revertidas e que ele não é mais o novo rei. Até agora, o ataque de Eve tornou o contrato KingOfEther permanentemente inválido, e o contrato de ataque se tornou o eterno “Rei do Ether”.

O rico e atraente Bob considera isso frustrante; se ele é tão rico, por que não pode ser rei?

Vamos ver o porquê.

Quando Bob chama KingOfEther.claimThrone() para enviar 20 ethers para o contrato KingOfEther, a lógica de reembolso de KingOfEther.claimThrone() será acionada e os 3 ethers de Eve serão devolvidos ao contrato de ataque. Vamos examinar mais uma vez o contrato de Ataque. Este contrato não implementa o método fallback() de payable (pagável), portanto, não pode receber ether. Como resultado, a lógica de reembolso de KingOfEther.claimThrone() sempre falhará e seu valor de retorno sempre será falso. As verificações sent, "Falha ao enviar Ether" são revertidos consistentemente. Desde que um reembolso seja acionado, depois que o contrato KingOfEther retransmitir o contrato de ataque, ninguém poderá se tornar o novo rei. Portanto, Eve executou um ataque de negação de serviço bem-sucedido.

Sugestões para Reparo

Como desenvolvedor:

  1. Deve-se prestar atenção no desenvolvimento de contratos inteligentes para lidar com falhas consistentes, como processamento assíncrono de lógica de chamada externa potencialmente falha.
  2. Ao usar Call para fazer chamadas externas, loops e percursos, fique de olho no consumo de gás.
  3. Evite o excesso de autorização de uma única função. Uma divisão razoável de permissões deve ser alcançada ao lidar com permissões de contrato, e o gerenciamento de carteiras multiassinaturas deve ser usado para funções com permissões para evitar a perda de permissão devido ao vazamento da chave privada.

Veja a seguir um exemplo de correção para o contrato vulnerável anteriormente mencionado:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract KingOfEther {    
    address public king;    
    uint public KingValue;    
    mapping(address => uint) public balances;

    function claimThrone() external payable {        
        balances[msg.sender] += msg.value;
        require(balances[msg.sender] > balance, "Need to pay more to become the king");                

        KingValue = balances[msg.sender];        
        king = msg.sender;    }
    function withdraw() public {        
        require(msg.sender != king, "Current king cannot withdraw");
        uint amount = balances[msg.sender];        
        balances[msg.sender] = 0;
        (bool sent, ) = msg.sender.call{value: amount}("");        
        require(sent, "Failed to send Ether");    
    }
}
Enter fullscreen mode Exit fullscreen mode

O mapeamento de saldos foi adicionado ao contrato de reparo, que registra a quantidade total de ether colocado no contrato por cada pessoa. Em comparação com o contrato anterior, o jogador agora pode adicionar Ether para recuperar o trono depois de perdê-lo. O ponto principal da versão reparada é que existe um método que lida com a lógica de reembolso de forma assíncrona. Para receber um reembolso, os jogadores devem chamar manualmente withdraw(). Mesmo que um jogador malicioso se recuse a aceitar Ether, isso não tem efeito e não resultará na negação de serviço mencionada anteriormente.

Como auditor:

Análise dos contratos internos:

  1. Determine se o contrato contém erros lógicos que afetam sua usabilidade.
  2. Preste bastante atenção à possibilidade de um DoS resultante da profundidade excessiva de chamadas de máquina virtual (1024 é a profundidade máxima).
  3. Concentre-se em saber se a lógica do código consome uma grande quantidade de gás.

Análise dos contratos externos:

  1. Preste atenção aos problemas de compatibilidade na interação com contratos externos, como: a compatibilidade do valor de retorno do TRC20-USDT não é processada, o que resulta no bloqueio dos tokens.
  2. Verifique se o valor de retorno da chamada do contrato externo corresponde ao resultado esperado.

Análise da gestão de direitos:

A visibilidade de todos os métodos de funções e os direitos de acesso devem ser examinados e confirmados durante a auditoria. É necessário combinar os documentos de projeto fornecidos pela equipe do projeto para confirmar se os direitos estão de acordo com as descrições dos documentos de projeto durante a auditoria. Se for constatado excesso de autorizações ou divisão pouco clara de autoridade, é essencial comunicar à equipe do projeto sobre a melhoria de seus processos e métodos para evitar erros administrativos e operacionais durante a vigência do contrato.

Artigo original publicado por SlowMist. Traduzido por Paulinho Giovannini.

Latest comments (0)