Ethernaut: Levels 4 to 6
April 15, 2020•974 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'.
go to:
Token
Delegation
Level 4 - Telephone
Target: claim ownership of the contract.
Contract
pragma solidity ^0.5.0;
contract Telephone {
address public owner;
constructor() public {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
Weakness
A conditional requirements uses tx.origin
Solidity Concepts: tx.origin vs msg.sender
tx.origin (address payable)
: sender of the transaction (full call chain)msg.sender (address payable)
: sender of the message (current call)
In the situation where a user call a function in contract 1, that will call function of contract 2:
at execution contract 1 | at execution in contract 2 | |
---|---|---|
msg.sender | userAddress | contract1Address |
tx.origin | userAddress | userAddress |
Hack
Deploy an attacker contract.
Call the changeOwner
function of the original contract from the attacker contract to ensure tx.origin != msg.sender
and pass the conditional requirement.
Takeaways
Level 5 - Token
Target: "You are given 20 tokens to start with and you will beat the level if you somehow manage to get your hands on any additional tokens. Preferably a very large amount of tokens."
Contract
pragma solidity ^0.5.0;
contract Token {
mapping(address => uint) balances;
uint public totalSupply;
constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}
Weakness
An sum operation is performed but overflow isn't checked for.
Solidity Concepts: bits storage, underflow/overflow
Ethereum’s smart contract storage slot are each 256 bits, or 32 bytes. Solidity supports both signed integers, and unsigned integers uint of up to 256 bits.
However as in many programming languages, Solidity’s integer types are not actually integers. They resemble integers when the values are small, but behave differently if the numbers are larger. For example, the following is true: uint8(255) + uint8(1) == 0. This situation is called an overflow. It occurs when an operation is performed that requires a fixed size variable to store a number (or piece of data) that is outside the range of the variable’s data type. An underflow is the converse situation: uint8(0) - uint8(1) == 255.
Hack
Provoke the overflow by transferring 21 tokens to the contract.
Takeaways
Check for over/underflow manually:
if(a + c > a) {
a = a + c;
}
Or use OpenZeppelin's math library that automatically checks for overflows in all the mathematical operators.
Level 6 - Delegation
Target: claim ownership of the contract.
Contract
pragma solidity ^0.5.0;
contract Delegate {
address public owner;
constructor(address _owner) public {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) public {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
function() external {
(bool result, bytes memory data) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
Weakness
The Delegation
fallback implements a delegatecall.
By sending the right msg.data
we can trigger the function pwn()
of the Delegate contract. Since this function is executed by a delegatecall the context will be preserved:
owner = msg.sender = address of contract that send data to the Delegation fallback (attacker contract)
Solidity Concepts: storage, call another contract's function
There are several ways to interact with other contracts from within a given contract.
If ABI available
If the ABI (like an API for smart contract) and the contract's address are known, we can simply instantiate (e.g with a contract interface) the contract and call its functions).
contract Called {
function fun() public returns (bool);
}
contract Caller {
Called public called;
constructor (Called addr) public {
called = addr;
}
function call () {
called.fun();
}
}
ABI not available: delegatecall
or call
Calling a function means injecting a specific context (arguments) to a group of commands (function) and commands are executing one by one with this context.
Bytecode
In Ethereum, a function call can be expressed by a 2 parts bytecode as long as 4 + 32 * N bytes.
- Function Selector: first 4 bytes of function call’s bytecode.
Generated by hashing target function’s name plus with the type of its arguments excluding empty space. Ethereum uses keccak-256 hashing function to create function selector:
functionSelectorHash = web3.utils.keccak256('func()')
- Function Argument: convert each value of arguments into a hex string padded to 32bytes.
If there is more than one argument, they are concatenated.
In Solidity encoding the function selector together with the arguments can be done with abi.encode
, abi.encodePacked
, abi.encodeWithSelector
and abi.encodeWithSignature:
abi.encodeWithSignature("add(uint256,uint256)", valueForArg1, valueForArg2)
Call: doesn't preserve context.
Can be used to invoke public functions by sending data
in a transaction.
contractInstance.call(bytes4(keccak256("functionName(inputType)"))
DelegateCall: preserves context
contractInstance.delegatecall(bytes4(keccak256("functionName(inputType)"))
Delegate calls preserve current calling contract's context (storage, msg.sender, msg.value).
The calling contract using delegate calls allows the called contract to mutate its state.
Hack
- Compute the encoded hash that will be used for
msg.data
- Send
msg.data
in a transaction to the contract fallback
Takeaways
- Use the higher level call() function to inherit from libraries, especially when you don’t need to change contract storage and do not care about gas control.
- When inheriting from a library intending to alter your contract’s storage, make sure to line up your storage slots with the library’s storage slots to avoid unexpected state changes..
- Authenticate and do conditional checks on functions that invoke delegatecalls.