X
BLOG

Battling Buffer Overflows & Other Memory Management Bugs

Battling Buffer Overflows & Other Memory Management Bugs Reading Time: 9 minutes

Memory management is fraught with danger, especially in C and C++. In fact, bugs associated with memory management weaknesses make up a sizeable part of the CWE Top 25. Eight of the top 25 are directly related to buffer overflows, poor pointers, and memory management.

The top software weakness by a large margin is CWE-119, “Improper Restriction of Operations within the Bounds of a Memory Buffer”. These types of errors figure prominently in safety and security issues in all sorts of software, including safety-critical applications in automobiles, medical devices, and avionics.

Here are the memory errors related to common weakness enumerations from the CWE Top 25:

RankIDNameScore  
[1]CWE-119Improper Restriction of Operations within the Bounds of a Memory Buffer75.56
[5]CWE-125Out-of-bounds Read26.53
[7]CWE-416Use After Free17.94
[8]CWE-190Integer Overflow or Wraparound17.35
[12]CWE-787Out-of-bounds Write11.08
[14]CWE-476NULL Pointer Dereference9.74
[20]CWE-400Uncontrolled Resource Consumption5.04
[21]CWE-772Missing Release of Resource after Effective Lifetime5.04

Although these errors have plagued C, C++, and other languages for decades, they still occur with increasing numbers today. They’re dangerous bugs in terms of consequences to quality, safety, and reliability, and their presence is a leading cause of security vulnerabilities.

Leading Cause of Security Vulnerabilities

Microsoft discovered that for the last 12 years, over 70% of the security vulnerabilities in their products were due to memory safety issues. These types of bugs are the largest attack surface for their application and hackers are using it. According to their research, the top root causes of security attacks were heap out-of-bounds, use-after-free, and uninitialized use. As they point out, the vulnerability classes have existed for 20 years or more and are still prevalent today.

In a similar way, Google found that 70% of the security vulnerabilities in the Chromium project (the open source base for the Chrome browser) are due to these same memory management issues. Their top root cause was also use-after-free with other unsafe memory management coming in second.

Given these examples of real-world findings, it’s critical that software teams take these types of errors seriously. Luckily, there are ways to prevent and detect these types of issues with static analysis that is effective and efficient.

How Memory Management Errors Turn Into Security Vulnerabilities

In most cases, memory management errors are the result of poor programming practices with the use of pointers in C/C++ and accessing memory directly. In other cases, it’s related to making poor assumptions about the length and content of data.

These software weaknesses are most often exploited with tainted data, data from outside the application that hasn’t been checked for length or format. The infamous Heartbleed vulnerability is a case of exploiting a buffer overflow. Technically, it’s a buffer overread. As we discussed in our previous blog about SQL injections, the use of input that’s unchecked and unconstrained is a security risk.

Let’s consider some of the major categories of memory management software weaknesses. The overarching one is CWE-119: Improper Restriction of Operations Within the Bounds of a Memory Buffer.

Buffer Overflow

Programming languages (most often C and C++) that allow direct access to memory and don’t automatically verify the locations accessed are valid and prone to memory corruption errors. This corruption can occur in data and code areas of memory, which can expose sensitive information, lead to unintended code execution, or cause an application to crash.

The following example shows a classic case of a buffer overflow from CWE-120:

char last_name[20];
printf ("Enter your last name: ");
scanf ("%s", last_name);

In this case, there is no restriction on the user input from scanf() yet the limit on the length of last_name is 20 characters. Entering a last name of more than 20 characters ends up copying the user input into memory beyond the limits of the buffer last_name. Here’s a more subtle example from CWE-119:

void host_lookup(char *user_supplied_addr){
struct hostent *hp;
in_addr_t *addr;
char hostname[64];
in_addr_t inet_addr(const char *cp);

/*routine that ensures user_supplied_addr is in the right format for conversion */

validate_addr_form(user_supplied_addr);
addr = inet_addr(user_supplied_addr);
hp = gethostbyaddr( addr, sizeof(struct in_addr), AF_INET);
strcpy(hostname, hp->h_name);
}

This function takes a user-supplied string containing an IP address (for example, 127.0.0.1) and retrieves the hostname for it.

