WEB3DEV

Cover image for Entendendo o Ataque de Repetição de Assinatura
Rafael Ojeda
Rafael Ojeda

Posted on

Entendendo o Ataque de Repetição de Assinatura

Entendendo o Ataque de Repetição de Assinatura

Entenda como os ataques de repetição de assinatura ocorrem no Solidity e como evitá-los.

Image description

O objetivo desta postagem é fornecer uma compreensão detalhada da vulnerabilidade causada pelo ataque de repetição de assinatura e maneiras de evitá-la. Esta é uma adição à série de nosso blog anterior, em que compartilhamos os detalhes de Integer Overflow e Underflow, Front Running Attack, Entendendo a Manipulação de Timestamp (carimbo de data/hora) de Bloco, Problemas com Autorização Usando tx.origin, Perda de Precisão em Operações em Solidity e problemas com modificadores de Visibilidade em Solidity.

Introdução

As assinaturas digitais servem como uma "impressão digital" exclusiva no mundo das transações de blockchain, fornecendo um meio de autenticação em um ambiente que depende muito da confiança. Essas assinaturas formam a espinha dorsal das transações em ecossistemas de blockchain, especialmente na operação de contratos inteligentes. No entanto, se as medidas de segurança em vigor para esses contratos inteligentes não forem suficientemente robustas, eles se tornarão alvos potenciais de "ataques de repetição de assinatura". Esses ataques podem permitir transações não autorizadas e até mesmo o roubo de fundos, o que representa um risco considerável para a segurança geral e a confiabilidade dos aplicativos descentralizados.

Vulnerabilidade

No entanto, vamos supor que Bob, que está recebendo a transação de Alice, tenha intenções maliciosas. Bob encontra e copia a assinatura de Alice - sua "impressão digital" exclusiva - da transação anterior. Se a segurança do contrato inteligente não estiver à altura, Bob poderá explorar essa assinatura capturada, da mesma forma que um indivíduo inescrupuloso poderia usar uma chave de casa copiada para entrar sem autorização.

O que Bob faz em seguida com a assinatura capturada de Alice? Bob opta por reproduzir ou duplicar essa transação em outra cadeia - a rede Arbitrum, que também é compatível com o mesmo aplicativo DeFi. O resultado é uma transferência não intencional de 50 ETH da conta de Alice para a de Bob, mas dessa vez na rede Arbitrum. Isso constitui um ataque de repetição de assinatura: Alice, completamente inconsciente dessa atividade secreta, sofre uma perda imprevista sem seu conhecimento ou aprovação.

Há vários fatores que podem tornar os contratos inteligentes vulneráveis a ataques de repetição de assinatura. Um ponto fraco fundamental é a falta de um "nonce". Essencialmente um identificador exclusivo ou um "número usado uma vez" para cada transação, um nonce é essencial para a segurança. Sua ausência pode permitir que alguém como Bob explore repetidamente a assinatura capturada de Alice para transações fraudulentas.

Outro possível ponto fraco é a falha na implementação de verificações específicas para diferentes redes de blockchain. Essa situação pode ser comparada ao uso do mesmo tipo de fechadura em todas as portas, tornando todas as portas acessíveis se uma única chave for copiada. Portanto, cada assinatura de transação também deve encapsular um ID de rede, um identificador exclusivo para a rede específica, para evitar o uso indevido de assinaturas entre cadeias.

Além disso, o código do contrato inteligente em si pode ser vulnerável mesmo com as verificações de nonce e ID de rede se não conseguir verificar corretamente as assinaturas. Portanto, é fundamental empregar estruturas de desenvolvimento de contratos inteligentes bem testadas, como o OpenZeppelin, para evitar cometer erros bobos.

Cenário de ataque 1: Nonce e ID da cadeia ausentes


// SPDX-License-Identifier: MIT

pragma solidity ^ 0.8.17; 

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol"; 

contract Vault {

    using ECDSA for bytes32;

         address public owner;

     constructor() payable {

        owner = msg.sender;

    }

     function transfer(address to, uint amount, bytes[2]memory sigs) external {

        bytes32 hashed = computeHash(to, amount);

        require(validate(sigs, hashed), "invalid sig");



        (bool sent, ) = to.call{ value: amount } ("");

        require(sent, "Failed to send Ether");

    }

     function computeHash(address to, uint amount) public pure returns(bytes32) {

        return keccak256(abi.encodePacked(to, amount));

    }

     function validate(bytes[2]memory sigs, bytes32 hash) private view returns(bool) {

        bytes32 ethSignedHash = hash.toEthSignedMessageHash();

        address signer = ethSignedHash.recover(sigs[0]);

        bool valid = signer == owner;

        if (!valid) {

           return false;

        }

         return true;

    }

}

Enter fullscreen mode Exit fullscreen mode

Na função transferência, o contrato verifica a validade das assinaturas fornecidas recuperando o endereço do signatário a partir do hash da mensagem assinada e comparando-o com o endereço do proprietário.

No entanto, o código não inclui nenhum mecanismo para verificar se uma mensagem assinada de forma válida é reutilizada de forma maliciosa em um contexto diferente em outras redes. Um invasor mal-intencionado pode, portanto, capturar uma transação assinada de forma válida e reproduzi-la várias vezes, resultando em transferências não autorizadas de fundos.

A exploração decorre do fato de que o contrato verifica apenas se o endereço do signatário recuperado corresponde ao endereço do proprietário, sem considerar se a assinatura já foi usada antes. Como resultado, um invasor pode capturar uma transação assinada legitimamente e enviá-la repetidamente ao contrato, executando com êxito a função transferência várias vezes em outras redes de blockchain.

