Is Your Code Secure Against the Threat of Buffer Overflow Vulnerabilities?

Buffer overflows have plagued the C/C++ development community for years. While the C language empowers developers to access memory directly via pointers, it also opens the door to overflow problems. Safe coding practices help developers avoid buffer overflows to some extent (at the cost of performance), but sometimes buffer overflows can be subtle and complex to find and resolve.

Often, only when a buffer overflow escapes into production and is discovered as a security flaw (or worse, exploited) does one realize how subtly it was introduced in the code. Remember the Heartbleed Bug in the popular OpenSSL library? The most recent Cloudbleed Bug or the Heartbleed Explanation provide good examples of the issue.

What Is a Buffer Overflow?

Broadly speaking, a buffer overflow occurs when an operation that writes data to an allocated buffer exceeds the buffer boundaries. In doing so, the operation accesses adjacent memory locations that it should not. Let’s view an example:

#define MAXSIZE 100
.
.
char localBuf [MAXSIZE]
.
.
gets (localBuf)

The gets() function does not allow you to check if the data read to localBuf has less than MAXSIZE characters. You rely on the sender of the data to stay within this limit. A malicious user can easily overflow the buffer by sending data greater than MAXSIZE characters and access adjacent regions in the stack.

Chart showing storage allocated to localbuf and the area that can be accessed by overflowing localbuf

Chart showing storage allocated to localbuf and the area that can be accessed by overflowing localbuf.

How Serious Are Buffer Overflows?

The severity of a buffer overflow depends on what is stored in the adjacent memory regions. When you call a function in your code, all data relevant to the call are stored in adjacent regions on the call stack.

Consider these two situations:

  • Area adjacent to the overflowing buffer stores another local variable or a function parameter
    The variable value is corrupted because of the buffer overflow. You can encounter the corrupted value causing a run-time error elsewhere in your code. The corrupted value can also lead to a silent incorrect behavior - one that can escape detection. You end up with unsafe and unreliable code.
  • Area adjacent to the overflowing buffer stores return address of the function
    The call stack also stores the address that the program jumps to when the called function returns. This address is compromised because of the buffer overflow. An attacker can overflow your buffer precisely enough so that your program, instead of returning to the call site, jumps to the location of malicious code. If your program has sufficient privileges, this malicious code can take control of your application and lead to exploitation.

How Difficult Is It to Avoid Buffer Overflows?

You might say that the issue shown above is easy to avoid. Before using unsecured functions such as gets(), check the buffer size. Or, even better, do not use such unsecured functions at all.

Consider another example of a buffer overflow and see how subtle it can be. This code snippet comes from the libPNG image decoder, used by several applications including Mozilla and some versions of Internet Explorer (cf. OWASP).

if (!(png_ptr->mode & PNG_HAVE_PLTE)) {
		/* Should be an error, but we can cope with it */
		png_warning(png_ptr, "Missing PLTE before tRNS");
	}
	else if (length > (png_uint_32)png_ptr->num_palette) {
		png_warning(png_ptr, "Incorrect tRNS chunk length");
		png_crc_finish(png_ptr, length);
		return;
	}
	...
	png_crc_read(png_ptr, readbuf, (png_size_t)length);

On first glance, the following check:

length > (png_uint_32)png_ptr->num_palette

is exactly what is needed to avoid a buffer overflow when you use length. However, the snag here is that the check occurs in an else if block. The if block preceding the else if block performs an unrelated check on png_ptr->mode; if that previous check fails, control goes outside the if - else if chain with just a warning. The second check on the variable length is not performed at all.

In other words, a subset of execution paths exists where, despite the check, a buffer overflow can occur. An issue as subtle as this can be detected only if you can keep track of all execution paths in the program. And that is precisely what Polyspace static analysis does.

How Can You Use Polyspace Static Analysis to Avoid Buffer Overflows?

There is a plethora of static analysis tools that claim to check for buffer overflows, and they do so using different heuristics or some form of data flow analysis. However, this is an insufficient approach since safety- and security-critical systems cannot afford to have any false negatives (i.e., a missed instance of a buffer overflow) in the deployed embedded software.

Polyspace® products take a two-step approach to address this challenge. This approach is in alignment with the requirements of the software development workflow.

Polyspace Bug Finder™ applies fast formal methods to enable developers to comply with coding guidelines, which helps avoid buffer overflows. Polyspace Bug Finder provides various checkers that not only identify buffer overflow issues, but also other potential constructs that can lead to and exploit a buffer overflow vulnerability. This early and quick feedback enables the development teams to address such issues before they propagate further downstream into the software builds, saving testing and debugging resources.

