Ethernaut —Naught Coin (ERC20) Exploitation

Bobby T
Coinmonks
12 min readJun 1, 2018

--

https://cdn-images-1.medium.com/max/640/1*8ivGGKYvMP0Vn0aTWm6aAw.png

Background

For the past week and a half, I have been doing my best to familiarize myself with Ethereum Smart Contracts and their implementation using Solidity. With that in mind, up until this point I had only played around with developing, deploying, then interacting with basic Solidity contracts I created on my local testnet. With this in mind, instead of creating my own vulnerable contracts based on some of the known attacks that Consensys has documented here, I looked around for Capture The Flag style challenges for Ethereum contracts. That’s when I stumbled on Zeppelin’s set of Ethernaut challenges, and decided to play around with the problems they created instead.

Baby’s First ERC20 Token!

After digging into the challenges a bit, I decided to start working on the Naught Coin challenge. This challenge involves identifying a vulnerability within an implementation of an ERC20 token. This implementation makes it so that the player receives an entire supply of tokens upon creation of the contract. The one catch to this is, the player must wait 10 years in order to transfer or use any of the tokens — Or that is at least what it should require. In order to complete this challenge, the player must successfully drain all of the tokens from the contract creator into another account without waiting 10 years.

Getting Started

Conceptual Prerequisites

Based on Zeppelin’s difficulty rating, this challenge is a 5/6 in difficulty. In my opinion, this is because a solid understanding of Solidity contract inheritance and variable states (specifically mappings) is required. Outside of this, understanding how a basic ERC20 token works is also required (this is later touched on).

Testing Environment

The Ethernaut set of challenges have already been set up for the player’s use on the Ropsten test network, and can be interacted with via the player’s browser console with a JavaScript interface.

Despite this, I decided to instead use a browser-local test environment to avoid having to deal with setting up a Ropsten account for testing. To do this, I used the browser version of Remix, which can be found here.

Preparing NaughtCoin For Remix (Browser Version)

The original source code for the NaughtCoin challenge has been pasted below, however, changes must be made for this to properly work within the browser version of Remix. If using MetaMask with Ropsten with the given instance provided by Ethernaut, this can safely be ignored, and all other steps of this challenge can be performed as-is.

pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';

contract NaughtCoin is StandardToken {

string public constant name = 'NaughtCoin';
string public constant symbol = '0x0';
uint public constant decimals = 18;
uint public timeLock = now + 10 years;
uint public INITIAL_SUPPLY = 1000000 * (10 ** decimals);
address public player;

function NaughtCoin(address _player) public {
player = _player;
totalSupply_ = INITIAL_SUPPLY;
balances[player] = INITIAL_SUPPLY;
Transfer(0x0, 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);
if (now < timeLock) {
_;
}
} else {
_;
}
}
}

For testing in the browser-based Remix, the following change must be made:

import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';

Must be changed to:

import 'https://github.com/OpenZeppelin/zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';

Making this change will allow imports to properly occur all the way up the chain of dependencies on compilation, which Remix will then import the source code of into the Remix editor side panel:

Initial Analysis Of NaughtCoin

To begin analysis of this challenge, I began by carefuly enumerating the NaughtCoin implementation as was provided in the Ethernaut challenge. To do this, I broke up the approach into logical progressions and detailed them below.

Imports

When I first started the challenge, the first thing that jumped out to me was the line:

contract NaughtCoin is StandardToken

If I wanted to understand what NaughtCoin was, I would need to identify what StandardToken was first, since NaughtCoin was derived from it. Thus, looking at the import ed contract, I decided to start walking down the chain of imports and implementations.

import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';

Tracing from StandardToken , the following imports and implementations were identified:

import "./BasicToken.sol";
import "./ERC20.sol";
contract StandardToken is ERC20, BasicToken

Thus, BasicToken and ERC20 are imported, then inherited from in StandardToken ‘s implementation.

Continuing from BasicToken , the following imports and implementations were identified:

import "./ERC20Basic.sol";
import "../../math/SafeMath.sol";
contract BasicToken is ERC20Basic

From this definition of the BasicToken contract, BasicToken inherits from the imported ERC20Basic , and makes use of the SafeMath library. For brevity, the SafeMath library is skipped over, as it is not relevant to include in this trace. Finally, ERC20Basicdoes not require any other contracts to derive from.

Jumping back up to StandardToken ‘s implementation, ERC20 is also inherited from. Tracing this import, the following imports and implementations were identified:

import "./ERC20Basic.sol";contract ERC20 is ERC20Basic

Similar to BasicToken , ERC20 also inherits from ERC20Basic , and does not require any further tracing because of the shared inheritance.

For convenient reference I have created a basic inheritance flow chart below.

NaughtCoin Contract Variables

Moving past imports and inheritance, definition of variables within NaughtCoin was my next focus. The definitions within NaughtCoin (but not including all inherited definitions) are as follows:

string public constant name = 'NaughtCoin';
string public constant symbol = '0x0';
uint public constant decimals = 18;
uint public timeLock = now + 10 years;
uint public INITIAL_SUPPLY = 1000000 * (10 ** decimals);
address public player;

Of particular importance to us are the timeLock and player variables. The timeLock definition provides the 10 year period that the challenge describes as being required before trading of tokens, and the player variable is the address to the player who receives all of the INITIAL_SUPPLY tokens.

NaughtCoin Constructor

Relevant NaughtCoin constructor code has been included below for reference:

function NaughtCoin(address _player) public {
player = _player;
totalSupply_ = INITIAL_SUPPLY;
balances[player] = INITIAL_SUPPLY;
Transfer(0x0, player, INITIAL_SUPPLY);
}

When the NaughtCoin contract is deployed, an address for player is required and subsequently stored. This is nearly equivalent to the standard usage of owner within most contracts, and is important for the operations involving the transfer of tokens(which we will be exploiting later).

The totalSupply is also stored and initialized to the value of INITIAL_SUPPLY . Though, the more interesting assignment that occurs during initialization is balances[player] . This assignment creates an entry in the balances mapping for the address of player with a value of INITIAL_SUPPLY . At initialization, the player is the only entry that is present and has a balance. Thus, to get NaughtCoin, other addresses must receive a transfer from the player who initialized the NaughtCoin contract. But, once again, referring back to the challenge description, this should not be possible until after a time period of 10 years since initialization.

NaughtCoin lockTokens Modifier

Relevant lockTokens modifier source code has been included below for reference:

modifier lockTokens() {
if (msg.sender == player) {
require(now > timeLock);
if (now < timeLock) {
_;
}
} else {
_;
}
}

In this challenge, the lockTokens modifier is what initially appeared to me as the most important, mostly because it was applied to the transfer function, which allows a particular sender to transfer tokens from one address to another within the scope of the balances .

It’s important for this portion of analysis to understand that a modifier (official docs here) is something that encapsulates the call to a particular function. If you want to perform actions before or after arbitrary function calls, a modifier is the way to invoke those actions. A simple way of reading modifiers is to consider the _ as the function body (think source code of the function: function lol() public { /* everything in here */ } ).

When first analyzing lockTokens , it appears that if you are the msg.sender (the person performing a call), and you are also the player who initiated NaughtCoin , you will continue execution within the first branch of the if statement:

if (msg.sender == player) {
require(now > timeLock);
if (now < timeLock) {
_;
}
}

This is where a require block checks to make sure that 10 years has passed since timeLock . If it hasn’t, a revert will occur (resetting the modified states of the contract and raising an error). The if statement then checks to see if now (the current date/time) is less than timeLock , and if true, will execute the function body.

Based on our analysis, we have identified that we are unable to pass the msg.sender == player check if we are performing a call from an account other than player . Furthermore, we are never able to achieve function execution with the require and if statements inside this block, since they contradict each other. Because of this, the transfer function is rendered pretty much useless, since we are never able to achieve execution from the player call scope. Thus, we are unable to use the transfer function to successfully move tokens to another addresses balance.

NaughtCoin transfer Function

Relevant transfer modifier source code has been included below for reference:

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

Last but not least, the transfer function. This function merely calls to StandardToken ‘s implementation of the transfer function using super , since NaughtCoin inherits from StandardToken .

More importantly however, the lockTokens modifier is applied to the transfer function. Thus, based on our previous analysis of the lockTokens modifier, we have determined we will never be able to execute super.transfer(...) , since we are never able to bypass the timeLock checks.

Conclusion

Based on our findings thus far, we have identified that the transfer function is completely off limits! However, that leaves us with nothing custom to analyze. Instead, we are now forced to look into the implementation of StandardToken for further leads on how to drain the player ‘s balance!

Looking Beyond NaughtCoin — Analysis of the Zeppelin ERC20 Function Implementations

Previously when performing the analysis on the imports and inheritance of NaughtCoin , we identified the chain of imports and inheritance. This is important, because it actually exposes a significant amount of extra functionality in NaughtCoin . In particular, we are looking to identify functions that allow us to modify balances before the 10 year time lock, and assign what is in balances[player] to a balances[notPlayer] of our choosing.

The ERC20 Interface Specification

In order for a token to be compatible with the ERC20 specification, it must have implementations for the following interface.

// ERC Token Standard #20 Interface// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.mdcontract ERC20Interface {function totalSupply() public constant returns (uint);function balanceOf(address tokenOwner) public constant returns (uint balance);function allowance(address tokenOwner, address spender) public constant returns (uint remaining);function transfer(address to, uint tokens) public returns (bool success);function approve(address spender, uint tokens) public returns (bool success);function transferFrom(address from, address to, uint tokens) public returns (bool success);event Transfer(address indexed from, address indexed to, uint tokens);event Approval(address indexed tokenOwner, address indexed spender, uint tokens);}

