Reentrancy Attack em Smart Contracts Solidity

Cripto

Reentrancy Attack em Smart Contracts Solidity

Luiz Duarte
Escrito por Luiz Duarte em 20/06/2023
Junte-se a mais de 34 mil devs

Entre para minha lista e receba conteúdos exclusivos e com prioridade

Imagine que você é dono de um caixa eletrônico, aqueles ATMs de banco, sabe? Você deve supor que se deixá-lo em um lugar público, com câmeras e muita gente passando, ou em um lugar privado, mas com guardas e seguranças ao redor, será melhor do que deixá-lo sozinho em uma viela qualquer da cidade, certo?

Porque se você optar pela segunda opção, será muito mais difícil mantê-lo seguro, certo? Frequentemente vai ter alguém tentando arrombá-lo, já que ninguém está vendo e tem muito dinheiro dentro dele.

O mesmo acontece com os protocolos DeFi. Sempre que você faz deploy do smart contract de um novo protocolo DeFi na blockchain e ele passa a receber fundos dos usuários, ele se torna uma presa tão fácil quanto um caixa eletrônico carregado “dando sopa” em um beco escuro de uma grande cidade à noite. Vão tentar te atacar, isso é certo.

E dentre os ataques possíveis que os protocolos DeFi vem a ser alvo, tem um que é muito comum. Muito comum de ser feito, muito comum de deixar brecha e, felizmente, muito comum de evitar também. Infelizmente, eventualmente alguém não evita e casos como esses acontecem:

Entre tantos outros.

O nome desse ataque comum é Reentrancy Attack. E antes que eu possa ensiná-lo como se proteger do mesmo, eu preciso lhe ensinar como esse ataque é feito, para fins didáticos. Peço que não use conhecimento para o mal.

Se preferir, você pode assistir ao vídeo abaixo ao invés de ler o artigo.

#1 – Reentrancy Attack Explicado

Imagine que você tem um contrato de protocolo DeFi como abaixo, que permite depósitos e saques de moedas. Olhe e pense se ele parece ou não ok, em especial a função de saque, que é o alvo dos hackers.

Antes de lhe dizer o que tem de errado com essa função, caso não tenha suposto por si só, eu preciso lhe explicar como o ataque é feito. Primeiro, é importante relembrar que na blockchain tanto as carteiras cripto quanto os smart contracts possuem endereços capazes de enviar e receber criptomoedas. Assim, eu posso interagir com o protocolo acima tanto usando uma carteira cripto quanto usando um outro smart contract qualquer, que é o que o hacker irá fazer.

Um hacker querendo fazer um Ataque de Reentrada (Reentrancy Attack), vai criar um smart contract semelhante ao abaixo.

Existem variações mais sofisticadas, mas como meu intuito não é entregar ferramenta de bandido para vocês, o exemplo acima já serve. O hacker vai fazer deploy desse contrato na blockchain, passando o endereço do protocolo que vai ser vítima do ataque. Claro que antes o hacker estuda o contrato do protocolo vítima para ver se ele é suscetível ao ataque, caso contrário só perderá dinheiro no ataque e até mesmo impossibilitará o mesmo, já que precisamos montar a interface IReentrancy de maneira que funcione com o contrato alvo (repare que a assinatura das funções é igual a do contrato Carteira.sol).

O que esse contrato faz após o deploy? O hacker chama a função attack, mandando junto dela um depósito inicial que será transferido para o protocolo. Esse investimento inicial muitas vezes os hackers obtém através de Flash Loans, o que caracteriza muitas vezes na mídia como um Flash Loan Attack. Até aqui nada demais, Flash Loans não são crime e nem mesmo fazer depósitos. O problema é o que vem a seguir.

O código chama na sequência a função de saque do protocolo, na mesma quantidade que acabou de depositar. Se você olhar a referida função no contrato Carteira.sol verá que ela valida o saldo, faz a transferência e depois atualiza o saldo, certo? No entanto, quando ela fizer a transferência ela estará transferindo para o smart contract do atacante e não para uma conta comum. Toda vez que um smart contract recebe um depósito ele dispara uma função nativa chamada receive, que por padrão não faz nada, apenas recebe o saldo mesmo.

