WEB3DEV

Cover image for Ataques de Reentrância e o DAO Hack
Banana Labs
Banana Labs

Posted on

Ataques de Reentrância e o DAO Hack

Ataques de reentrância, como o usado no hack DAO, são possibilitados por vulnerabilidades na maneira como estruturamos o código Solidity.

Neste blog, exploraremos um dos hacks Solidity mais infames de todos os tempos, que aconteceu nos anos de formação do desenvolvimento de contratos inteligentes Ethereum. Este ataque usou a exploração de reentrância para comprometer uma DAO (organização autônoma descentralizada) chamada The DAO. O hack usou um exploit comumente chamado de ataque de reentrância.

Para quem se destina o artigo?

Este blog pressupõe que você entenda:

  • O básico da tecnologia blockchain, especificamente Ethereum.
  • A Ethereum Virtual Machine (EVM) é executada em nós Ethereum e é uma máquina de estado sincronizada e descentralizada.
  • No contexto do Ethereum, um contrato inteligente refere-se ao código de software escrito na linguagem Solidity e executado no EVM.
  • No contexto do Ethereum, a palavra “conta” refere-se a uma entidade que pode ter um saldo de ether, envia transações na rede Ethereum e é de dois tipos – controlado pelo usuário ou um contrato inteligente implantado.

Você não precisa de nenhum conhecimento de Solidity, pois os exemplos de código são bastante leves. No entanto, uma compreensão básica de qualquer linguagem de programação (de tipo estático ou dinâmico) é útil.

Se desejar, você também pode codificar junto com um tutorial de ataque de reentrância aqui.

Uma Breve História Do DAO Hack

Em 2015, a nascente comunidade Ethereum estava começando a falar sobre DAOs – organizações autônomas descentralizadas. A ideia por trás dessas comunidades baseadas em blockchain era que elas poderiam coordenar o esforço humano por meio da execução de código verificável (especificamente, contratos inteligentes executados na blockchain Ethereum) e por meio da tomada de decisão descentralizada nos protocolos da comunidade. Em 2016, quando a rede principal Ethereum tinha cerca de um ano, uma DAO chamada “The DAO” foi criada. Era um fundo de investimento descentralizado e controlado pela comunidade. Ela levantou US$ 150 milhões em ether (cerca de 3,54 milhões de ETH) vendendo seu próprio token comunitário. As pessoas compraram o token comunitário do DAO depositando ETH, e esse ETH se tornou os fundos de investimento que o DAO investiria em nome de sua comunidade de investidores detentores de token.

Como era tão cedo na evolução do Ethereum, contratos inteligentes e DAOs, havia muito entusiasmo sobre essas maneiras sem precedentes de organizar e coordenar a atividade humana.

Infelizmente, menos de 3 meses após o lançamento do DAO, ele foi atacado por um hacker “blackhat”. Nas semanas seguintes, o hacker começou a drenar a maior parte dos US$ 150 milhões em ETH do contrato inteligente do DAO. O hacker usou o que veio a ser chamado de ataque de “reentrância”. O nome é descritivo, e em breve vamos mergulhar mais na estrutura e na técnica desse ataque. Mas, como você pode imaginar, isso foi uma violação muito séria do DAO, uma violação da confiança dos investidores e um golpe significativo na credibilidade do Ethereum.

Também criou brechas ideológicas muito profundas. Enquanto participantes e críticos da indústria observavam os fundos sendo drenados da DAO, houve um amplo debate sobre a melhor maneira de responder. Em uma extremidade do espectro ideológico estava a visão de que a promessa de blockchains criptograficamente habilitadas era sua imutabilidade e resistência à adulteração. A intervenção, mesmo pelas razões certas, ainda estava adulterando. Um sistema realmente confiável e resistente a adulterações não exigiria intervenção, mesmo que as consequências fossem sérias – esse é o preço a ser pago pela resistência descentralizada à adulteração.

Por outro lado, as economias das pessoas estavam sendo roubadas em câmera lenta e os danos à confiança e otimismo do público sobre a tecnologia blockchain exigia uma intervenção, mesmo que fosse apenas para proteger as pessoas de perderem suas economias de vida, e a obrigação ética de impedir o roubo .

