Memory Corruption: Examples, Impact, and 4 Ways to Prevent It

Lian Granot
Lian Granot

7  min read | min read | 23/05/2024

What Are Memory Corruption Vulnerabilities? 

Memory corruption vulnerabilities occur when a flaw in software leads to the modification of memory in unintended ways, potentially causing unexpected behavior or providing avenues for exploitation. These vulnerabilities can be enabled by various execution errors or coding mistakes, such as improper input validation, incorrect memory allocation, or failure in memory management operations. They can result in  buffer overflows, use-after-free errors, double-free errors, and memory leaks.

The root cause of memory corruption often lies in the software’s inability to correctly manage memory operations. For instance, when a program writes more data to a buffer than it can hold (buffer overflow), it ends up overwriting adjacent memory. Similarly, accessing memory after it has been freed (use-after-free) or freeing the same memory space more than once (double-free) leads to corruption. 

These issues primarily arise due to the complexity of manual memory management in languages like C and C++, and the challenges associated with ensuring that all memory operations are safe. Developers must vigilantly check their code for these vulnerabilities, as exploiting them can lead to severe security breaches, crashes, and data leakage.

This is part of a series of articles about operating system security.

 

The Impact of Memory Corruption 

The impacts of memory corruption vulnerabilities can be severe, including system compromise, data theft, and unauthorized access. In the worst-case scenario, attackers exploiting these vulnerabilities can gain control over a system, enabling them to execute arbitrary code with the permissions of the application affected by the memory corruption.

Memory corruption vulnerabilities can also undermine the reliability and stability of software, leading to unexpected crashes and undetermined behavior. These issues can damage user trust, result in operational and reputational damage for organizations and even endanger people’s lives when impacting critical industrial or medical systems. 

 

Types of Memory Corruption Vulnerabilities with Examples 

Here are some examples of memory corruption vulnerabilities.

Warning: Please avoid executing these code examples, except in a controlled environment, because they can crash your system.

Buffer Overflows

Buffer overflows occur when data exceeds its allocated memory space, overwriting adjacent memory. This often results from inadequate input validation, allowing attackers to inject malicious code or manipulate the program’s control flow. The infamous Morris Worm and other instances of shellcode execution exploited buffer overflow vulnerabilities. RIPPLE20 and INFRA:HALT are other examples.

These vulnerabilities are particularly dangerous because they can be used to alter a program’s execution path, leading to arbitrary code execution.

Code example:

#include <stdio.h>

#include <string.h>

void vulnerableFunction(char *str) {

    char buffer[10];

    // The following line introduces a buffer overflow vulnerability.

    strcpy(buffer, str);

}

int main() {

    // Input string larger than the allocated buffer size.

    char largeInput[] = “TooLongInputDataExceedingBuffer”;

    vulnerableFunction(largeInput);

    return 0;

}

In the example above, vulnerableFunction contains a buffer overflow vulnerability due to the use of strcpy without bounds checking. If an attacker can control the input (str), they could potentially overwrite adjacent memory, leading to arbitrary code execution or program crash.

Use-After-Free Vulnerabilities (Dangling Pointer)

Use-after-free vulnerabilities occur when a program attempts to use memory after it has been freed, leading to undefined behavior. Attackers exploit these vulnerabilities to execute arbitrary code or corrupt data by manipulating the freed memory before it’s reused.

Code example:

#include <stdio.h>

#include <stdlib.h>

typedef struct {

    int importantData;

    void (*funcPtr)();

} Resource;

void maliciousCode() {

    printf(“Exploited!\n”);

    // Insert any malicious code here

}

int main() {

    Resource *res = (Resource*)malloc(sizeof(Resource));

    res->importantData = 42;

    res->funcPtr = NULL;

    // Free the memory

    free(res);

    // Overwrite the function pointer with the address of maliciousCode

    res->funcPtr = maliciousCode;

    // Trigger the use-after-free vulnerability by
    // calling the function pointer

    res->funcPtr();

    return 0;

}

In this example, use-after-free occurs when a freed memory location is accessed. If an attacker can manipulate the memory allocation patterns, they could exploit this vulnerability to execute arbitrary code or manipulate program data.

Double-Free

