Ethernaut: 13. 게이트키퍼 원

Play the level

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

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

contract GatekeeperOne {
  using SafeMath for uint256;
  address public entrant;

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

  modifier gateTwo() {
    require(gasleft().mod(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(tx.origin), "GatekeeperOne: invalid gateThree part three");
    _;
  }

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


와우 이것은 도전적이었습니다! 수정자로 구현된 3개의 장애물(게이트)을 통과해야 합니다.
  • 심플 msg.sender != tx.origin .
  • 귀여운 gasLeft().mod(8191) == 0 .
  • require 시리즈는 게이트 키가 어떻게 생겼는지 알려줍니다.

  • 게이트 1



    첫 번째 게이트에 대한 해결책은 간단합니다. 계약을 중개인으로 사용하면 됩니다. 이전 퍼즐에서 우리는 msg.sender가 계약일 수 있는 트랜잭션의 즉각적인 발신자임을 배웠습니다. 그러나 tx.origin는 일반적으로 귀하인 거래의 발신자입니다.

    게이트 2



    여기서 트랜잭션에 사용되는 가스를 조정해야 합니다. 우리는 에테르 값을 지정하는 방법과 유사하게 전달할 가스를 지정하여 이를 수행할 수 있습니다: foo{gas: ...}() . 적절한 가스 양을 찾는 것은 까다로운 부분입니다. 그때까지 얼마나 많은 가스를 갖게 될지 정확히 알 수 없기 때문입니다. 우리가 할 수 있는 일은 다음과 같습니다. 좋은 대략적인 가스 값을 찾은 다음 그 주변의 값 범위를 무자비하게 시도합니다. 이를 수행하는 단계는 다음과 같습니다.

      function enterOnce(uint _gas) public {
        bytes memory callbytes = abi.encodeWithSignature(("enter(bytes8)"),key);
        (bool success, ) = target.call{gas: _gas}(callbytes);
        require(success, "failed my boy.");
      } 
    


  • 계약서를 Remix에 복사 붙여넣고 게이트에 들어가려고 합니다(이 시점에서 게이트 1이 통과한다고 가정). 위에 나와 있는 내 공격자 계약서에 이를 위한 작은 유틸리티를 작성했습니다.
  • 매우 운이 좋지 않은 경우 이 게이트에서 거래가 거부됩니다. 괜찮습니다. 디버그하고 싶기 때문입니다!
  • GAS가 백그라운드에서 수행하는 작업인 gasleft() opcode에 도달하기 위해 Remix에서 트랜잭션을 디버그합니다. 여기에서 "Step Details"의 remaining gas 필드를 살펴보겠습니다. 여러 가지 방법으로 쉽게 갈 수 있습니다.
  • "통화가 되돌아간 곳으로 이동하려면 여기를 클릭하십시오."를 클릭합니다. 그런 다음 opcode를 찾을 때까지 조금 뒤로 이동합니다.
  • gasleft()가 있는 줄에 중단점을 놓고 디버거에서 오른쪽 화살표를 클릭하면 해당 opcode에 매우 가깝게 이동합니다.
  • 또 다른 멋진 방법은 실제로 SafeMath 라이브러리 모듈러스 함수 내부로 들어간 다음 디버거에서 로컬 변수를 보는 것입니다. 그 중 하나는 8191이고 다른 하나는 문제의 가스입니다.

  • 제 경우에는 10000 가스를 전달했고 GAS opcode에서 바로 9748이 남았습니다. 그것은 내가 거기에 도착하기 위해 252 가스를 사용했음을 의미합니다. 전체 가스 요구 사항을 충족하기에 충분히 큰 "k"를 위해 8191 * k + 252 가스로 시작하면 괜찮을 것입니다! 문제는 컴파일러 버전과 관련하여 가스 사용량이 변경될 수 있지만 퍼즐에서 ^0.6.0가 위에서 사용되었음을 알 수 있으므로 해당 버전으로 위의 모든 단계를 수행할 것입니다.
  • 가스 후보를 8191 * 5 + 252 = 41207로 마진 32로 설정했습니다. 그런 다음 게이트 키퍼에 느슨하게 했습니다!

  •   function enter(uint _gas, uint _margin) public { 
        bytes memory callbytes = abi.encodeWithSignature(("enter(bytes8)"),key);
        bool success;
        for (uint g = _gas - _margin; g <= _gas + _margin; g++) {
          (success, ) = target.call{gas: g}(callbytes);
          if (success) {
            correctGas = g; // for curiosity
            break;
          }
        }
        require(success, "failed again my boy.");
      }
    


    그것은 성공적이었고 나는 또한 41209로 밝혀진 정확한 가스 양을 기록했습니다.

    게이트 3



    우리는 8바이트 키를 사용하고 있으므로 각 문자가 2바이트(16비트)인 키가 ABCD라고 가정합니다.
  • CD == D so ​​C : 모두 0이어야 합니다.
  • CD != ABCD 그래서 AB는 모두 0이 아니어야 합니다.
  • CD == uint16(tx.origin) : C는 이미 0이며 이제 Dtx.origin의 마지막 16비트가 될 것임을 알고 있습니다.

  • 따라서 내 uint16(tx.origin)C274입니다. AB = 0x 0000 0001를 얻기 위해 _gateKey = 0x 0000 0001 0000 C274를 설정합니다. 또는 &tx.origin 로 비트 단위로 마스킹(0x FFFF FFFF 0000 FFFF )하여 비트 단위 마스킹을 사용할 수 있습니다.

    그게 다야 여러분 :)

    좋은 웹페이지 즐겨찾기