À medida que esses debates continuavam, um grupo de hackers “whitehat” foi montado para atuar como uma força de contra-ataque. Eles estavam no campo de intervenção e usaram o mesmo hack – o exploit de reentrância – para tentar drenar a DAO mais rápido que o invasor. A ideia era resgatar os fundos para que pudessem ser devolvidos aos investidores. E muitos reembolsos foram feitos aos membros da DAO, embora muitos dos investidores tenham saído do protocolo por meio de “pods de fuga” que lhes permitiam extrair seu investimento na saída.

Enquanto isso, como o hacker ainda estava drenando milhões, a equipe principal do Ethereum se deparou com uma decisão muito desafiadora. Uma maneira de frustrar o hacker seria bifurcar a blockchain Ethereum. Isso seria como mudar a história, onde uma realidade alternativa se desenrolava. Neste exemplo, ao bifurcar o Ethereum, o novo fork funcionaria como se o hack nunca tivesse acontecido, e o ETH do hacker seria válido apenas no fork legado da rede. Se os usuários adotassem o novo fork e abandonassem o antigo, o ETH do hacker valeria muito pouco. Esse fork invalidaria os blocos históricos que armazenavam as transações do ataque do hacker. Mas esse passo extremo foi completamente contrário aos princípios que sustentam o Ethereum – esse tipo de intervenção é o tipo de ação centralizada e “unilateral” que o Ethereum deveria eliminar.

Aqueles que votaram a favor do fork estavam apoiando um mundo onde haveria duas blockchains Ethereum paralelas. Essa votação venceu com 85% dos votos, e o fork aconteceu (apesar de alguns mineradores resistirem, pois não havia defeito real no protocolo Ethereum). É por isso que agora existem duas blockchains Ethereum – Ethereum Classic e a rede Ethereum que conhecemos hoje. Ambas têm seus tokens ETH nativos, e esses tokens têm preços muito diferentes no mercado. Você pode ver o anúncio da Ethereum Foundation sobre o fork aqui.

A DAO foi historicamente significativo, e então o hack e as decisões resultantes também se tornaram históricos.

Mas como exatamente o hack funcionou? Vamos explorar.

O Que É Um Ataque De Reentrância Em Solidity?

As aplicações executadas na blockchain Ethereum são chamadas de “contratos inteligentes” (embora não haja efeito legal para eles). Contratos inteligentes são pedaços de código, mais comumente escritos em uma linguagem chamada Solidity, que são executados no blockchain e podem interagir com contas de usuários externos e outros contratos inteligentes na rede Ethereum. Essa interação livre e a composição de contratos inteligentes estão no centro de seu design. A movimentação de pagamentos entre contas também está no centro da filosofia. Esses princípios são refletidos na maneira como a linguagem Solidity é executada na Máquina Virtual Ethereum.

O ataque de reentrância explora a maneira como algo chamado de funções de “fallback” funciona. As funções de fallback são construções especiais no Solidity que são acionadas em situações específicas. Os recursos das funções de fallback são:

  1. Elas são sem nome.
  2. Elas são chamadas externamente (ou seja, elas não podem ser chamadas de dentro do contrato em que estão escritas).
  3. Pode haver zero ou uma função de fallback por contrato – não mais.
  4. Elas são acionadas automaticamente quando outro contrato chama uma função no contrato inteligente anexo do fallback, mas o nome da função chamada não corresponde ou existe.
  5. Elas também podem ser acionadas se o ETH for enviado para o contrato de inclusão do fallback, não houver “calldata” (um local de dados como memória ou armazenamento) e não houver nenhuma função receive() declarada - nesta circunstância, um fallback deve ser marcado pagável para que ele acione e receba o ETH.
  6. As funções de fallback podem incluir lógica arbitrária nelas.

São os quinto e sexto recursos das funções de fallback que são explorados pelo hack de reentrância. O hack também depende de uma certa ordem de operações no contrato da vítima. Então vamos explorar como isso acontece.

Na ilustração abaixo, as caixas vermelha e verde são contratos inteligentes e, apenas para torná-lo interessante, apresentei o ataque de reentrância no cenário da DAO e seu famoso hack. Esta é uma versão altamente simplificada, reduzida ao essencial para entender apenas a reentrância, e o código abaixo não é semelhante ao código da DAO.

Em nossa ilustração, o contrato inteligente da DAO mantém uma variável de estado chamada Saldos que rastreia o investimento de cada investidor na DAO. Isso é separado do saldo ETH do contrato inteligente, que não é armazenado em uma variável de estado.