Double-free errors occur when a program mistakenly frees the same memory region more than once, leading to memory corruption or crashes. They often result from logic errors in the program’s flow and can be exploited to perform malicious actions, similar to use-after-free vulnerabilities.

Code example:

#include <stdlib.h>

int main() {

    char *ptr = (char*) malloc(10 * sizeof(char));

    // First free

    free(ptr); 

    // ptr is now dangling, but not set to NULL.

    // Second free on the same pointer, leading to double-free.

    free(ptr); 

    return 0;

}

The above code demonstrates a double-free vulnerability. Freeing a pointer more than once can lead to memory corruption, crashes, or further exploitation, especially if an attacker can influence the program to allocate and free memory in a specific sequence.

Memory Leaks

Memory leaks happen when a program fails to release unused memory, leading to the gradual consumption of all available memory resources. While not directly exploitable for arbitrary code execution, memory leaks can facilitate other types of attacks by weakening the system’s stability and performance.

Code example:

#include <stdlib.h>

void leakMemory() {

    char *ptr = (char*)malloc(10 * sizeof(char)); // Memory is allocated

    // some application logic executes here using the allocated memory

    // Missing free() call when memory is no longer needed, leading to memory leak

}

int main() {

    for(int i = 0; i < 1000; i++) {

        leakMemory();

    }

    // The allocated memory in leakMemory is never freed.

    return 0;

}

 

Common Consequences of Corruption in Memory

Here are some common issues that may result from memory corruption.

Undefined Behavior

Undefined behavior occurs when the program operates outside its specification, leading to unpredictable results. This often stems from memory corruption vulnerabilities, allowing malicious actors to exploit these behaviors for unintended outcomes. Addressing undefined behavior requires strict adherence to language and software libraries specifications and rigorous testing.

Security Vulnerabilities

Memory corruption introduces significant security vulnerabilities, ranging from data leakage to remote code execution. These vulnerabilities serve as entry points for attackers to compromise systems or steal sensitive information. Understanding and addressing the root causes of memory corruption is essential for enhancing software security.

Program Crashes and Instability

Memory corruption often results in program crashes and instability. These issues stem from the unpredictable state of corrupted memory, leading to unhandled exceptions and errors. Ensuring program stability requires diligent testing and the adoption of defensive programming practices.

Data Corruption

Data corruption results when memory corruption leads to the alteration or destruction of data, compromising data integrity. This can be extremely serious, leading to the loss of critical information, system functionality and even device compromise when security or system parameters are modified. Beyond immediate data corruption, it could affect operational and decision-making capabilities. Preventive measures include implementing data validation and integrity checks.

 

4 Ways to Mitigate Memory Corruption Vulnerabilities 

Here are some of the measures that can help reduce the risk of memory corruption.

1. Enforce Secure Development and Testing Standards

Adopting secure development practices is crucial in mitigating memory corruption vulnerabilities. This involves integrating security at every stage of the software development life cycle (SDLC) to identify and resolve vulnerabilities early. Regular security training for developers can enhance awareness and understanding of common vulnerabilities and their mitigations. 

Employing static and dynamic analysis and testing tools can automate the detection of security flaws, including memory corruption issues, during the coding and testing phases. Code reviews, where peers examine the source code for potential vulnerabilities, also play a vital role in securing applications against memory corruption.

In addition to secure coding practices, rigorous testing is essential for uncovering and mitigating memory corruption vulnerabilities before software release. Fuzz testing, in particular, is effective in detecting buffer overflows, use-after-free vulnerabilities, and other memory corruption issues by feeding unexpected or random data to applications and monitoring for crashes or misbehaviors.

2. Ensure Memory Isolation

Memory isolation techniques are critical in preventing attackers from exploiting memory corruption vulnerabilities to affect other parts of a program or system. Implementing hardware-based security features, such as non-executable (NX) memory pages, can prevent the execution of arbitrary code in certain areas of memory, reducing the risk of exploits. 

Similarly, Address Space Layout Randomization (ASLR) makes it more difficult for attackers to predict the location of specific processes and data in memory, complicating the exploitation of memory corruption vulnerabilities.

