Preservation
// 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;
}
}
Goal
The goal of this level is to claim ownership of the Preservation instance.
Exploit
In order to finish this CTF we will have to use another smart contract. We can name this contract Attacker.
This contract utilizes a library to store two different times for two different timezones. The constructor creates two instances of the library for each time to be stored.
The first issue that arises is related to the LibraryContract
. If this contract was defined as library instead of contract, we would not have been able to exploit it, because library
keyword prevents the libraries from storing and accessing state variables.
When using delegatecall
we must have in mind that the storage layout is very important, therefore the order of the state variables in the caller and the callee should be consistent and same.
If we take a closer look we can see that when we call setFirstTime
or setSecondTime
functions from Preservation
, we delegatecall to the LibraryContract
.
To be more specific, it means we will call setTime
function inside LibraryContract
and update storedTime
variable to the value passed in setTime
function. When we write to storedTime
variable we are writing to the first storage slot inside LibraryContract
, but since we are using delegatecall
this will update the first storage slot of Preservation
contract and it will update the timeZone1Library
variable of the contract, and we want this variable to hold the value of our Attacker
contract address.
It means we need to figure out how to call setFristTime
with the desired input, and that would be the uint256 value of our Attacker
contract address. To do that we need to cast the address to uint256.
After calling setFirstTime
for the first time, we can call it once again inside our Attacker
contract but since timeZone1Library
is already updated to hold the value of the Attacker
contract address, it will delegatecall to it, calling the setTime
function. This means we need to have a setTime
function in our Attacker
contract. Inside setTime
we will update the value of the owner
variable, and this will update the owner
variable in the Preservation
contract.
One thing to note is that the Attacker
contract should have the same storage layout as the Preservation
contract.
To exploit the contract we need to:
- create Attacker contract
-
create attack(Preservation preservation) external function inside the Attacker contract, passing in the Preservation contract as function argument, and inside this function we will:
- call setFristTime function from the Preservation contract and pass the uint256 value representation of the Attacker contract address as argument, this will update timeZone1Library variable inside Preservation contract to the value of the Attacker contract address
- call setFristTime again, this will call cause the Preservation contract to delegatecall to the Attacker contract by calling setTime inside the Attacker contract
- check if the owner variable inside Preservation is the same as msg.sender, otherwise revert
-
add setTime function, inside this function we will update the owner of the Preservation contract by assigning a value to the owner state variable of the Attacker contract
One question that bugs me is, why this solution won't work if we omit the require statement inside the attack
function!?
Attacker contract code
contract Attacker {
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
function attack(Preservation preservation) external {
preservation.setFirstTime(uint256(uint160(address(this)))); //set the value of timeZone1Library to address(this)
preservation.setFirstTime(uint256(uint160(msg.sender))); // call setFristTime from preservation again to execute setTime inside this contract
require(preservation.owner() == msg.sender, "hack failed");
}
function setTime(uint _time) public {
owner = address(uint160(_time));
}
}