The function validates the user input (good!) but doesn’t check the output of gethostbyaddr()(bad!). In this case, a long hostname is enough to overflow the hostname buffer that’s currently limited to 64 characters. Note that if gethostaddr() returns a null when a hostname can’t be found, there’s also a null pointer dereference error as well!

Use-After-Free Errors

Interestingly, Microsoft in their study, observed that use-after-free errors were the most common memory management issues they faced. As the name implies, the error relates to the use of pointers (in the case of C/C++) that access previously freed memory. C and C++ usually rely on the developer to manage memory allocation, which can often be tricky to do completely correctly. As the following example (from CWE-416) shows, it’s often easy to assume a pointer is still valid:

char* ptr = (char*)malloc (SIZE);
if (err) {
abrt = 1;
free(ptr);
}
...
if (abrt) {
logError("operation aborted before commit", ptr);
}

In the above example, the pointer ptr is free if an err is true but then is dereferenced later, after being freed, if abrt is true (which is set to true, if err is true). This might seem contrived, but if there is a lot of code between these two code snippets, it’s easy to overlook. In addition, this might only occur in an error condition that doesn’t get properly tested.

NULL Pointer Dereference

Another common software weakness is using pointers (or objects in C++ and Java) that are expected to be valid but are NULL. Although these dereferences are caught as exceptions in languages like Java, they can cause an application to halt, exit, or crash. Take the following example, in Java, from CWE-476:

String cmd = System.getProperty("cmd");
cmd = cmd.trim();

This looks innocuous since the developer might assume that the getProperty() method always returns something. In fact, if the property “cmd” doesn’t exist, a NULL is returned causing a NULL dereference exception when it’s used. Although this sounds benign, it can lead to disastrous outcomes.

In rare circumstances, when NULL is equivalent to the 0x0 memory address and privileged code can access it, then writing or reading memory is possible, which may lead to code execution.

Mitigations

There are several mitigations available that developers should implement. Primarily, developers need to ensure pointers are valid for languages like C and C++ with verified logic and thorough checking.

For all languages, it’s imperative that any code or libraries that manipulate memory validate input parameters to prevent out-of-bounds access. Below are some mitigating options that are available. But developers shouldn’t rely on them to make up for poor programming practices.

Programming Language Choice

Some languages provide built-in protection against overflows such as Ada and C#.

Use of Safe Libraries

Using libraries, like the Safe C String Library, that provide built-in checks to prevent memory errors is available. However, not all buffer overflows are the result of string manipulation. Barring this, programmers should always resort to functions that take the length of buffers as arguments, for example, strncpy() versus strcpy().

Compilation and Runtime Hardening

This approach makes use of compilation options that add code to the application to monitor pointer usages. This added code can prevent overflow errors from occurring at runtime.

Execution Environment Hardening

Operating systems have options to prevent execution of code in data areas of an application, such as a stack overflow with code injection. There are also options to randomly arrange the memory mapping to prevent hackers from predicting where exploitable code may reside.

Despite these mitigations, there is no replacement for proper coding practices to avoid buffer overflows in the first place. Therefore, detection and prevention are critical to reducing the risks of these software weaknesses.

Shift the Detection and Elimination of Buffer Overflows

Adopting a DevSecOps approach to software development means integrating security into all aspects of the DevOps pipeline. Just as quality processes like code analysis and unit testing are pushed as early as possible in SDLC, the same is true for security.

Buffer overflows and other memory management errors could be a thing of the past if development teams adopted such an approach more broadly. As Google and Microsoft’s research shows, these errors still make up 70% of their security vulnerabilities. Regardless, let’s outline an approach that prevents them as early as possible.

Finding and fixing memory management errors pays off big time compared to patching a released application. The detect and prevent approach outlined below is based on shifting left the mitigation of buffer overflows to the earliest stages of development. And reinforcing this with detection via static code analysis.

Detection

Detecting memory management errors relies on static analysis to find these types of vulnerabilities in the source code. Detection happens at the developer’s desktop and in the build system. It can include existing, legacy, and third-party code.

Detecting security issues on a continuous basis ensures finding any issues that:

  • Developers missed in the IDE.
  • Exist in code that predates your new detect-and-prevent approach.

