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 13 - Gatekeeper One

Target: make it past the gatekeeper.

## Contract

``````pragma solidity ^0.5.0;

import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract GatekeeperOne {

using SafeMath for uint256;

modifier gateOne() {
require(msg.sender != tx.origin);
_;
}

modifier gateTwo() {
require(gasleft().mod(8191) == 0);
_;
}

modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}

function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
``````

## Weakness

• Contract relies on `tx.origin`.
• - Being able to read the public contract logic teaches how to pass gateTwo and gateThree.

## Solidity Concepts: explicit conversions and masking

### Explicity type conversions

Be careful, conversion of integers and bytes behave differently!

conversion to uint bytes
shorter type left-truncate: `uint8(273 = 0000 0001 0001 0001) = 00001 0001 = 17` right-truncate: `bytes4(0x1111111122222222) = 0x11111111`
larger type left-padded with 0: `uint16(17 = 0001 0001) = 0000 0000 0001 0001 = 17` right-padded with 0: `bytes8(0x11111111) = 0x1111111100000000`

Masking means using a particular sequence of bits to turn some bits of another sequence "on" or "off" via a bitwise operation.
For example to "mask off" part of a sequence, we perform an `AND` bitwise operation with:

• `0` for the bits to mask
• `1` for the bits to keep
``````    10101010
AND 00001111
=  00001010
``````

## Hack

1. Pass `gateOne`: deploy an attacker contract that will call the victim contract's `enter` function to ensure `msg.sender != tx.origin`. This is similar to what we've accomplished for the Level 4 - Telephone
2. Pass `gateTwo`
3. Pass `gateThree` Note that we need to pass a 8 bytes long `_gateKey`. It is then explicitly converted to a 64 bits long integer.
1. Part one
• `uint16(uint64(_gateKey))`: uint64 _gateKey is converted to a shorter type (uint16) so we keep the last 16 bits of _gateKey.
• `uint32(uint64(_gateKey))`: uint64 _gateKey is converted to a shorter type (uint32) so we keep the last 32 bits of _gateKey
• `uint32(uint64(_gateKey)) == uint16(uint64(_gateKey))`: we convert uint16 to a larger type (uint32), so we pad the last 16 bits of gateKey with 16*0 on the left. This concatenation should equal the last 32 bits of gateKey.
• Mask to apply on the last 32 bits of _gateKey: `0000 0000 0000 0000 1111 1111 1111 1111 = 0x0000FFFF`
2. Part two
• `uint32(uint64(_gateKey)`: last 32 bits of _gateKey
• `uint32(uint64(_gateKey)) != uint64(_gateKey)`: the last 32 bits of gateKey are converted to a larger type (uint64), so we pad them with 320 on the left. This concanetation (320-last32bitsofGateKey) should not equal _gateKey: so we need to keep the first bits of _gateKey
• Mask to apply to keep the first 32 bits: `0xFFFFFFFF`
3. We then concatenate both masks: `0xFFFF FFFF 0000 FFFF` Requires keeping the first 32 bits, mask with 0xFFFFFFFF. Concatenated with the first part: mask = 0xFFFF FFFF 0000 FFFF
4. Part three: `uint32(uint64(_gateKey)) == uint16(tx.origin)`
• we need to take _gatekey = tx.origin
• we then apply the mask on tx.origin to ensure part one and two are correct

## Takeaways

• Abstain from asserting gas consumption in your smart contracts, as different compiler settings will yield different results.
• Be careful about data corruption when converting data types into different sizes.
• Save gas by not storing unnecessary values.
• Save gas by using appropriate modifiers to get functions calls for free, i.e. external pure or external view function calls are free!
• Save gas by masking values (less operations), rather than typecasting

# Level 14 - Gatekeeper Two

Target: make through the gatekeeper.

## Contract

``````pragma solidity ^0.5.0;

contract GatekeeperTwo {

modifier gateOne() {
require(msg.sender != tx.origin);
_;
}

modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller) }
require(x == 0);
_;
}

modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);
_;
}

function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
``````

## Weakness

• gateOne relies on `tx.origin`.
• Being able to reading the public contract logic teaches how to pass gateTwo and gateThree.

## Solidity Concepts: inline assembly & contract creation/initialization

From the Ethereum yellow paper section 7.1 - subtleties we learn:

while the initialisation code is executing, the newly created address exists but with no intrinsic body code⁴.
4.During initialization code execution, EXTCODESIZE on the address should return zero [...]

## Hack

1. gateOne: similar to the gateOne of Level 13 - Gatekeeper One or to the hack of Level 4 - Telephone
2. gateTwo: call the `enter` function during contract initialization, i.e from within `constructor` to ensure `EXTCODESIZE = 0`
3. gateThree
• `uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey)` noted `a ^ b` means `a XOR b`
• `uint64(0) - 1`: underflow, this is equals to `uint64(1)` So we need to take `_gatekey = ~a` (Bitwise NOT) to ensure that the XOR product of each bit of `a` and `b` will be 1.

## Takeaways

During contract initialization, the contract has no intrinsic body code and its `extcodesize` is 0.

# Level 15 - Naughtcoin

## Contract

``````pragma solidity ^0.5.0;

import 'openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol';
import 'openzeppelin-solidity/contracts/token/ERC20/ERC20.sol';

contract NaughtCoin is ERC20, ERC20Detailed {

// string public constant name = 'NaughtCoin';
// string public constant symbol = '0x0';
// uint public constant decimals = 18;
uint public timeLock = now + 10 * 365 days;
uint256 public INITIAL_SUPPLY;

ERC20Detailed('NaughtCoin', '0x0', 18)
ERC20()
public {
player = _player;
INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));
// _totalSupply = INITIAL_SUPPLY;
// _balances[player] = INITIAL_SUPPLY;
_mint(player, INITIAL_SUPPLY);
}

function transfer(address _to, uint256 _value) lockTokens public returns(bool) {
super.transfer(_to, _value);
}

// Prevent the initial owner from transferring tokens until the timelock has passed
modifier lockTokens() {
if (msg.sender == player) {
require(now > timeLock);
_;
} else {
_;
}
}
}
``````

## Weakness

`NaughCoin` inherits from the ERC20 contract.
Looking at this contract, we notice that `transfer()` is not the only function to transfer tokens.

Indeed `transferFrom(address sender, address recipient, uint256 amount)` can be used instead: provided that a 3rd user (`spender`) was allowed beforehand by the `owner` of the tokens to spend a given `amount` of the total `owner`'s balance, `spender` can transfer `amount` to `recipient` in the name of `owner`.

Successfully executing `transferFrom` requires the caller to have allowance for `sender`'s tokens of at least `amount`. The allowance can be set with the `approve` or `increaseAllowance` functions inherited from ERC20.

## Concepts: ERC20 token contract

The ERC20 token contract is related to the EIP 20 - ERC20 token standard. It is the most widespread token standard for fungible assets.

Any one token is exactly equal to any other token; no tokens have special rights or behavior associated with them. This makes ERC20 tokens useful for things like a medium of exchange currency, voting rights, staking, and more.

## Hack

### Architecture

`transferFrom` calls `_transfer` and `_approve`. `_approve` calls `allowance` and checks whether the caller was allowed to spend the `amount` by `sender`.

### Workflow

We want to set the player's allowance for the attack contract. For this we need to call`approve()` which calls `_approve(msg.sender, spender, amount)`. In this call we need `msg.sender == player`, so we can't call `victim.approve()` from the attacker contract. If we would, then `msg.sender == attackerContractAddress`. This would set the attack contract's allowance instead of the player's one.
Finally we let the attacker call `transferFrom()` to transfer to itself the player's tokens.

## Security Takeaways

Get familiar with contracts you didn't write, especially with imported and inherited contracts. Check how they implement authorization controls.

# Solutions on GitHub

You'll only receive email when Gauthier Riou publishes a new post

More from Gauthier Riou