Cenário de ataque 2: ID da cadeia ausente


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

 import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol";

 contract Vault {

    using ECDSA for bytes32;

    address public owner;

    mapping(bytes32 => bool) public executed;

     constructor() payable {

        owner = msg.sender;

    }

     function transfer(address to, uint amount, uint nonce, bytes[2] memory sigs) external {

        bytes32 hashed = computeHash(to, amount, nonce);

        require(!executed[hashed], "tx already executed");

        require(validate(sigs, hashed), "invalid sig");

        executed[hashed] = true;

        (bool sent, ) = to.call{value: amount}("");

        require(sent, "Failed to send Ether");

    }

     function computeHash(address to, uint amount, uint nonce) public view returns (bytes32) {

        return keccak256(abi.encodePacked(address(this), to, amount, nonce));

    }

     function validate(bytes[2] memory sigs, bytes32 hash) private view returns (bool) {

        bytes32 ethSignedHash = hash.toEthSignedMessageHash();

        address signer = ethSignedHash.recover(sigs[0]);

        bool valid = signer == owner;

        if (!valid) {

           return false;

        }

        return true;

    }

}

Enter fullscreen mode Exit fullscreen mode

No exemplo acima, o contrato implementa nonce, o que impossibilita a repetição de assinaturas existentes porque um nonce, uma vez usado, torna-se inválido para transações futuras. Embora o contrato acima seja seguro contra ataques de repetição de assinatura em uma única cadeia, ele ainda é vulnerável a ataques de repetição de assinatura se for implementado em várias cadeias.

Contrato seguro


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;



import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol";

contract Vault {



using ECDSA for bytes32;     

address public owner;    

mapping(bytes32 => bool) public executed;

     constructor() payable {

        owner = msg.sender;

    }

     function transfer(address to, uint amount, uint nonce, uint chainId, bytes[2] memory sigs) external {

        bytes32 hashed = computeHash(to, amount, nonce, chainId);

        require(!executed[hashed], "tx already executed");

        require(validate(sigs, hashed), "invalid sig");

        executed[hashed] = true;

        (bool sent, ) = to.call{value: amount}("");

        require(sent, "Failed to send Ether");

    }

     function computeHash(address to, uint amount, uint nonce, uint chainId) public view returns (bytes32) {

        return keccak256(abi.encodePacked(address(this), to, amount, nonce, chainId));

    }

     function validate(bytes[2] memory sigs, bytes32 hash) private view returns (bool) {

        bytes32 ethSignedHash = hash.toEthSignedMessageHash();

        address signer = ethSignedHash.recover(sigs[0]);

        bool valid = signer == owner;

         if (!valid) {

            return false;

        }

         return true;

    }

}

Enter fullscreen mode Exit fullscreen mode

No exemplo acima, o contrato verifica tanto nonce quanto chainId para que a assinatura seja considerada válida. Em contraste com os exemplos anteriores, em que o contrato apenas solicitaria a assinatura de um usuário, essa estratégia permite que você seja um pouco mais específico antes de solicitar que um usuário assine uma transação. Ao usar nonce e chainId juntos no processo de assinatura, você está fornecendo uma carga útil muito mais detalhada para o usuário assinar. As chances de nonce e chainId serem reutilizados são extremamente baixas.

Prevenção

Há várias maneiras de evitar o ataque de repetição de assinatura no Solidity:

  • Um nonce é um número exclusivo criado para cada transação. O uso de um nonce exclusivo para cada transação garante que cada transação seja distinta e não possa ser replicada.
  • O uso de um valor baseado em tempo como um componente da assinatura é outro método para evitar o ataque de repetição de assinatura. Isso garante que cada transação seja única e não possa ser reproduzida após um determinado período de tempo. Isso pode ser feito no Solidity adicionando "timestamp" à assinatura.
  • Outra maneira de evitar ataques de repetição é usar um esquema de assinatura específico da cadeia, como o EIP-155, que inclui o ID da cadeia na mensagem assinada. Isso evitará que as transações assinadas em uma cadeia sejam válidas em outra cadeia com um ID diferente.

Conclusão

Resumindo, os ataques de repetição de assinatura representam uma ameaça significativa à segurança dos contratos inteligentes. É fundamental que os desenvolvedores e usuários de contratos inteligentes compreendam os riscos associados a essa vulnerabilidade e tomem as medidas adequadas para mitigá-los.

Ao reutilizar uma assinatura válida, os invasores podem executar transações não autorizadas na rede blockchain, resultando em perdas financeiras e outras consequências prejudiciais. No entanto, ao usar nonces exclusivos, valores baseados em tempo, esquemas de verificação de assinatura e estruturas bem auditadas, como o OpenZeppelin, os desenvolvedores podem impedir efetivamente os ataques de repetição de assinatura.

Sobre nós

O projeto Neptune Mutual protege a comunidade Ethereum contra ameaças cibernéticas. O protocolo usa cobertura paramétrica em vez de seguro discricionário. Ele tem um processo de reivindicação fácil e confiável na cadeia. Isso significa que, quando os incidentes são confirmados por nossa comunidade, a resolução é rápida.

Junte-se a nós em nossa missão de cobrir, proteger e assegurar os ativos digitais na cadeia.

Website oficial: https://neptunemutual.com

Blog: https://neptunemutual.com/blog/

Twitter: https://twitter.com/neptunemutual

Forums: https://community.neptunemutual.com/

Telegram: https://t.me/neptunemutual

Discord: https://discord.gg/2qMGTtJtnW

YouTube: https://www.youtube.com/c/NeptuneMutual

LinkedIn:https://www.linkedin.com/company/neptune-mutual

Este artigo foi escrito por Neptune Mutual e traduzido por Rafael Oeda

Você pode ler o artigo original aqui.

Top comments (0)