The recommended approach is a trust-but-verify model. Security analysis is done at the IDE level where developers make real-time decisions based on the reports they get. Next, verify at the build level. Ideally, the goal at build level isn’t to find vulnerabilities. It’s to verify that the system is clean.

Parasoft C/C++test includes static analysis checkers for these types of memory management errors, including buffer overflows. Consider the following example taken from C/C++ test.

Parasoft C/C++test Example: Detection of Out-of-Bounds Memory Access

Figure xx: Parasoft C/C++test example showing the detection of an out-of-bounds memory access.

Zooming in on the details, the function printMessage() error detects the error:

Parasoft-C/C++test Example of Error

Parasoft C/C++test also provides trace information on how the tool arrived at this warning:

Parasoft C/C++test Example of Trace Info

The sidebar shows details about how to repair this vulnerability along with appropriate references:

ParasoftC/C++test Example of Repair

Accurate detection along with supporting information and remediation recommendations are critical for making static analysis and early detection of these vulnerabilities useful and immediately actionable for developers.

Preventing Buffer Overflows and Other Memory Management Errors

The ideal time and place to prevent buffer overflows is when developers are writing code in their IDE. Teams that are adopting secure coding standards such as SEI CERT C for C and C++ and OWASP Top 10 for Java and .NET or CWE Top 25, all have guidelines that warn about memory management errors.

For example, CERT C includes the following recommendations for memory management:

CERT C REC 08 Memory Management (MEM) List

These recommendations include preventative coding techniques that avoid memory management errors in the first place. Each set of recommendations includes a risk assessment along with remediation costs, allowing software teams to prioritize the guidelines as follows:

CERT C REC 08 Memory Management (MEM) table

A key prevention strategy is to adopt a coding standard adapted from industry guidelines like SEI CERT and enforce it on future coding. Prevention of these vulnerabilities with better coding practices is cheaper, less risky, and has the highest return on investment.

Running static analysis of the newly created code is fast and simple. It’s easy for teams to integrate both on the desktop IDE and into the CI/CD process. To prevent this code from ever making it into the build, it’s good practice to investigate any security warnings and unsafe coding practices at this stage.

Parasoft C/C++test Example of Security Warnings

Screenshot of Parasoft integration into the Eclipse IDE which brings prevention and detection of vulnerabilities to the developer’s desktop.

An equally important part of detecting poor coding practices is how useful the reports are. It’s important to understand the root cause of static analysis violations in order to fix them quickly and efficiently. This is where commercial tools such as Parasoft’s C/C++test, dotTEST, and Jtest shine.

Parasoft’s automated testing tools give full traces for warnings, illustrate these within the IDE, and collect build and other information on a continuous basis. This collected data alongside test results and metrics provides a comprehensive view of compliance with the team’s coding standard along with general quality and security status.

Developers can further filter findings based on other contextual information such as metadata on the project, the age of the code, and the developer or team responsible for the code. Tools like Parasoft with artificial intelligence (AI) and machine learning (ML) use this information to help further determine the most critical issues.

The dashboards and reports include the risk models that are part of the information provided by OWASP, CERT, and CWE. That way, developers better understand the impact of the potential vulnerabilities reported by the tool and which of these vulnerabilities to prioritize. All the data generated at the IDE level is correlated with downstream activities outlined above.

Summary

The buffer overflows and other memory management errors continue to plague applications. They remain a leading cause of security vulnerabilities. Despite knowledge of how it works and is exploited, it remains prevalent. See the IoT Hall of Shame for recent examples.

We propose a prevent and detect approach to compliment active security testing that prevents buffer overflows before they are written into the code as early as possible in the SDLC. Preventing such memory management errors at the IDE and detecting them in the CI/CD pipeline is key to routing them out of your software.

Smart software teams can minimize memory management errors. They can make an impact on quality and security with the right process, tools, and automation in their existing workflows.

See Parasoft in Action

Written by

Arthur Hicken

Arthur has been involved in software security and test automation at Parasoft for over 25 years, helping research new methods and techniques (including 5 patents) while helping clients improve their software practices.

Get the latest software testing news and resources delivered to your inbox.

Try Parasoft