Ethernaut - Walkthrough for Noobs - 14 - Gatekeeper Two

Ethernaut - Walkthrough for Noobs - 14 - Gatekeeper Two

ยท

4 min read

This is similar to the previous challenge Gatekeeper One in the sense that, it also has three modifiers/gates that you need to pass. You will learn about extcodesize() and the bitwise XOR operator.

This gatekeeper introduces a few new challenges. Register as an entrant to pass this level.

We need to pass all three gates.

Gate 1

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

This should be pretty similar to Gatekeeper One and Telephone. A word of advice or caution, if you are at Level 14 and you can't make sense of this. I suggest you do each and every level from the ground up again.

If we create an intermediary contract to call the GatekeeperTwo contract, we would pass this modifier.

Gate 2

modifier gateTwo() {
    uint x;
    assembly { x := extcodesize(caller()) }
    require(x == 0);
    _;
  }

We can leave solidity and shift to inline assembly, for fine-grained control. This low-level language is called Yul.

An inline assembly block is marked by assembly { ... }, where the code inside the curly braces is code in the Yul language.

extcodesize is an opcode that returns the size of the code on that address. If the size is larger than zero that address should be a contract.

caller() this is the address of the last call sender (except delegatecall() as seen in a previous challenge)

Here, x is used to store the size of the code on the caller's address. That is the contract we will be using to interact with GatekeeperTwo.

But, the problem is, this x is checked to be zero in the next line, which basically states that the caller should be an EOA. But again, the first gate ensures that we must interact with GatekeeperTwo with another contract.

How can both things be true at the same time?

For this to be true, a smart contract must have zero code, but how. Turns out there is a special case when this is true. There are two different bytecodes on Ethereum.

  1. Creation Bytecode: this is required by Ethereum to create the contract. it includes the constructor logic, and constructor parameters of the smart contract, this generates runtime bytecode.

  2. Runtime Bytecode: this describes the code in a smart contract and each and every function it executes. extcodesize will show the size of this.

So during contract initialization, it does not have any runtime bytecode, since the size of the contract at that point in time is zero. I think you understand what the idea here is. If we call the enter() function in the GatekeeperTwo contract from the constructor of our attack contract. We should be good to go.

Note: Read more about runtime and creation bytecodes, pick your poison: Easy Mode v/s Nerd Mode

Gate 3

modifier gateThree(bytes8 _gateKey) { //takes a 8 byte input
    require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
    _;
  }

Okay, this should also be fairly easy to understand. Let's break this down:

  1. (uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) : msg.sender is packed and encoded --> returns bytes --> which is hashed using keccak256 --> low ordered 8 are taken from the hash ---> which is casted to a uint64.

  2. ^ : This stands for bitwise operator XOR. If bits at each position are the same it will output 0, else the output will be 1. Read more.

    So if, a ^ b == type(uint64).max means that a must be the exact inverse of b.

    Also, we know that if A XOR B = C then A XOR C == B . This is not intuitive but logical. Think why this is true.

type(uint64).max : gives the maximum number that can be in uint64.

To pass the third gate, this should do the job.

bytes8 myKey = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ (type(uint64).max));

//if A XOR B == C  ----> A XOR C == B

Solution

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

interface IGateKeeperTwo {
    function enter(bytes8 _gateKey) external returns (bool); //creating an interface
}

contract IWantIn {
    constructor() public {
        IGateKeeperTwo gatekeepertwo = IGateKeeperTwo('instance address without quotes'); //creates an instance of the interface. 
        bytes8 myKey = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max); //creates a Key that passes Gate 3. 
        gatekeepertwo.enter(myKey); //inputs that key in the enter function. 

    }
}
  1. Open Remix, copy the code, compile, and deploy the contract.

  2. Check if you are the entrant now in the console.

  3. Submit Instance.

GateKeeperTwo๐Ÿ’‚๐Ÿ’‚

ย