Smart Contract Exploits Part 4 (Miscellaneous)

The final instalment (and first time ever) that the complete set of solutions is published.

by Caleb Lau, Lead developer of Celebrus Advisory


Contract Exploits Pt. 4 – Featuring Capture the Ether – Miscellaneous

Fourth and final part – Just two challenges under Miscellaneous, challenges which can’t seem to find a home in the other sections. Let’s get into it right away.

For those who missed the first part: https://bitcoinmalaysia.com/2018/09/22/smart-contract-exploits-part-1-lotteries/
he second part is found here: https://bitcoinmalaysia.com/2018/09/25/smart-contract-exploits-part-2-math/
And the third part is found here: https://bitcoinmalaysia.com/2018/09/28/smart-contract-exploits-part-3-accounts/
The website where these challenges could be found: https://capturetheether.com/challenges/
And the author of these challenges is the very brilliant smarx, catch him on his twitter handle @smarx.

As before, this article will require some prior knowledge with Solidity and its surrounding dev tools.

Without further ado – Huge spoilers ahead!

 

 

16. Assume Ownership

 

Source code as below.

pragma solidity ^0.4.21;

contract AssumeOwnershipChallenge {
    address owner;
    bool public isComplete;

    function AssumeOwmershipChallenge() public {
        owner = msg.sender;
    }

    function authenticate() public {
        require(msg.sender == owner);

        isComplete = true;
    }
}

 

This question is pretty much a free 300 points; However the implications could be serious and this was used as an actual exploit in the wild.

Looking at the contract itself, we can the function AssumeOwmershipChallenge() is suppose to be the constructor function of the contract. Due to the misspelling however, this becomes a normal callable function instead, allowing us to set the owner to our calling address and winning the challenge.

How it looks like when deployed on Remix.

From Solidity v0.4.22, we could now define our constructor function with constructor(arg1, arg2…), which allows the constructor function to stand out, also avoiding exploits of this nature.

So, to win this challenge, we simply need to copy the code into Remix, point it to the deployed contract address, call function AssumeOwmershipChallenge() and subsequently call authenticate().

 

 

17. Token Bank

 

Source code as below.

pragma solidity ^0.4.21;

interface ITokenReceiver {
    function tokenFallback(address from, uint256 value, bytes data) external;
}

contract SimpleERC223Token {
    // Track how many tokens are owned by each address.
    mapping (address => uint256) public balanceOf;

    string public name = "Simple ERC223 Token";
    string public symbol = "SET";
    uint8 public decimals = 18;

    uint256 public totalSupply = 1000000 * (uint256(10) ** decimals);

    event Transfer(address indexed from, address indexed to, uint256 value);

    function SimpleERC223Token() public {
        balanceOf[msg.sender] = totalSupply;
        emit Transfer(address(0), msg.sender, totalSupply);
    }

    function isContract(address _addr) private view returns (bool is_contract) {
        uint length;
        assembly {
            //retrieve the size of the code on target address, this needs assembly
            length := extcodesize(_addr)
        }
        return length > 0;
    }

    function transfer(address to, uint256 value) public returns (bool success) {
        bytes memory empty;
        return transfer(to, value, empty);
    }

    function transfer(address to, uint256 value, bytes data) public returns (bool) {
        require(balanceOf[msg.sender] >= value);

        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;
        emit Transfer(msg.sender, to, value);

        if (isContract(to)) {
            ITokenReceiver(to).tokenFallback(msg.sender, value, data);
        }
        return true;
    }

    event Approval(address indexed owner, address indexed spender, uint256 value);

    mapping(address => mapping(address => uint256)) public allowance;

    function approve(address spender, uint256 value)
        public
        returns (bool success)
    {
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
        return true;
    }

    function transferFrom(address from, address to, uint256 value)
        public
        returns (bool success)
    {
        require(value <= balanceOf[from]);
        require(value <= allowance[from][msg.sender]); balanceOf[from] -= value; balanceOf[to] += value; allowance[from][msg.sender] -= value; emit Transfer(from, to, value); return true; } } contract TokenBankChallenge { SimpleERC223Token public token; mapping(address => uint256) public balanceOf;

    function TokenBankChallenge(address player) public {
        token = new SimpleERC223Token();

        // Divide up the 1,000,000 tokens, which are all initially assigned to
        // the token contract's creator (this contract).
        balanceOf[msg.sender] = 500000 * 10**18;  // half for me
        balanceOf[player] = 500000 * 10**18;      // half for you
    }

    function isComplete() public view returns (bool) {
        return token.balanceOf(this) == 0;
    }

    function tokenFallback(address from, uint256 value, bytes) public {
        require(msg.sender == address(token));
        require(balanceOf[from] + value >= balanceOf[from]);

        balanceOf[from] += value;
    }

    function withdraw(uint256 amount) public {
        require(balanceOf[msg.sender] >= amount);

        require(token.transfer(msg.sender, amount));
        balanceOf[msg.sender] -= amount;
    }
}

 