O hacker implanta um contrato inteligente que atua como “investidor” e esse contrato deposita algum ETH na DAO. Isso dá direito ao hacker de chamar posteriormente a função de withdraw() no contrato inteligente da DAO. Quando a função de withdraw() é eventualmente chamada, o contrato da DAO envia ETH para o hacker. Mas o contrato inteligente do hacker intencionalmente não possui uma função receive(), portanto, quando recebe ETH da solicitação de retirada, a função de fallback do hacker é acionada. Essa função de fallback poderia estar vazia e ainda receber o ETH, mas, em vez disso, contém algum código malicioso.

Este código, imediatamente após a execução, chama a função de withdraw() do contrato inteligente da DAO novamente. Isso desencadeia um loop de chamadas porque neste ponto a primeira chamada para withdraw() ainda está em execução. Ele só terminará de ser executado quando a função de fallback do contrato do hacker terminar, mas, em vez disso, chamou novamente o método de withdraw(), que inicia um ciclo encadeado de chamadas entre o contrato do hacker e o contrato inteligente da DAO.

reentrancy attack

Cada vez que withdraw() é chamad, o contrato inteligente da DAO tenta enviar ao hacker uma quantidade de ETH equivalente ao depósito do hacker. Mas, crucialmente, ele não atualiza o saldo da conta do hacker até que a transação de envio de ETH seja concluída. Mas a transação de envio de ETH não pode terminar até que a função de fallback do hacker termine de ser executada. Portanto, o contrato da DAO continua enviando mais e mais ETH para o hacker sem diminuir o saldo do hacker - drenando os fundos da DAO.

Isso se tornará um pouco mais fácil de seguir no passo a passo do código abaixo.

Exemplo De Código Do Ataque De Reentrância

Vamos começar com o código da DAO, no qual uma ordem específica de operações cria uma vulnerabilidade para um ataque de reentrância.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.10;

 contract Dao {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        require(msg.value >= 1 ether, "Deposits must be no less than 1 Ether");
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
        // Verifica o saldo do usuário
        require(
            balances[msg.sender] >= 1 ether,
            "Insufficient funds.  Cannot withdraw"
        );
        uint256 bal = balances[msg.sender];

        // Saque do saldo do usuário
        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent, "Failed to withdraw sender's balance");

        // Atualização do saldo do usuário.
        balances[msg.sender] = 0;
    }

    function daoBalance() public view returns (uint256) {
        return address(this).balance;
    }
}
Enter fullscreen mode Exit fullscreen mode

Observe o seguinte:

  • O contrato inteligente mantém um mapeamento de endereços de investidores e saldos de ETH. O ETH investido é mantido no próprio saldo do contrato, que é diferente da variável de estado dos saldos.
  • deposit() requer uma contribuição mínima de 1 ETH e, uma vez recebida uma contribuição, aumenta o saldo do investidor.
  • A função withdraw() envia o ETH retirado para o investidor (usando msg.sender.call) antes de redefinir seu saldo para zero. A transação de envio não termina de ser executada até que a função de fallback do hacker termine de ser executada, portanto, o saldo do hacker não é definido como zero até que a função de fallback seja concluída. Esta é a principal vulnerabilidade no contrato da DAO.

Agora vamos examinar o contrato inteligente do hacker, que executou o exploit.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.10;

interface IDao {
    function withdraw() external ;
    function deposit()external  payable;
 }

contract Hacker{
    IDao dao; 

    constructor(address _dao){
        dao = IDao(_dao);
    }

    function attack() public payable {
        // Adição de pelo menos 1 Ether na Dao.
        require(msg.value >= 1 ether, "Need at least 1 ether to commence attack.");
        dao.deposit{value: msg.value}();

        // Saque à partir da Dao
        dao.withdraw();
    }

    fallback() external payable{
        if(address(dao).balance >= 1 ether){
            dao.withdraw();
        }
    }

    function getBalance()public view returns (uint){
        return address(this).balance;
    }
}
Enter fullscreen mode Exit fullscreen mode

