6
0

Understanding Overflow and Underflow Attacks on Smart Contracts

3 weeks ago

    Consider the following diagram:

    Understanding Overflow and Underflow Attacks on Smart Contracts

    That’s a normal odometer which calculates the distance of your car has traveled. This odometer goes from 000000 – 999999. This is why the moment you cross over to 1,000,000 km your odometer will revert back to 000000.

    This may have some seriously grave repercussions.

    Understanding Overflow and Underflow Attacks on Smart Contracts

    Understanding Overflow and Underflow Attacks on Smart Contracts

    Think about the turn of the millennium and the Y2K problem. Y2K was a class of computer bugs that was threatening to cause havoc during the turn of the millennium. To keep it as simple as possible, many programs represented four-digit years with only the final two digits. So, 1998 was stored as 98 and 1999 as 99. However, this would be problematic when the year changes to 2000, since the system will save it as 00 and revert back to 1900.

    Both the examples that we have given you above are a class of errors called “integer overflow.” In this guide, we are going to explore the detrimental effects of the overflow and underflow attacks on smart contracts.

    The Overflow Error

    An overflow occurs when a number gets incremented above its maximum value. Suppose we declare an uint8 variable, which is an unsigned variable and can take up to 8 bits. This means that it can have decimal numbers between 0 and 2^8-1 = 255.

    Keeping this in mind, consider the following example.

    uint a = 255;

    a++;

    This will lead to an overflow because a’s maximum value is 255.

    Solidity can handle up to 256-bit numbers. Incrementing by 1 would to an overflow situation:

    This will lead to an overflow, because a’s maximum value is 255.

    Solidity can handle up to 256 bit numbers. Incrementing by 1 would to an overflow situation:

    0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
    + 0x000000000000000000000000000000000001
    —————————————-
    = 0x000000000000000000000000000000000000

    Let’s check the overflow error with a simple token transfer contract (Code taken from GitHub):

    mapping (address => uint256) public balanceOf;
    
    // INSECURE
    function transfer(address _to, uint256 _value) {
        /* Check if sender has balance */
        require(balanceOf[msg.sender] >= _value);
        /* Add and subtract new balances */
        balanceOf[msg.sender] -= _value;
        balanceOf[_to] += _value;
    }
    
    // SECURE
    
    function transfer(address _to, uint256 _value) {
        /* Check if sender has balance and for overflows */
        require(balanceOf[msg.sender] >= _value && balanceOf[_to] + _value >= balanceOf[_to]);
    
        /* Add and subtract new balances */
        balanceOf[msg.sender] -= _value;
        balanceOf[_to] += _value;
    }
    
    
    
    

    So, what do we have here?

    There are two “transfer” functions which are used in the program above. One is checking for an overflow, while the other isn’t checking for it. The secure transfer function is checking if the balance reaches the maximum value.

    Now, you need to keep one thing in mind. This function may not be relevant all the time especially in the scenario given above. As a developer, one must think if the value is ever going to reach such a high level or are they just needlessly spending gas.

    The Underflow Error

    Now, we reach the other end of the spectrum, the Underflow error. This works in the exact opposite direction. Remember how uint8 can take values only between 0 and 255? Consider the following code.

    unint8 a = 0;

    a–;

    We just caused an underflow which will cause a to have the maximum possible value which is 255.

    Applying the same logic in solidity smart contracts:

    0x000000000000000000000000000000000000
    – 0x000000000000000000000000000000000001
    —————————————-
    = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

    As you can see, it can lead to serious data misrepresentation.

    Let’s check out a piece of code and see how simple underflow can cause havoc in a smart contract(code taken from GitHub):

    mapping (address => uint256) public balanceOf;
    
    // INSECURE
    function transfer(address _to, uint256 _value) {
        /* Check if sender has balance */
        require(balanceOf[msg.sender] >= _value);
        /* Add and subtract new balances */
        balanceOf[msg.sender] -= _value;
        balanceOf[_to] += _value;
    }
    
    // SECURE
    function transfer(address _to, uint256 _value) {
        /* Check if sender has balance and for overflows */
        require(balanceOf[msg.sender] >= _value && balanceOf[_to] + _value >= balanceOf[_to]);
    
        /* Add and subtract new balances */
        balanceOf[msg.sender] -= _value;
        balanceOf[_to] += _value;
    }
    

    In the code example given above, a hacker can take advantage of manipulateMe because the dynamic arrays are stored in a sequential manner. All that a hacker needs to do is:

    Call popBonusCode to underflow
    Compute the storage location of manipulateMe
    Modify and update manipulateMe’s value using modifyBonusCode

    Obviously it is simple to point out all the errors in isolated functions, however, imagine a long and complicated smart contract with thousands of lines of code. It can be really easy to lose track of such an error while code-checking.

    Dangers of Overflow and Underflow Attacks

    The underflow error is more likely to happen than the overflow error, because it will be somewhat infeasible for someone to get the required number of tokens to cause an overflow.

    Imagine a situation where a token holder has only X tokens. Suppose he tries to spend X+1 tokens. If the program doesn’t even check for that, there is a chance that an attacker will end up with more tokens than what they actually have and get a maxed out balance.

    Consider the following example (taken from nethemba):

    pragma solidity ^0.4.22;
    
    contract Token {
    
     mapping(address => uint) balances;
    
     function transfer(address _to, uint _value) public {
       require(balances[msg.sender]  _value >= 0);
       balances[msg.sender] -= _value;
       balances[_to] += _value;
     }
    }
    

    In the code given above, the require condition in “transfer” function might look correct at first glance, but only until you realize that operations between two units produce unit value.

    What this ultimately means is that the value of balances[msg.sender] – _value >= 0 will always be true regardless of the condition.

    Because of this condition, a hacker can actually own more funds than what they actually own and max out their balance. E.g. if the hacker owns 100 tokens and tries to own 101 tokens, they will end up with 100-101 tokens, which gives him 2^256-1 tokens as a result of underflow!

    This can simply break the whole system.

    Real World Problems Cause by Underflow

    4chan’s /biz/ grouped together created “Proof of Weak Hands Coin” or POWH. It was a legit Ponzi Scheme, however, people still bought into it and it grew to over a million dollars.

    However, turns out that POWH coin’s developers didn’t secure all the operations and weren’t able to put up the proper defenses against overflow and underflow attacks. Because of this very reason, an unknown hacker was able to siphon away 2000 ETH which was worth ~$2.3 million.

    As you can see, it is important for a developer to strengthen their defenses against overflow and underflows attacks. As a bounty hunter, you should be on the lookout for overflow/underflow attacks.

    Like what you read? Give us one like or share it to your friends

    6
    0

    Comments

    There are no comments yet