That’s a lot of code. Basically, the idea is that this contract acts as a “bank” which tracks our bank balance, and will issue a balance reflecting the number of tokens held, where 500,000 will be held by us, and another 500,000 will be held by the proxy contract which deploys Capture the Ether challenges. The underlying 1,000,000 ERC223 tokens are held in entirety by the challenge contract upon deployment acting as a token bank, as noticed when the ERC223 contract was created it assigns the total supply to the msg.sender. We are able to withdraw the number of tokens allocated to us per the bank balance, and the exploit requires us to withdraw all 1,000,000 ERC223 tokens away from the bank.

Looking through the contract, we can see a withdraw function, presumably one where we will somehow exploit, and it seems that we could, given that the balance is deducted only after the token.transfer line and there is a tokenFallback interface which we can use. We could model our exploit using an exploit contract, where the tokenFallback interface would trigger the same function on our exploit contract which carries out a second withdrawal, all without having the balance decremented for the require(balanceOf[msg.sender] >= amount) balance check to take place. This is in fact similar in nature to the re-entrance attack resulting in the closure of the DAO back during 2016.

Now that we have our exploit entry, what we need to do is to figure out how the full setup and sequence to execute the re-entrance attack, which will need a bank balance as a prerequisite.

To get some bank balance on our exploit contract, we could carry out the below sequence:

  1. Withdraw our 500,000 ERC223 tokens from the token bank.
  2. Create and deploy an exploit contract which implements a tokenFallback() function, which does nothing on its first call so we could transfer the tokens we currently own to this exploit contract.
  3. From the exploit contract, transfer the tokens back to the token bank. This triggers the token bank’s tokenFallback() function, giving our exploit contract 500,000 bank balance.

Now we can execute the actual exploit:

  1. Our exploit contract calls withdraw() of 500,000 tokens, which calls for our exploit contract’s tokenFallback() function.
  2. Our exploit contract’s tokenFallback() calls for withdraw() of 500,000 tokens again. And on the third re-entrance does not do anything.
  3. As the bank balance is deducted only after the token transfer, the re-entrance will execute successfully. (the balance will overflow but by now the challenge is completed)

Piecing together the code:

Our exploit contract.

We will do setup step 1 and 2 manually, which could be easily done through Remix. Step 3 could be done through the transferToTokenBank() function. Upon completion of the setup we just need to call execute() to run the exploit to completion.

Running the steps:

Step 1, withdrawing tokens.
Step 2, transferring all 500,000 ERC223 tokens to our exploit contract.
Step 3, execute transferToTokenBank() so our exploit contract gets loaded with a balance.
Our exploit contract now has some bank balance to execute to re-entrance attack.

Once we initialise the CodeToCall object with the Token Bank Challenge contract address, we are ready to carry out our exploit.

Execute exploit – Note how Transfer() was ran twice.

You might have suspected this, as since we deducted more balance than we actually have, our balance would have overflowed. And at this point, isComplete will be set to true, completing this challenge.

We are done!

 

Conclusion

This wraps up the fourth and final part of this multi-part series. Public blockchains deal with real value, and extra care should be taken when designing and building software directly accessing real value. While some of these exploits could be avoided on a language layer such as having robust bound checks and setting limits to recursive calls, I personally find many of these exploits are not entirely due to Solidity itself, but also building on blockchains do tend to require a different intuition and therefore a different approach.

Therefore keep hacking, breaking stuff, and reviewing how smart contracts react to different implementations. And we can work towards developing more secure and safe smart contracts for everyone.

Leave a comment

four × three =

This site uses Akismet to reduce spam. Learn how your comment data is processed.