No entanto, você pode implementar código na sua receive, para reagir a esses depósitos e aí que entra a “reentrada”. Dentro da receive o hacker coloca código para chamar um novo saque. Isso impede que o fluxo da transação chegue na etapa de atualizar saldo no contrato Carteira.sol, ele vai reentrar na função withdraw e fazer um novo teste e uma nova transferência. Como o saldo ainda não foi atualizado, esse segundo saque também será permitido…

Após a segunda transferência, o contrato receberá fundos, dispara o receive novamente e aí já viu, vai ficar em loop até secar a fonte e depois bastará o hacker transferir o montante final para outra conta e fugir.

Está feito o estrago.

Curso Node.js e MongoDB

#2 – Como se proteger de Reentrancy Attack

Existem duas maneiras principais de se proteger de Ataques de Reentrada:

  • implementando lógica de maneira mais defensiva;
  • bloqueando reentradas em funções críticas;

O primeiro ponto é o ideal e pode ser usado em conjunto do segundo, no entanto nem sempre é trivial.

Em nosso exemplo anterior, é simples de resolver a brecha pois é um protocolo muito simples, mas vale o exercício já que muitas vezes é esse o racional que você deve utilizar. Repare a diferença sutil, porém poderosa, na função de saque:

Desta forma, após a primeira entrada na função, o atacante vai ter seu saldo deduzido ANTES de fazer a transferência. Assim, ao disparar o receive no contrato atacante e tentar fazer novo saque, o require que valida a existência de saldo vai acusar que não tem mais, e vai recusar a transação como um todo, inviabilizando ataques de reentrada.

A outra forma de resolver é bloqueando reentradas definitivamente. Isso tem um custo em gás mais alto mas ao mesmo tempo é uma segurança a mais que você pode colocar na sua função, especialmente se for um código mais complexo e difícil de identificar todas as brechas que podem ser exploradas em um Reentrancy Attack. Falarei sobre esta segundo linha de defesa no próximo tópico.

Curso Beholder
Curso Beholder

#3 – Bloqueando a Reentrada

Um bloqueio de reentrada é basicamente uma implementação que impede de, em uma mesma transação, a mesma função ser chamada duas vezes. Isso bloqueia completamente ataques de reentrada, mas também adiciona mais processamento, armazenamento e consequentemente custo de gás no seu contrato, então tem de valer a pena, ok?

O conceito principal é o uso de semáforos ou flags para controlar a entrada na função, como abaixo, em uma implementação simplista.

Na implementação acima, temos um semáforo indicando se a função já está sendo processada ou não, com o default sendo false. Quando a função de saque é chamada, ela somente executa se já não estiver em execução (nesta transação), disparando erro caso contrário. Se ela não estava processando, ela altera a flag/semáforo (o que impedirá novas reentradas), faz o seu processamento e depois libera a entrada à função novamente, encerrando a transação.

Caso você não se sinta confortável com essa reentrada ou esteja buscando ainda mais profissional e largamente usado no mercado, pode usar a implementação de bloqueio de reentrada da OpenZeppelin, chamada Reentrancy Guard, cujo uso é bem simples.

O import você deve adicionar no topo do contrato e ele lhe permitirá usar o function modifier nonReentrant nas funções que desejar ter o bloqueio de reentrada. Como dito antes, isso adiciona custos adicionais em gás, por isso não é recomendado sair adicionando em funções que não estão expostas a ataques de reentrada.

E com isso espero ter te ajudado a entender mais sobre este famoso ataque e de como se proteger dele. Outro ataque importante de ser estudado é o Gas Griefing, que explico neste artigo.

Até a próxima!

Olá, tudo bem?

O que você achou deste conteúdo? Conte nos comentários.

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *