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.

Nicole Zhu
Coinmonks
Published in
4 min readSep 17, 2018

--

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, then 1, until 2²⁵⁶-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.

  1. 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

  1. In Remix, access your level instance.
  2. Invoke setFirstTime with the converted uint(address).
  3. Double check that timeZone1Library is now your malicious contract address.

Part III — Gain Ownership

  1. For the second time, invoke setFirstTime, with an arbitrary input uint variable. This time, you are actually invoking owner = msg.sender.
  2. 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, not contract, to ensure libraries will not modify caller storage data when caller uses delegatecall.
  • 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.

More Levels

Get Best Software Deals Directly In Your Inbox

--

--