Ethernaut: Levels 7 to 9
April 15, 2020•1,121 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'.
Level 7 - Force
Target: make the balance of the contract greater than zero.
Contract
pragma solidity ^0.5.0;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
Solidity Concept: selfdestruct
3 methods exist to receive Ethers:
- Message calls and payable functions
addr.call{value: x}('')
: returns success condition and return data, forwards all available gas, adjustable<address payable>.transfer(uint256 amount)
: reverts on failure, forwards 2300 gas stipend, not adjustable<address payable>.send(uint256 amount) returns (bool)
: returns false on failure, forwards 2300 gas stipend, not adjustablefunction receive() payable external {}
- contract designated as recipient for mining rewards
selfdestruct(address payable recipient)
: destroy the current contract, sending its funds to the given Address and end execution.
Hack
As the contract to hack has no payable function to receive ether, we send ether to it by selfdestructing another contract, designating the victim contract as the recipient.
Takeaways
By selfdestructing a contract, it is possible to send ether to another contract even if it doesn't have any payable functions.
This is dangerous as it can result in losing ether: sending ETH to a contract without withdraw function, or to a removed contract.
Level 8 - Vault
Contract
Target: Unlock vault.
pragma solidity ^0.5.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) public {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
Weakness
The unlock
function relies on a password with a private
visibility. There is no real privacy on Ethereum which is public blockchain. The private
visibility parameter is misleading as all data can still be read. Indeed the contract is available, so an attacker can know in which storage slot a variable is stored in and access its value manually using getStorageAt
.
Solidity Concepts: storage
Storage: storage vs memory
- storage: persistent data between function calls and transactions.
- Key-value store that maps 256-bit words to 256-bit words.
- Not possible to enumerate storage from within a contract
- Costly to read, and even more to initialise and modify storage. Because of this cost, you should minimize what you store in persistent storage to what the contract needs to run. Store data like derived calculations, caching, and aggregates outside of the contract.
- A contract can neither read nor write to any storage apart from its own.
- memory ~ RAM: not persistent. A contract obtains a freshly cleared instance of memory for each message call
Layout
Data stored is storage in laid out in slots according to these rules:
- Each slot allows 32 bytes = 256 bits
- Slots start at index 0
- Variables are indexed in the order they’re defined in contract
contract Sample { uint256 first; // slot 0 uint256 second; // slot 1 }
- Bytes are being used starting from the right of the slot
- If a variable takes under < 256 bits to represent, leftover space will be shared with following variables if they fit in this same slot.
- If a variable does not fit the remaining part of a storage slot, it is moved to the next storage slot.
- Structs and arrays (non elementary types) always start a new slot and occupy whole slots (but items inside a struct or array are packed tightly according to these rules).
- Constants don’t use this type of storage. (They don't occupy storage slots)
Read storage: web3.eth.getStorageAt
Knowing a contract's address and the storage slot position a variable is stored in, it is possible to read its value value using the getStorageAt
function of web3.js.
Hack
- Read contract to find out in slot
password
is stored in:locked
bool takes 1 bit of the first slot index 0password
is 32 bytes long. It can fit on the first slot so it goes on next slot at index 1
- Read storage at index 1
- Pass this value to the unlock function ## Takeaways
- Nothing is private in the EVMhttps://solidity.readthedocs.io/en/v0.6.2/security-considerations.html#private-information-and-randomness: addresses, timestamps, state changes and storage are publicly visible.
- Even if a contract set a storage variable as
private
, it is possible to read its value withgetStorageAt
- When necessary to store sensitive value onchain, hash it first (e.g with sha256)
Level 9 - King
Target: Prevent losing kingship when submitting your instance.
Contract
pragma solidity ^0.5.0;
contract King {
address payable king;
uint public prize;
address payable public owner;
constructor() public payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}
function() external payable {
require(msg.value >= prize || msg.sender == owner);
king.transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
function _king() public view returns (address payable) {
return king;
}
}
Weakness
The contract uses transfer
instead of a withdraw pattern to send Ether.
Solidity Concepts: sending and receiving Eth
- Neither contracts nor “external accounts” are currently able to prevent that someone sends them Ether. Contracts can react on and reject a regular transfer
- If a contract receives Ether (without a function being called), either the receive Ether or the fallback function is executed. If it does not have a receive nor a fallback function, the Ether will be rejected (by throwing an exception).
Hack
Upon submission, the level contract sends an Ether amount higher than prize
to the contract instance contract fallback to reclaim kingship. The fallback uses transfer
to send the prize value to the current king which about to be replace. Only then the king address is updated. If the current king is a contract without a fallback or receive function execution will fail before the king address can be updated.
- Deploy a malicious contract without neither a payable fallback nor a payable receive function
- Let this malicious contract become king by sending Ether to the vKing contract
- Submit instance
Takeaways
- Assume any external account or contract you don't know/own is potentially malicious
- Never assume transactions to external contracts will be successful
- Handle failed transactions on the client side in the event you do depend on transaction success to execute other core logic.
Especially when transferring ETH:
- Avoid using
send()
ortransfer()
. If usingsend()
check returned value - Prefer a 'withdraw' pattern to send ETH