Physical Address

304 North Cardinal St.
Dorchester Center, MA 02124

Integer Underflow: A common, but serious programming pitfall

Lets understand how a single miscalculated number can result in devastating security breaches.
Image courtesy: freepik

Assigning numerical values to variables is such a common thing in programming that we never ever consider the possibility that these simple operations could sometimes result in very serious security flaws. Today in this post I’ll be throwing some light on this topic and try to explain how a tiny oversight in code could result in a problem which open the doors for some major security vulnerabilities.

A little background

Ever heard of The Heartbleed Bug? I guess you have. It was a major security vulnerability in the popular OpenSSL cryptographic software library that induced both excitement and fear in the hacker/dev community during its time. It was caused by an arithmetic overflow that allowed an attacker to read sensitive information, such as private keys, usernames, passwords, and other confidential data from memory.

What is integer underflow?

Talking in a layman term, it simply means a special wraparound condition in which the result of an integer subtraction operation instead of reducing the value of an integer, actually increases its value. So basically it does exactly the opposite of what a programmer had intended.

Technically speaking: Integer underflow occurs when an arithmetic operation attempts to produce a numeric value lower than the minimum limit of the data type, causing the value to wrap around to the maximum possible value.

It can also be understood by an example of a car’s analog odometer, which flips back to the maximum number when it crosses zero in reverse.

What causes integer underflow?

Look at the following ‘C’ code snippet, here we are declaring an unsigned integer ‘x’ and then assigning ‘0’ to it. Later we are subtracting ‘1’ from this number, which in general mathematical sense would result in ‘-1’, but because computers use bits to store the information it actually results in ‘4294967295’ (a 32 bit unsigned integer), causing an integer underflow bug.

#include <stdio.h>

int main(){
    unsigned int x = 0;
    x = x - 1;  // This leads to underflow and x becomes 4294967295 (for 32-bit unsigned integers)
    printf("%d", x); // OUTPUT: 4294967295
}

Let’s assume this code represents ‘magic’ in an RPG game’s inventory system, and each time the player uses his ‘magic’ ability we reduce it by ‘1’, now after it reaches ‘0’ this automatically recharges the players magic with 4294967295.

A better approach to deal with this is as below:

#include <stdio.h>

int main(){
    unsigned int x = 0;

    /** Some game logic here */

    if(x != 0){
        x -= 1; // This will prevent the integer underflow bug.
    }

    printf("%d", x); // OUTPUT: 0
}

You must be thinking, ‘Okay it created some problem but where is the vulnerability?’. If a player gets a huge number of magic power ability that doesn’t amount to a serious security flaw, but just a small programming oversight, right? So lets discuss the vulnerability in our next section.

How arithmetic under / overflows causes security problems?

Lets assume we have a vulnerable application that copies data to a target location from a given source. For this copy operation to complete securely we perform an input size check and allow the copy operation only when the size is within limit. Lets see how we can violate this security and still perform the copy.

#include <iostream>

void copyData(char* dest, const char* src, unsigned int len) {
    if (len > 255) {
        std::cout << "Error: Data too large to copy." << std::endl;
        return;  // Prevent copying if the data is too large
    }
    
    for (unsigned int i = 0; i < len; ++i) {
        dest[i] = src[i];
    }

    std::cout << "Data copied successfully!" << std::endl;
}

int main() {
    char buffer[256];  // 256-byte buffer
    const char* data = "This is a test data string.";  // Sample data
    
    unsigned int length;
    std::cout << "Enter length of data to copy: ";
    std::cin >> length;

    copyData(buffer, data, length);

    return 0;
}

Understanding the problem:

Looking at the code above might not make the problem obvious in the first glance, but as we dig a little deeper we see that the problem lies in the ‘if’ check where we are performing only upper-bound check. So if the user provides a negative number (like -1), the unsigned int length will wrap-around to a large positive number (in this case, 4294967295 for a 32-bit system). This bypasses the if (len > 255) check since 4294967295 is technically greater than 255, as unsigned int treats negative input values as large positive numbers.

How attackers use this vulnerability:

“Never trust a user’s input and always code defensively”, This is the mantra that we must always stick to. Our application is making an assumption that a safe value will always be passed to it, when in fact, it is not guaranteed. In a real word scenario an attacker could exploit this vulnerability in our application to overwrite important memory regions, crash the program or even worse execute arbitrary code remotely, also known as an RCE attack.

What can we do to mitigate such bugs

There is no fool proof way to completely mitigate such bugs, its just that we have to be more attentive while writing security checks and a little mindful of how the logic that we are coding could be violated by an attacker. However, there are some pointers that are worth mentioning here:

  • Input Validation: Always validate user inputs and explicitly handle negative values before performing arithmetic operations with unsigned integers.
  • Signed Integers for Calculations: Use signed integers where underflow or overflow needs to be handled explicitly.
  • Bounds Checking: Ensure that checks are performed to detect underflow/overflow conditions before performing operations like memory copying.

Following is the secured version of the IF condition check:

 if (len < 0 || len > 255) {  // Prevent underflow and overflow
        std::cout << "Error: Invalid data length." << std::endl;
        return;
    }

Conclusion

There is another related topic with arithmetic-underflow which is arithmetic-overflow, this is exactly the same concept but just in reverse. Here instead of an underflow an overflow happens and big values wraparound to become small values. However both these conditions can be a significant security risk if not handled properly. Input validation, bounds checking, and using appropriate data types are critical to prevent such vulnerabilities.

Share this post with your friends on:
Mohit Tomar
Mohit Tomar

A nerd with a passion for computer security, AI/ML, and all things Linux. I enjoy diving deep into servers, coding, and the latest in cybersecurity. Always curious and ready to tackle complex problems with a geeky flair.

Articles: 5

Leave a Reply

Your email address will not be published. Required fields are marked *