Reentrancy attack (ataque de reentrada)
O ataque de reentrância foi o responsável por inúmeros hacks, incluindo o da DAO em 2016. Esse tipo de ataque ocorre quando um contrato realiza uma chamada externa para um endereço não confiável sem antes modificar seu próprio estado. O contrato não confiável pode então invocar recursivamente a função de onde foi chamada.
Por exemplo, uma função de saque que envia ETH antes de atualizar o saldo do usuário pode ser explorada se o endereço do destinatário for um contrato com uma função fallback que chame novamente a função de saque.
Exemplo: Vamos ver um exemplo onde um ataque de reentrância é possível.
//Mala práctica
function withdraw() public {
uint amount = balances[msg.sender];
msg.sender.transfer(amount);
balances[msg.sender] = 0;
}
Neste exemplo, usando a função withdraw()
, um usuário pode sacar o saldo de ether que ele havia depositado anteriormente no contrato. A função lê o saldo do usuário, envia o valor correspondente em ether para o endereço que fez a chamada e, por fim, zera o saldo desse endereço.
Como sabemos, é possível criar uma função fallback que receba ether e execute algum código. Assim, um atacante pode implantar um contrato com uma função fallback como a seguinte:
//Código usado por un atacante
address attackedAddress = 0x1234;
function attack() public onlyOwner {
attackedAddress.withdraw();
}
function() external payable {
while(attackedAddress.balance > 0) {
attackedAddress.withdraw();
}
}
No contrato acima, attackedAddress
é o endereço do contrato que possui ether e será atacado. Um atacante pode implantar esse contrato e iniciar o ataque utilizando a função attack()
, que drenará todos os fundos do contrato atacado.
Mitigação:
Usar o padrão de “Retirada” (withdraw pattern) em vez do padrão de “Envio” (send pattern).
Atualizar o estado do contrato antes de realizar chamadas externas.
Limitar o uso de chamadas externas e utilizar bibliotecas de segurança como OpenZeppelin.
Referência:
Last updated