Coisas para observar aqui:

  • A função attack() deposita o “investimento” do hacker na DAO e então inicia o ataque chamando a função withdraw() do contrato DAO, que desconstruímos no parágrafo anterior.
  • A função de fallback inclui o código malicioso. Ele verifica se o contrato da DAO ainda tem algum ETH e, em seguida, chama a função withdraw() do contrato da DAO. Vimos no parágrafo anterior que a função de withdraw() do contrato DAO não atualiza o saldo da conta do hacker enquanto a transação de envio de ETH ainda estiver em execução. E essa transação continua sendo executada porque a função de fallback do hacker continua chamando o método gets(). Isso drena o saldo ETH do contrato DAO sem atualizar a variável de estado dos saldos.
  • Uma vez que o saldo de ETH do contrato DAO é drenado, a função fallback() não executará mais a função de withdraw(), e isso (finalmente!) concluirá a execução da função fallback(), que concluirá a transação de envio de ETH. Só então o saldo da conta do hacker será zerado, quando a DAO não terá mais ETH.

Corrigindo A Vulnerabilidade De Reentrância

Existem algumas maneiras de corrigir uma vulnerabilidade de reentrância, mas em nosso exemplo, a correção mais simples é alterar a ordem das operações na função de withdraw() da DAO para que o saldo do chamador seja redefinido para 0 antes que o contrato da DAO envie seu ether usando a função de chamada de baixo nível. Ficaria assim:

Contract Dao {


     function withdraw() public {
        // Avaliação do saldo do usuário
        require(
            balances[msg.sender] >= 1 ether,
            "Insufficient funds.  Cannot withdraw"
        );
        uint256 bal = balances[msg.sender];

        //Atualização do saldo do usuário.
        balances[msg.sender] = 0;

        // Saque do saldo do usuário
        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent, "Failed to withdraw sender's balance");

        // Atualização do saldo do usuário.
        balances[msg.sender] = 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

Dessa forma, quando a função call() de baixo nível aciona a função fallback() do contrato do hacker, e essa função tenta reentrar na função withdraw(), o saldo do hacker é zero no ponto de reentrada e o método require() será avaliado como falso, revertendo a transação ali mesmo. Isso fará com que a chamada original para call() se mova para o return e, como falhou, o valor de sent será falso, o que fará com que a próxima linha (require(sent, “Failed to withdraw sender’s balance”);) se reverta.

O hacker iria retirar seu depósito e nada mais.

Outra opção seria o contrato DAO usar modificadores de função para “bloquear” a função withdraw() enquanto ela ainda estiver em execução, de modo que qualquer reentrância seja bloqueada por este bloqueio. Conseguiríamos isso adicionando essas linhas ao contrato da DAO.

Contract Dao {
   bool internal locked;

   modifier noReentrancy() {
        require(!locked, "No reentrancy");
        locked = true;
        _;
        locked = false;
    }

 //……
    function withdraw() public noReentrancy { 

    // A lógica da saque vem aqui...
    }
}
Enter fullscreen mode Exit fullscreen mode

Esse protetor de reentrância usa o que é conhecido como padrão de sinalizador mutex (mutuamente exclusivo) para proteger a função de withdraw() impedindo que ela seja invocada enquanto uma invocação anterior não foi concluída. Assim quando a função de fallback() do contrato do hacker tenta reentrar no contrato DAO através da função de withdraw(), o modificador da função será acionado e a função require() vai causar a reversão retornando a mensagem “No reentrancy”.

Conclusão

Obviamente, este é um passo a passo altamente simplificado que explica o conceito de exploits de reentrância usando código de exemplo em vez de exemplos de códigos em produção. E embora eu tenha usado o hack DAO como pano de fundo para explicar o hack de reentrância, o código real da DAO era muito diferente. No entanto, o hack DAO foi baseado nos princípios de “reentrância” porque o hacker retirou recursos recursivamente sem que seu saldo fosse atualizado. Você pode examinar o repositório GitHub da DAO e ver as tentativas de correção da lógica de atualização de saldo no histórico de commits.

Se você é um desenvolvedor e deseja proteger seus contratos inteligentes, dApp ou protocolo, considere usar o Chainlink em seus aplicativos de contrato inteligente. Para obter mais recursos de aprendizado e referência, confira o hub educacional de blockchain, a documentação do desenvolvedor ou entre em contato com um especialista. Você também pode mergulhar direto em conectar seus contratos inteligentes a dados do mundo real por meio de oráculos descentralizados.


Esse artigo é uma tradução de Zubin Pratap feita por @bananlabs. Você pode encontrar o artigo original aqui

Oldest comments (0)