For example, if you are following a security standard such as CWE or CERT C, you can use Polyspace Bug Finder to adhere to the rules. The following CWE IDs, CERT C rules, and ISO 17961 secure coding guidelines handle buffer overflows:

  • CWE
    • CWE 121: Stack-based buffer overflow
    • CWE 129: Improper validation of array index
  • CERT C
    • INT04-C: Enforce limits on integer values originating from tainted sources
    • ARR30-C: Do not form or use out-of-bounds pointers or array subscripts
  • ISO/IEC 17961
    • invptr: Forming or using out-of-bounds pointers or array subscripts
    • taintformatio: Using a tainted value to write to an object using a formatted input or output function

You also have the MISRA C:2012 Directive 4.14 that states, “The validity of values received from external sources shall be checked.”

Polyspace Bug Finder, through its various checkers, provides extensive support for detecting and avoiding stack-based buffer overflows.

  • Array access out of bounds: You can use an index to go beyond the size of the array.
    int i;
        int fib[10];
     
        for (i = 0; i < 10; i++) 
           {
            if (i < 2) 
                fib[i] = 1;
             else 
                fib[i] = fib[i-1] + fib[i-2];
           }
    
        printf("The 10-th Fibonacci number is %i .\n", fib[i]);   
    
  • Pointer access out of bounds: You can use a pointer assigned to a block of memory to access memory beyond that block.
    int arr[10];
     int *ptr=arr;
    
     for (int i=0; i<=9;i++)
       {
        ptr++;
        *ptr=i;
        /* Defect: ptr out of bounds for i=9 */
       }
    
  • Array access with tainted index: You can obtain a variable from an external source and use it as array index without checking if the value is less than array size.
    #define MAXSIZE 100 
    int arr[MAXSIZE];
      
      int func(int num) {
      int localVar;
      /* Inermediate code where no check on num is done */
    arr[num]= localVar;
    }
    
  • Destination buffer overflow in string manipulation: You use string manipulation functions such as sprintf() and write strings that are too large for the buffer that you are writing to.
    char buffer[20];
    char *fmt_string = "This is a very long string, it does not fit in the buffer";
    
    sprintf(buffer, fmt_string);
    
  • Buffer overflow from incorrect string format specifier: When you use functions such as sscanf, your string format specifier indicates a string size greater than the storage area allocated for the string.
       
    char buf[32];
    sscanf(str[1], "%33c", buf);
    

Despite the Polyspace Bug Finder analysis, a few subtle and complex buffer overflow defects can remain in the source code, since Polyspace Bug Finder is not a sound tool (i.e., it can have false negatives). This is where Polyspace Code Prover™ can help, as it is an abstract interpretation-based formal semantic analysis tool that can exhaustively verify the dynamic run-time behavior.

How Can You Use Polyspace Code Prover to Avoid Buffer Overflows?

In our previous example, despite a check on length, a buffer overflow could potentially creep in unnoticed through one control path. You encounter more complexity when such paths are dependent on the run-time information.

Polyspace Code Prover performs precisely this function. The Polyspace Code Prover run-time checks, Out of bounds array index and Illegally dereferenced pointer, look for potential buffer overflows along all execution paths for all combinations of inputs and other variables in your program. This means that in the example discussed earlier:

if (!(png_ptr->mode & PNG_HAVE_PLTE)) {
		/* Should be an error, but we can cope with it */
		png_warning(png_ptr, "Missing PLTE before tRNS");
	}
	else if (length > (png_uint_32)png_ptr->num_palette) {
		png_warning(png_ptr, "Incorrect tRNS chunk length");
		png_crc_finish(png_ptr, length);
		return;
	}
	...
	png_crc_read(png_ptr, readbuf, (png_size_t)length);

Polyspace Code Prover can identify that the variable length is not bounded along all paths. The corresponding Polyspace Bug Finder checkers, Array access out of bounds and Pointer access out of bounds, look for such issues, too. They report when at least one control path causing an overflow is found. However, these checkers do not perform an exhaustive analysis. You must use a code verification tool such as Polyspace Code Prover and ensure that all pointer accesses are green and are therefore never proven to result in a buffer overflow.

Such detailed analysis is ideal before code reviews and unit testing, as you can reduce and optimize your test cases based on the run-time data provided by Polyspace Code Prover. Thus, using both Polyspace Bug Finder and Polyspace Code Prover, you can ensure safety and security against the malignant threat of buffer overflows.

Other Real-World Examples