Ethernaut: Levels 16 to 18
April 17, 2020•1,753 words
The Ethernaut is a Web3/Solidity based wargame inspired from overthewire.org, played in the Ethereum Virtual Machine. Each level is a smart contract that needs to be 'hacked'.
goto:
Recovery
MagicNumber
Level 16 - Preservation
Target
A contract creator has built a very simple token factory contract. Anyone can create new tokens with ease. After deploying the first token contract, the creator sent 0.5 ether to obtain more tokens. They have since lost the contract address.
This level will be completed if you can recover (or remove) the 0.5 ether from the lost contract address.
Contract
pragma solidity ^0.5.0;
contract Preservation {
// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}
// set the time for timezone 1
function setFirstTime(uint _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
}
// Simple library contract to set the time
contract LibraryContract {
// stores a timestamp
uint storedTime;
function setTime(uint _time) public {
storedTime = _time;
}
}
Weakness
Preservation
uses Libraries: Libraries usedelegatecall
s. [Level 6 -Delegation] taught us that usingdelegatecall
is risky as it allows the called contract to modifiy the storage of the calling contract.- Storage layouts of
Preservation
andLibraryContract
don't match: Calling the library won't modifiy the expectedstoredTime
variable. ## Solidity Concept: libraries > Libraries are similar to contracts, but their purpose is that they are deployed only once at a specific address and their code is reused using the DELEGATECALL (CALLCODE until Homestead) feature of the EVM. This means that if library functions are called, their code is executed in the context of the calling contract, i.e. this points to the calling contract, and especially the storage from the calling contract can be accessed.
So Libraries are a particular case where functions are on purpose called with delegatecall
because preserving context is desired.
Hack
As libraries use delegatecall
, they can modify the storage of Preservation
.
LibraryContract
can modify the first slot (index 0) of Preservation
, which is address public timeZone1Library
. So we can "set" timeZone1Library
by calling setFirstTime(_timeStamp)
. The uint _timeStamp
passed will converted to an address
type though. It means we can cause setFirstTime()
to execute a delegatecall
from a library address different from the one defined at initialization. We need to define this malicious library so that its setTime
function modifies the slot where owner
is stored: slot of index 2.
Takeaways
Level 17 - Recovery
Contract
pragma solidity ^0.5.0;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Recovery {
//generate tokens
function generateToken(string memory _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);
}
}
contract SimpleToken {
using SafeMath for uint256;
// public variables
string public name;
mapping (address => uint) public balances;
// constructor
constructor(string memory _name, address _creator, uint256 _initialSupply) public {
name = _name;
balances[_creator] = _initialSupply;
}
// collect ether in return for tokens
function() external payable {
balances[msg.sender] = msg.value.mul(10);
}
// allow transfers of tokens
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] = balances[msg.sender].sub(_amount);
balances[_to] = _amount;
}
// clean up after ourselves
function destroy(address payable _to) public {
selfdestruct(_to);
}
}
Weakness
The generation of contract addresses are pre-deterministic and can be guessed in advance.
Solidity Concepts: selfdestruct, encodeFunctionCall, & generation of contract addresses
- selfdestruct: see [Level 7 - Force] Sefdestruct is a method tha can be used to send ETH to a recipient upon destruction of a contract.
- encodeFunctionCall
At Level 6 - Delegation, we learnt how to make function call even though we don't know the ABI: by sending a raw transaction to a contract and passing the function signature into the data argument. More convenienttly, this can be done with the encodeFunctionCall function of web3.js:
web3.eth.abi.encodeFunctionCall(jsonInterface, parameters)
- generation of contract addresses, from the Etherem yellow paper, section 7 - contract creation:
So in JavaScript, using the web3.js and rlp libraries, one can compute the contract address generated upon creation as follows.
// Rightmost 160 digits means rightmost 160 / 4 = 40 hexadecimals characters
contractAddress = '0x' + web3.utils.sha3(RLP.encode([creatorAddress, nonce])).slice(-40))
Hack
- Instantiate level. This will create 2 contracts:
- nonce 0:
Recovery
contract - nonce 1:
SimpleToken
contract
- nonce 0:
- Compute the
address
of theSimpleToken
:- sender = instance address
- nonce = 1
- Use
encodeFunctionCall
to call thedestruct
function ofSimpleToken
instance ataddress
. ## Takeaways > Contract addresses are deterministic and are calculated by keccack256(rlp([address, nonce])) where the address is the address of the contract (or ethereum address that created the transaction) and nonce is the number of contracts the spawning contract has created (or the transaction nonce, for regular transactions). Because of this, one can send ether to a pre-determined address (which has no private key) and later create a contract at that address which recovers the ether. This is a non-intuitive and somewhat secretive way to (dangerously) store ether without holding a private key. An interesting blog post by Martin Swende details potential use cases of this.
Level 18 - MagicNumber
Target
provide the Ethernaut with a "Solver", a contract that responds to "whatIsTheMeaningOfLife()" with the right number.
Contract
pragma solidity ^0.5.0;
contract MagicNum {
address public solver;
constructor() public {}
function setSolver(address _solver) public {
solver = _solver;
}
/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}
Solidity Concepts
Contract creation bytecode
Smart contracts run on the Ethereum Virtual Machine (EVM). The EVM understands smart contracts as bytecode. Bytecode is a sequence of hexadecimal characters:
0x6080604052348015600f57600080fd5b5069602a60005260206000f3600052600a6016f3fe
.
Developers on the other hand, write and read them using a more human readable format: solidity files.
The solidity compiler digests .sol
files to generate:
- contract creation bytecode: this is the smart contract format that the EVM understands
- assembly code: this is the bytecode as a sequence of opcodes. From a human point of view, it is less readable that Solidity code but more readable than bytecode.
- Application Binary Interface (ABI): this is like a customized interpret in a JSON format that tells applications (e.g a Dapp making function calls using web3.js) how to communicate with a specific deployed smart contract. It translates the application language (JavaScript) into bytecode that the EVM can understand and execute.
Contract creation bytecode contain 2 different pieces of bytecode:
- creation code: only executed at deployment. It tells the EVM to run the constructor to initialize the contract and to store the remaining runtime bytecode.
- runtime code: this is what lives on the blockchain at what Dapps, users will interact with.
EVM = Stack Machine
As a stack machine, the EVM functions according to the Last In First Out principle: the last item entered in memory will be the first one to be consumed for the next operation.
So an operation such as 1 + 2 * 3
will be written 3 2 * 1 +
and will be executed by a stack machine as follows:
Stack Level | Step 0 | Step 1 | Step 2 | Step 3 | Step 4 | Step 5 | Step 6 |
---|---|---|---|---|---|---|---|
0 | 3 | 2 | * | 6 | 1 | + | 7 |
1 | 3 | 2 | 6 | 1 | |||
2 | 3 | 6 |
In addition to its stack component, the EVM has memory, which is like RAM in the sense that it is cleared at the end of each message call, and storage, which corresponds to data persisted between message calls.
OPCODES
How do we control the EVM? How do we tell it what to execute?
We have to give it a sequence of instructions in the form of OPCODES. An OPCODE can only push or consume items from the EVM’s stack, memory, or storage belonging to the contract.
Each OPCODE takes one byte.
Each OPCODE has a corresponding hexadecimal value: see the opcode values mapping here (from pyevm) or in the Ethereum Yellow Paper - appendix H.
So "assembling" the OPCODES hexadecimal values together means reconstructing the bytecode.
Splitting the bytecode into OPCODES bytes chunks means "disassembling" it.
For a more detailed guide on how to deconstruct a solidity code, check this post by Alejandro Santander in collaboration with Leo Arias.
Hack
Runtime code
# (bytes) OPCODE Stack (left to right = top to bottom) Meaning bytecode 00 PUSH1 2a push 2a (hexadecimal) = 42 (decimal) to the stack 602a 02 PUSH1 00 2a push 00 to the stack 6000 05 MSTORE 00, 2a mstore(0, 2a)
, store 2a = 42 at memory position 052 06 PUSH1 20 push 20 (hexadecimal) = 32 (decimal) to the stack (for 32 bytes of data) 6020 08 PUSH1 00 20 push 00 to the stack 6000 10 RETURN 00, 20 return(memory position, number of bytes)
, return 32 bytes stored in memory position 0f3 The assembly of these 10 bytes of OPCODES results in the following bytecode:
602a60005260206000f3
Creation code
We want to excute the following:mstore(0, 0x602a60005260206000f3)
: store the 10 bytes long bytecode in memory at position 0.
This will store602a60005260206000f3
padded with 22 zeroes on the left to form a 32 bytes long bytestring.return(0x16, 0x0a)
: starting from byte 22, return the 10 bytes long runtime bytecode.
# (bytes) OPCODE Stack (left to right = top to bottom) Meaning bytecode 00 PUSH10 602a60005260206000f3 push the 10 bytes of runtime bytecode to the stack 69602a60005260206000f3 03 PUSH 00 602a60005260206000f3 push 0 to the stack 6000 05 MSTORE 0, 602a60005260206000f3 mstore(0, 0x602a60005260206000f3)
052 06 PUSH a push a = 10 (decimal) to the stack 600a 08 PUSH 16 a push 16 = 22 (decimal) to the stack 6016 10 RETURN 16, a return(0x16, 0x0a)
f3 The complete contract creation bytecode is then
69602a60005260206000f3600052600a6016f3
Deploy the contract with
web3.eth.sendTransaction({ data: '0x69602a60005260206000f3600052600a6016f3' })
, which returns a Promise. The deployed contract address is the value of thecontractAddress
property of the object returned when the Promise resolves.Pass the address of the deployed solver contract to the
setSolver
function of theMagicNumber
contract.
Takeaways
Having an understanding of the EVM at a lower level, especially understanding how contracts are created and how bytecode can be dis/assembled from/to OPCODES is benefetial to smart contract developers in several ways:
- better debugging
- possibilities to finely optimize contract runtime or creation code
However both operations, assembling OPCODES into bytecode or disassembling bytecode into OPCODES, are cumbersome and tricky to manually perform without mistakes. So for efficiency and security reasons, developers are better off leaving it to compilers, writing solidity code and working with ABIs!