DEV Community

Erhan Tezcan
Erhan Tezcan

Posted on

Ethernaut: 13. Gatekeeper One

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Wow this was challenging! We must pass 3 obstacles (gates) that are implemented as modifiers:

  1. Simple msg.sender != tx.origin.
  2. A cute gasLeft().mod(8191) == 0.
  3. A series of require's telling us what the gate key must look like.

Gate 1

Solution to the first gate is trivial, just use a contract as a middleman. From previous puzzles we have learned that msg.sender is the immediate sender of a transaction, which may be a contract; however, tx.origin is the originator of the transaction which is usually you.

Gate 2

Here we need to adjust the gas used in the transaction. We can do this by specifying the gas to be forwarded similar to how we specify ether value: foo{gas: ...}(). To find the proper gas amount is the tricky part, because we don't know exactly how much gas we will have by then. Here is what we can do: we will find a good approximate gas value, and then brutely try a range of values around it. The steps to do that is as follows:

  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.");
  } 
Enter fullscreen mode Exit fullscreen mode
  1. Copy paste the contract in Remix, and try to enter the gate (assuming that gate 1 is passing at this point). I wrote a small utility for this in my attacker contract, shown above.

  2. Unless you are extremely lucky, the transaction will be rejected by this gate. That is ok, because we want to debug it!

  3. Debug the transaction in Remix to get to the GAS opcode, which is what gasleft() is doing in the background. There, we will look at the remaining gas field in "Step Details". You can easily get there in several ways:

    1. Clicking "Click here to jump where the call reverted." and then going backward a bit until you find the opcode.
    2. Putting a breakpoint to the line with gasleft() and clicking right arrow at the debugger, which will go very close to that opcode.
    3. Another cool way is to actually get inside the SafeMath libraries modulus function, and then look at the local variables in the debugger. One of them will be 8191, the other will be the gas in question.
  4. In my case, I had forwarded 10000 gas and right at the GAS opcode I had 9748 left. That means I used 252 gas to get there. If I start with 8191 * k + 252 gas for some large enough "k" to meet the overall gas requirement, I should be okay! The thing is, gas usage can change with respect to the compiler version, but in the puzzle we see that ^0.6.0 is used above, so we will do all the steps above with that version.

  5. I set the gas candidate as 8191 * 5 + 252 = 41207 with a margin of 32. Then I let it loose on the gate keeper!

  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.");
  }
Enter fullscreen mode Exit fullscreen mode

It was successful, and I also kept record of the correct gas amount which turned out to be 41209.

Gate 3

We are using an 8-byte key, so suppose the key is ABCD where each letter is 2 bytes (16 bits).

  1. CD == D so C: must be all zeros.
  2. CD != ABCD so AB must not be all zeros.
  3. CD == uint16(tx.origin): C is already zeros, and now we know that D will be the last 16-bits of tx.origin.

So, my uint16(tx.origin) is C274; and I will just set AB = 0x 0000 0001 to get _gateKey = 0x 0000 0001 0000 C274. Alternatively, you can use bitwise masking by bitwise-and'ing (&) your tx.origin with 0x FFFF FFFF 0000 FFFF.

That is all folks :)

Top comments (0)