[ethernaut] DoubleEntryPoint

wooz3k.eth·2023년 1월 11일
1
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "openzeppelin-contracts-08/access/Ownable.sol";
import "openzeppelin-contracts-08/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 {
      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) {
        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) {
        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;
    }
}

이 문제는 cryptovalut가 DET token을 잃는 취약점을 찾고 취약점을 방지해주는 forta bot을 만들면 풀리는 문제이다.

접근 방식 및 분석

  • cryptovalut에 token을 빼는 것에 대한 취약점을 찾아야 하기 때문에 관점을 cryptovalut에 DET token을 빼는 공격자 관점으로 바라보자.
  • 가장 먼저 DET token에 delegateTransfer에서 _transfer 함수를 호출하는 것이 보인다.
  • ERC20 구현체를 잘 인지하고 있다면 이 부분에서 감이 올 것이다. _transfer() 함수는 transfer() 함수에서 msg.sender를 검증하고 호출하는 함수이다. 즉, unsafe한 함수인 것이다.
  • 이 부분을 이해했다면 _transfer(address(cryptovalut), ?, address(this).balanceOf(address(cryptovalut))만 실행할 수 있으면 cryptovalut에 토큰을 모두 빼올 수 있을 것이다.
  • delegateTransfer 함수를 중점적으로 분석하면 2개의 modifier가 정의 되어있다. 한번 이를 분석해보자.

onlyDelegateFrom

    modifier onlyDelegateFrom() {
        require(msg.sender == delegatedFrom, "Not legacy contract");
        _;
    }
  • msg.senderdelegateFrom이어야 한다.
  • constructor를 보면 delegatedFrom = legacyToken;를 볼 수 있다. 즉, legacyToken에서 이를 호출하여야 한다.
  • legacyToken에서 delegateTransfer를 어떻게 호출할 수 있는지 살펴보자.

legacyToken - transfer

    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);
        }
    }
  • legacytoken에서 delegateTransfer abi를 발견하였다.
  • delegate가 설정되어 있으면 수행하는데 delegate가 어디로 설정되어있는지 확인해보자.
cast call --rpc-url $G_RPC $legacyToken "delegate()"
  • delegate는 DET token address인 것을 확인할 수 있었다.
  • 즉, legacyToken에서 transfer를 호출하면 DET token에 delegateTransfer() 가 호출되어 onlyDelegateFrom을 통과할 수 있다.
  • 통과 조건을 알았으나 취약점으로 이어지려면 msg.sendercryptovalut가 되어야한다. cryptovalut를 살펴보자.

Cryptovalut

contract CryptoVault {
    address public sweptTokensRecipient;
    IERC20 public underlying;

    constructor(address recipient) {
        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)));
    }
}
  • seepToken 함수를 살펴보면 token.transfer()를 수행한다.
  • underlying에 address가 legacy token으로 set 되어있으면 우리는 취약점으로 이어질 수 있다는 것을 알 수 있다.
  • setUnderlying으로 underlying 을 set할 수 있는데 public으로 되어있다.
  • 우리는 cryptovalut에 모든 DET balance를 뺄 수 있는 취약점을 발견하였다.

fortaNotify

    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");
    }
  • 이제 마지막으로 fortaNotify만 분석하고 detection bot을 구현하면 문제가 풀릴 것이다.
  • notify 함수를 실행하는데 notify에 구현을 보면 우리가 구현해야하는 handleTransaction을 호출한다.
  • botRaisedAlertshandleTransaction으로 취약한 tx가 발생할 경우 경고 횟수를 카운트해주는 변수인 것으로 확인되었다.
  • 즉, 찾은 취약점에서 문제가 발생하는 경우는 msg.sendercryptovalut일 경우에 문제가 발생하기 때문에 이 경우에 botRaisedAlerts를 증가시켜주는 bot만 만들어주면 문제 의도대로 문제가 풀릴 것이다.

Forta Detection Bot

contract DetectionBot 
{
    DoubleEntryPoint public target = DoubleEntryPoint(0x6F1a8fc447b9Ea4FE983Da8199d42815AaBd0eef);
    function handleTransaction(address user, bytes calldata msgData) external
    {
        if(bytes4(msgData) == bytes4(target.delegateTransfer.selector))
        {
            address check;
            (,,check) = abi.decode(msgData[4:], (address, uint256, address));
            
            if(check == target.cryptoVault())
            {
                IForta(msg.sender).raiseAlert(user);
            }
        }
    }
}

이렇게 문제를 해결하였다.

profile
Theori ChainLight Web3 Researcher

0개의 댓글