Skip to content

19. AlienCodex

r1oga edited this page Oct 30, 2022 · 2 revisions

Target

Claim ownership of the contract

Weakness

codex is stored as a dynamic array. retract() reduces codex length without checking against underflow. So it is actually possible to set the codex array length to 2²⁵⁶ -1, which gives power to modify all storage slots.

Solidity Concepts

Storage Layout of dynamically sized variables

Each smart contract running on the Ethereum Virtual Machine maintains its own state using a key:value storage mapping. The number possible of keys is so huge that most keys actually contain empty values. Each key is called a slot. They are 2²⁵⁶ - 1 slots. Each slot can contain 32 bytes of data.
In the Level 8 -Vault, I listed the basic storage layout rules. Each statically sized variable gets a reserved slot which is defined at compilation time.
But what about dynamically sized variables? As their size is not fixed beforehand, how to know which slots to reserve?
With regular hard drive space or RAM an allocation step to find free space to use exists, which is followed by a release step to put that space back into the pool of available storage. The number of storage locations of a smart contract is so huge that it manages its storage differently. It just needs to figure a way to define a storage location to start from. Indeed the likelihood of having location clashes is (not rigorously) 0.

Due to their unpredictable size, mappings and dynamically-sized array types cannot be stored “in between” the state variables preceding and following them. Instead, they are considered to occupy only 32 bytes with regards to the rules above and the elements they contain are stored starting at a different storage slot that is computed using a Keccak-256 hash.
Assume the storage location of the mapping or array ends up being a slot p after applying the storage layout rules. For dynamic arrays, this slot stores the number of elements in the array (byte arrays and strings are an exception, see below).
Array data is located starting at keccak256(p) and it is laid out in the same way as statically-sized array data would

storage of dynamic array storage of mapping

Hack

  1. Analyze storage layout

    Slot # Variable
    0 contact bool (1 bytes) & owner address (20 bytes), both fit on one slot
    1 codex.length
    keccak256(1) codex[0]
    keccak256(1) + 1 codex[1]
    ...
    2²⁵⁶ - 1 codex[2²⁵⁶ - 1 - uint(keccak256(1))]
    0 codex[2²⁵⁶ - 1 - uint(keccak256(1)) + 1] --> can write slot 0!
  2. call make_contact to be able to pass the contacted modifer

  3. call retract: this provokes and underflow which leads to code.length = 2²⁵⁶ - 1

  4. Compute codex index corresponding to slot 0: 2²⁵⁶ - 1 - uint(keccak256(1)) + 1 = 2²⁵⁶ - uint(keccak256(1))

  5. Call reverse passing it index and your address left padded with 0 to total 32 bytes as content

Takeaways

Modifying a dynamic array length without checking for over/underflow is very dangerous as it can expand the array's bounds to the entire storage area of 2^256 - 1. This can possibly enable modifying the whole contract storage.

Clone this wiki locally