Ethernaut 题库闯关 #24 - Double Entry Point

Ethernaut题库闯关连载的第24篇

今天这篇是Ethernaut 题库闯关连载的第24篇,难度等级: 有点难。

这是系列的最后一篇,如果你跟随这个专栏,每一篇都认真思考, 相信你对 Solidity 安全有全新的认识。

挑战 24 - Double Entry Point(双重进入点)

本关有一个具有特殊功能的 CryptoVault,即 sweepToken功能。这是一个常用的功能,用于检索卡在合约中的代币并转移。CryptoVault的操作有一个underlying代币不能被转移,因为它是CryptoVault的重要核心逻辑组件。任何其他代币都可以被转移。

underlyingDoubleEntryPoint合约定义中实现的DET代币的实例,CryptoVault持有100个。此外,CryptoVault还持有100个LegacyToken LGT

在这一关中,我们需要找出CryptoVault的错误所在,并保护它不被耗尽代币。

该合约的特点是Forta合约,任何用户都可以注册自己的检测机器人合约。Forta是一个去中心化的、基于社区的监测网络,以尽快检测DeFi、NFT、治理、跨链桥和其他Web3系统上的威胁和异常情况。我们的任务是实现一个 检测机器人并在 Forta合约中注册。该机器人的实现将需要提出正确的警报,以防止潜在的攻击或错误的利用。

合约源码如下:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

interface DelegateERC20 {
  function delegateTransfer(address to, uint256 value, address origSender) external returns (bool);
}

interface IDetectionBot {
    function handleTransaction(address user, bytes calldata msgData) external;
}

interface IForta {
    function setDetectionBot(address detectionBotAddress) external;
    function notify(address user, bytes calldata msgData) external;
    function raiseAlert(address user) external;
}

contract Forta is IForta {
  mapping(address => IDetectionBot) public usersDetectionBots;
  mapping(address => uint256) public botRaisedAlerts;

  function setDetectionBot(address detectionBotAddress) external override {
      require(address(usersDetectionBots[msg.sender]) == address(0), "DetectionBot already set");
      usersDetectionBots[msg.sender] = IDetectionBot(detectionBotAddress);
  }

  function notify(address user, bytes calldata msgData) external override {
    if(address(usersDetectionBots[user]) == address(0)) return;
    try usersDetectionBots[user].handleTransaction(user, msgData) {
        return;
    } catch {}
  }

  function raiseAlert(address user) external override {
      if(address(usersDetectionBots[user]) != msg.sender) return;
      botRaisedAlerts[msg.sender] += 1;
  } 
}

contract CryptoVault {
    address public sweptTokensRecipient;
    IERC20 public underlying;

    constructor(address recipient) public {
        sweptTokensRecipient = recipient;
    }

    function setUnderlying(address latestToken) public {
        require(address(underlying) == address(0), "Already set");
        underlying = IERC20(latestToken);
    }

    /*
    ...
    */

    function sweepToken(IERC20 token) public {
        require(token != underlying, "Can't transfer underlying token");
        token.transfer(sweptTokensRecipient, token.balanceOf(address(this)));
    }
}

contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable {
    DelegateERC20 public delegate;

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    function delegateToNewContract(DelegateERC20 newContract) public onlyOwner {
        delegate = newContract;
    }

    function transfer(address to, uint256 value) public override returns (bool) {
        if (address(delegate) == address(0)) {
            return super.transfer(to, value);
        } else {
            return delegate.delegateTransfer(to, value, msg.sender);
        }
    }
}

contract DoubleEntryPoint is ERC20("DoubleEntryPointToken", "DET"), DelegateERC20, Ownable {
    address public cryptoVault;
    address public player;
    address public delegatedFrom;
    Forta public forta;

    constructor(address legacyToken, address vaultAddress, address fortaAddress, address playerAddress) public {
        delegatedFrom = legacyToken;
        forta = Forta(fortaAddress);
        player = playerAddress;
        cryptoVault = vaultAddress;
        _mint(cryptoVault, 100 ether);
    }

    modifier onlyDelegateFrom() {
        require(msg.sender == delegatedFrom, "Not legacy contract");
        _;
    }

    modifier fortaNotify() {
        address detectionBot = address(forta.usersDetectionBots(player));

        // Cache old number of bot alerts
        uint256 previousValue = forta.botRaisedAlerts(detectionBot);

        // Notify Forta
        forta.notify(player, msg.data);

        // Continue execution
        _;

        // Check if alarms have been raised
        if(forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
    }

    function delegateTransfer(
        address to,
        uint256 value,
        address origSender
    ) public override onlyDelegateFrom fortaNotify returns (bool) {
        _transfer(origSender, to, value);
        return true;
    }
}

通过这一关,我们需要利用到双重进入点。

研究合约

这个挑战似乎是OpenZeppelin和Forta(一个实时安全和运营监控)之间的一个联合。在我看来,这个挑战试图向我们解释应该如何整合Forta系统来监控合约。让我们看看它是如何进行的。

从挑战书的描述中,我们有两个代币:LegacyToken,顾名思义是一个已经被 废弃的代币(在现实生活中会发生这种情况吗),取而代之的是一个新的代币 DoubleEntryPoint

我们还有一个名为 CryptoVault的金库,它有一些功能(与挑战的范围无关),并提供一个名为 sweepToken(IERC20 token)的实用方法,允许任何人向 sweptTokensRecipient(在部署时定义的地址)sweep(转账)被意外发送到金库的代币。该函数内部的唯一检查是,你不能转移Vault的underlying代币。

在部署时,我们以这种配置开始:

  • CryptoVault持有100 DET (DoubleEntryToken)
  • CryptoVault持有100 LGT (LegacyToken)

我们的目标是创建一个Forta DetectionBot,监测合约并防止外部攻击者耗尽CryptoVault,使其耗尽不应耗尽的代币。

让我们回顾一下每个合约,看看是否能找到一些攻击的载体:

LegacyToken.sol

contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable {
    DelegateERC20 public delegate;

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    function delegateToNewContract(DelegateERC20 newContract) public onlyOwner {
        delegate = newContract;
    }

    function transfer(address to, uint256 value) public override returns (bool) {
        if (address(delegate) == address(0)) {
            return super.transfer(to, value);
        } else {
            return delegate.delegateTransfer(to, value, msg.sender);
        }
    }
}

这是一个继承自OwnableERC20代币。合约的 所有者可以 铸造新的代币,并通过调用 delegateToNewContract更新 delegate变量的值。

奇怪的部分是在transfer函数里面,它已经覆盖了ERC20标准提供的默认函数。

如果没有定义委托(address(delegate) == address(0)),合约就使用ERC20标准的默认逻辑;否则就执行return delegate.delegateTransfer(to, value, msg.sender)

在此案例中,delegate就是DoubleEntryPoint合约本身。这意味着什么呢?当你在LegacyToken上执行转移时,实际上它是将操作转发给执行DoubleEntryPoint.delegateTransfer。让我们切换到的DET token代码,看看发生了什么事

DoubleEntryPoint.sol...

剩余50%的内容订阅专栏后可查看

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Ethernaut CTF
Ethernaut CTF
信奉 CODE IS LAW.