Ethernaut Lvl 16 Preservation Walkthrough: How to inject malicious contracts with delegatecall
This is a in-depth series around Zeppelin team’s smart contract security puzzles. I’ll give you the direct resources and key concepts you’ll need to solve the puzzles 100% on your own.
This levels requires you to combine knowledge from levels 6 and 12 to claim ownership of the contract.
Refresher on delegatecall
Recall from level 6:
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.
Hint: if Contract A invokes Contract B, and you can control Contract B, you can easily mutate the state of Contract A.
Refresher on Contract Storage
Recall from level 12:
- Ethereum allots 32-byte sized storage slots to store state. Slots start at index
0
and sequentially go up to 2²⁵⁶ slots. - Basic datatypes are laid out contiguously in storage starting from position
0,
then1
, until2²⁵⁶-1
. - If the combined size of sequentially declared data is less than 32 bytes, then the sequential data points are packed into a single storage slot to optimize space and gas.
Hint: If you can match up storage data locations between Contract A and Contract B, you can precisely manipulate the desired variables in Contract A.
Detailed Walkthrough
First, notice that LibraryContract
modifies the state at slot 0. Moreover, it allows external parties to replace storedTime
with any other 32 byte variable.
Hint: What if we replaced slot 0 with a contract address?
uint storedTime;
function setTime(uint _time) public {
storedTime = _time;
}
Second, notice you can invoke setTime
from Preservation.sol through the delegatecall in setFirstTime
.
This means that you can modify slot 0 in Preservation.sol, i.e. change the address of timeZone1Library
!
Hint: What if you can repoint
timeZone1Libary
to a malicious contract that modifies other state variables in Preservation.sol?
Part I — Create a malicious contract
In this first part, let’s create the malicious contract that timeZone1Libary
will repoint toward.
- Notice that Preservation.sol stores
owner
address at slot 2. Let’s create a malicious contract that has the same storage layout:
contract BadLibraryContract {
address public timeZone1Library; // SLOT 0
address public timeZone2Library; // SLOT 1
address public owner; // SLOT 2
uint storedTime; // SLOT 3
...
2. Create a setTime
function inside BadContract which updates slot 2 with your wallet address.
function setTime(uint _time) public {
owner = msg.sender;
}
Note: it is important to use the same function name as in LibraryContract because Preservation.sol invokes functions by name:
bytes4(keccak256(“setTime(uint256)”));
3. Deploy BadLibraryContract to Ropsten in Remix, and save its instance address. Then, calculate the uint(address)
to derive the input variable for:
setFirstTime(uint _timeStamp)
Part II — Update timeZone1Library to the malicious contract
- In Remix, access your level instance.
- Invoke
setFirstTime
with the converteduint(address)
. - Double check that
timeZone1Library
is now your malicious contract address.
Part III — Gain Ownership
- For the second time, invoke
setFirstTime
, with an arbitrary input uint variable. This time, you are actually invokingowner = msg.sender
. - Double check that owner in Preservation.sol is now your wallet address.
Key Security Takeaways
- Ideally, libraries should not store state.
- When creating libraries, use
library
, notcontract
, to ensure libraries will not modify caller storage data when caller usesdelegatecall
. - Use higher level function calls to inherit from libraries, especially when you i) don’t need to change contract storage and ii) do not care about gas control.