We are presented with a simple contract “Alien Codex” that inherits from Ownable base contract. Our task is to become the owner of Alien Codex i.e. we should be able to manipulate the Ownable._owner private state variable and store an address of our choosing. Seeing how simple the Alien Codex contract is and the fact that all functions in the contract are manipulating array gave me the hint to focus there first.
I used Remix for this level because it provides an easy way to see the values in the storage slots. All the functions in the Alien Codex contract use the contacted() modifier for some reason so running make_contact() naturally becomes the first step. Lets see how the state looks after I run make_contact().
The storage has only the Slot#0 filled in with the value 0x0000000000000000000000015b38da6a701c568545dcfcb03fcb875f56beddc4. Where 01 (or true) is the value for “contact” Boolean variable in Alien Codex and 5b38da6a701c568545dcfcb03fcb875f56beddc4 is the address in the Ownable._owner state variable. Remember, EVM will cram multiple data in a storage slot to use the maximum it can out of 256 bits of space available in a single slot.
Ok nothing exciting so far so lets push an element with a random value in the array by running record() and see how the storage looks now.
We see two new slots being used now:
- Slot#1 containing the length of the array i.e. 0x0000000000000000000000000000000000000000000000000000000000000001
- Slot#0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 containing the first element i.e. 0x0000000000000000000000000000000000000000000000000000000000011111
Slot#1 is where the current size (length) of codex[] array is saved. The first element then gets stored at slot number keccak256(1) (1 being the slot number) = 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6. This slot contains the value we pushed i.e. codex[0] = 0x0000000000000000000000000000000000000000000000000000000000011111. If we push another value, it will be stored at slot# keccak256(1)+1 (because a bytes32 occupies 1 slot) and the array size stored at slot#1 will become 0x0000000000000000000000000000000000000000000000000000000000000002.
Now we can do something really interesting with the codex[] array. Lets call the retract() to delete the element we pushed. This will make the value stored at slot#1 = 0. Now lets call retract() again. What? Yup, lets see what happens after we deduct 1 from the array length that is already 0.
Slot#1 that stores the array length has a value of 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff. What happened is that the length of the array is stored in a uint256 and calling retract() the second time caused the length to underflow to the uint256’s maximum value i.e. (2^256 – 1) = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff. So in essence, we now have a new array with a total of 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff addressable elements i.e. the array spans across all the storage slots. Now that we can access all storage slots via codex[] array, may be we can modify slot#0 and become the owner of the contract. How you ask? Look at the sketch by me below:
As you see, codex[0] will store value at slot# keccak256(1) i.e. 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6. codex[1] will be stored at slot# keccak256(1) + 1, codex[2] will be stored at slot# keccak256(1) + 2 and so on. How about the last slot – what array index will store a value there?
Looking at he diagram above, there are a total of 2^256 slots and codex[0] starts at slot# keccak256(1). So the array index that will modify the last slot becomes “(2^256 – 1) – keccak(1)”. Great what if we add 1 to it? will it wrap around and modify slot#0? Lets find out.
(2^256 – 1) = 115792089237316195423570985008687907853269984665640564039457584007913129639935
keccak(1) = 80084422859880547211683076133703299733277748156566366325829078699459944778998
(2^256 – 1) – keccak(1) = 35707666377435648211887908874984608119992236509074197713628505308453184860937
Finally, 35707666377435648211887908874984608119992236509074197713628505308453184860937 + 1 = 35707666377435648211887908874984608119992236509074197713628505308453184860938
So if the wrap-around works then setting the array index we calculated should allow us to change the admin. Lets try to execute revise() function with these arguments and see if it works:
- 35707666377435648211887908874984608119992236509074197713628505308453184860938
- 0x000000000000000000000001D732931c0fBEfcd235d731b10463318c2A11D6f7
We will fetch the new value of “owner” to see if it worked:
And yes indeed it worked. Mission accomplished.