This specification is one of the most popular, which makes finding further documentation pretty easy. However, I pasted the specification interface since this will be rather useful to reference when walking through this challenge solution.

Identifying Interesting Functions

To begin identifying where I should look first, I found that just looking at the ERC20 interface gave me a few methods I would want to look further into. In particular, the first function that jumped out to me was the transferFrom function. We had identified that NaughtCoin ‘s transfer implementation was rendered useless by the lockTokens modifier, but NaughtCoindid not implement the transferFrom function, which means that the StandardToken implementation of transferFrom is being called.

Having deduced this, the question of what transferFrom actually does is the next question to tackle on our journey to draining some coins.

Analyzing the implementation of transferFrom

Relevant transferFrom function source code, found within StandardToken.sol has been included below for reference:

function transferFrom(
address _from,
address _to,
uint256 _value
)
public
returns (bool)
{
require(_to != address(0));
require(_value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);

balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
emit Transfer(_from, _to, _value);
return true;
}

Upon reading the source code of the transferFrom function, it appears that it allows a third party to transfer a designated amount of tokens from one account to another account. This is derived from reading the requirements for transfer (first three lines of the function body).

In the first require , the _to function argument is compared to the address(0) value. Doing a bit of research regarding the address(0) value, it was discovered that address(0) is sometimes used as an address to burn tokens. More importantly, it is an address that nobody can control. Thus, this require checks to make sure that someone cannot just burn tokens arbitrarily (which would have worked for us, since we just want to drain our tokens out of balances[player] ). Next, the _value passed to the function is checked to make sure that the balance of _from is greater. This prevents anyone from withdrawing more tokens than are available. Finally, the last require is checking to make sure that the _value passed is less than, or equal to, the allowed amount of tokens that the msg.sender is allowed to manage on behalf of _from . The rest of this function body just contains the logic for appropriately transferring tokens from the balances[_from] to the balances[_to] .

The important takeaway from this function is that we have found a new important variable in this contract: the allowed mapping. It seems that based on the third require we are able to use transferFrom if we have an entry of player as the _from (e.g. allowed[player] ), a sub entry of _to in allowed (e.g. allowed[player] ), and any other address than address(0) as the _to .

Based on the findings in transferFrom , we now know that if we want to be able to modify balances through the transferFrom function we need to first have an entry for player in allowed , which appears to define an account that is managed, and a corresponding address under this entry in allowed , which appears to be an account that is allowed to manage the top level account, up to a certain amount of tokens defined by the return of allowed[player][managerAccount] .

Searching For Allowance (Approval)

Relevant approve function source code, found within StandardToken.sol has been included below for reference:

function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}

Now that we have identified the constraints we need in order to execute a transfer using transferFrom , we need to massage the constraints into the allowed mapping. Referring back to the ERC20 interface specification, there is a function named approve , which accepts an address for the spender , and a uint tokens .

Similar to the transferFrom function, StandardToken also appears to provide the implementation for the approve function. Within this function, it builds the exact allowed hierarchy that we need to have permissions to use transferFrom , as long as the msg.sender is the player , and the spender is the account we wish to give tokens to.

Putting It All Together — Exploiting NaughtCoin

With the information we have now, we should have all the calls we need to be able to successfully use the transferFrom . The exploit chain has been detailed as follows.

Initially, the player is the owner of the tokens/contract, and the contract is initialized with balances[owner]=INITIAL_SUPPLY . As such, our challenge is to drain it into an account we can control — the exfil_address . To do this, we have identified that we have the ability to call the transferFrom function to bypass the lockTokens modifier, and transfer a certain amount of tokens from one account to another. The one catch is the requirement that the account performing the transfer is allowed to perform the operations, with the particular amount of tokens. Thus, we go ahead and call the approve function as the player ( owner ), with the exfil_address as the approved spender, with a total amount of tokens allowed to be spent as INITIAL_SUPPLY . Once approved, this will allow exfil_address to transfer the INITIAL_SUPPLY of tokens from player ( owner ) to the exfil_address . Thus, we have successfully drained the player of tokens before the timeLock period of 10 years.

The calls detailed in the exploit can be performed trivially through the browser version Remix through the use of the in-memory execution and the 5 default accounts. Only two accounts are required — One for the deployment of the NaughtCoin contract and the subsequent approve call (which will populate the existing contracts in the sidebar), and the other for calling the transferFrom as the exfil_account . Once the contract is deployed, the call graph pictured above can be followed exactly within Remix to achieve successful execution of the vulnerability.

Conclusion

This challenge was a wild ride of learning how to debug Solidity contracts, and understanding how execution scopes modify the underlying logic of a contract. It is also an extremely thorough method of learning the ERC20 specification and implementation, since you must understand each major function in order to exploit NaughtCoin .

--

--

Bobby T
Coinmonks

OSINT | Web Sec | @HackUCF. Opinions are my own. Even ghosts leave 👣.