Software-based isolation, such as sandboxing, confines code execution to isolated environments, limiting the potential impact of a security breach. By restricting access to system resources and sensitive data, sandboxing helps contain the damage that can be caused by exploited vulnerabilities.

3. Use Memory-Safe Programming Languages

Memory-safe programming languages, such as Rust and Go, are designed to prevent common memory corruption issues like buffer overflows and use-after-free vulnerabilities. These languages achieve memory safety through features like automatic memory management, bounds checking, and ownership models, which eliminate the need for manual memory management and reduce the risk of programmer errors leading to vulnerabilities. 

Adopting memory-safe languages for new projects or gradually transitioning critical components of existing systems to these languages can significantly reduce the attack surface related to memory corruption.

While transitioning to memory-safe languages may not be feasible for all projects, especially legacy systems or resource-constrained IoT systems, incorporating elements of these languages for new components or services can offer incremental improvements in security.

4. Apply Modern C++ Idioms

Modern C++ offers several features and ‘idioms’ (language-specific coding techniques) that promote safer memory management practices and reduce the likelihood of memory corruption vulnerabilities. 

For example, smart pointers such as std::unique_ptr and std::shared_ptr, automatically manage memory, preventing leaks and double-free errors. The use of containers like std::vector and algorithms from the Standard Template Library (STL) eliminates the need for manual memory management and reduces the risk of buffer overflows.

Applying the principle of ‘resource acquisition is initialization’ (RAII) ensures that resources are properly released when they are no longer needed, preventing resource leaks. Modern C++ also encourages the use of strong type checking and explicit casting to minimize errors that could lead to memory corruption. Adopting these modern C++ idioms and practices can make C++ code safer, more maintainable, and less prone to memory corruption vulnerabilities.

5. Prevent Memory Corruption Errors at Runtime with Sternum

The vast majority of IoT/embedded devices use C code, and are prone to memory corruptions and other operational and security vulnerabilities.

Sternum’s patented EIV™ technology protects from these with runtime protection that deterministically prevents all memory and code manipulation attempts, offering blanket protection, even for zero days, from a broad range software weaknesses (CWEs)—including for example CWE-787 and CWE-120  with zero false positive.

Embedding itself directly in the firmware code, EIV™ is agentless and connection agnostic. Operating at the bytecode level, it is also universally compatible with any relevant device and operating system (RTOS and Linux, including OpenWrt, Zephyr, Micrium, FreeRTOS, etc.) and has low overhead of only 1-3%, even on legacy devices. 

Moreover, EIV’s runtime protection features are augmented by (XDR-like) threat detection capabilities of Sternum’s Cloud platform and extended observability features.

The benefits of using Sternum for IoT security include:

  • Agentless security—integrates directly into firmware, making it a part of the core build. This ensures that the solution cannot be externally compromised and leveraged as a point of failure.
  • Automatic mitigation of known and zero-day threats—prevents 96.5% attacks in benchmark (RIPE) security tests. Its vulnerability-agnostic approach makes it equally effective in dealing with known and zero-day threats. This not only improves security but can also cut security patch management costs by as much as 60%.
  • Supply chain protection—relies on binary instrumentation, making it able to protect all running code. This extends to 3rd party and operating system libraries, effectively preventing all supply chain exploit attempts. 
  • Protection of isolated devices—does not rely on external communication to secure devices, making it equally effective for connected and isolated devices.
  • Live attack information with zero false positives—real-time alert system notifies about all blocked attacks, providing—for each—detailed logs and attack path analysis. The deterministic nature of EIV’s integrity checks ensures that all alerts are always valid. 
  • Advanced threat intelligence—leveraging AI anomaly detection and granular device-level understanding of user interactions, user activity, and device behavior. Can automatically spot indicators of exposure and compromise, malicious behavior, security blindspots, stealthy and sophisticated threats
  • Streamlined compliance—helps meet the latest cyber regulations for IoT devices (IEC 62443, FDA, NIST, etc) and the most current FBI recommendations for Internet of Medical Things (IoMT) endpoint protection.

 

Learn more about Sternum for IoT security and how we help protect against memory leaks? Schedule a demo: https://www.sternumiot.com/request-demo

JUMP TO SECTION

Enter data to download case study

By submitting this form, you agree to our Privacy Policy.