Memory Safety: 5 Common Memory Bugs and How to Secure Your Systems

8  min read | 06/05/2024

Lian Granot
Lian Granot
Hadas Spektor
Hadas Spektor

What Is Memory Safety? 

Memory safety refers to the protection of memory space from unauthorized access and manipulation. Memory safety involves controlling the way that software accesses and manipulates memory to ensure it does not lead to corruption, leaks, or unauthorized access to other parts of memory.

Critical for software reliability and security, memory safety helps in preventing a range of software vulnerabilities that can lead to crashes, data corruption, or security breaches. By adhering to memory safety principles, developers can reduce the risk of memory risks in their software.

It is important to realize that some programming languages are memory safe by design. These include Rust, Java, Go, and Python.

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

Common Memory Bugs and Their Impact 

Here are some examples of common security flaws in memory.

1. Use After Free Vulnerabilities

Use After Free (UAF) vulnerabilities occur when an application attempts to access memory after it has been freed or deallocated. This can lead to unpredictable behavior, including the execution of arbitrary code, application crashes, and data corruption. 

Attackers can exploit UAF vulnerabilities by manipulating the freed memory and injecting malicious code that gets executed by the dangling pointer. These vulnerabilities are particularly dangerous because they can provide an attacker with a way to execute code within the context of the application, leading to system compromise.

2. Out-of-Bounds Reads

Out-of-Bounds (OOB) Reads happen when a program attempts to read data from a memory location outside the bounds of the allocated memory. This can result in the exposure of sensitive information, or crashes due to accessing illegal memory areas. 

Attackers can exploit these vulnerabilities to read sensitive information from the process’s memory space, which may lead to information disclosure vulnerabilities or aid in further exploitation techniques.

3. Out of Bounds Writes

Out-of-bounds (OOB) writes occur when a program tries to write data outside the bounds of allocated memory. This can corrupt or overwrite adjacent memory areas, potentially leading to undefined behavior, application crashes, or data corruption. In some cases, attackers can exploit these vulnerabilities to overwrite sensitive data or control structures, which can result in arbitrary code execution or privilege escalation.

Preventing OOB writes requires rigorous bounds checking and proper use of safe data structures. Developers can minimize these vulnerabilities by avoiding the use of raw arrays and pointers in non-memory-safe languages, opting instead for safer abstractions like vectors or containers with built-in boundary checks.

4. Null Pointer Dereferencing

Null Pointer Dereferencing occurs when a program attempts to read or write to a memory location pointed to by a null pointer, leading to crashes or denial of service (DoS) conditions. 

While this may not directly lead to arbitrary code execution, it can be used in conjunction with other vulnerabilities to bypass security mechanisms or cause application instability, thereby impacting the application’s availability.

5. Integer Overflow or Wraparound

Integer Overflow or Wraparound happens when an arithmetic operation results in a number larger than the maximum value the data type can hold, causing the value to wrap around to a smaller, unexpected value. 

This condition can lead to buffer overflows, memory corruption, and potentially arbitrary code execution. Attackers can exploit these vulnerabilities to manipulate the memory layout of an application, overwrite critical data, or execute malicious code.

Which Programming Languages Are Inherently Memory-Safe? 

There are several programming languages that are designed to avoid memory safety issues. Software based on these languages is, in principle, memory safe by default.

Rust

Rust is a relatively new programming language offering memory safety guarantees through its ownership model, without sacrificing performance. This model manages memory lifecycle and concurrency to prevent common memory bugs. Rust’s approach to memory safety, combined with its concurrency management, makes it suitable for system-level programming.

Java 

Java is designed to be a memory-safe programming language by implementing a garbage collection mechanism that automatically manages memory allocation and deallocation. This approach helps in preventing memory leaks and use-after-free vulnerabilities, common issues in languages that require manual memory management. Furthermore, Java’s type system and runtime checks help prevent buffer overflows and array index out-of-bounds errors, making it a safer choice for developers concerned about memory safety issues.

Python 

Python, an interpreted, high-level programming language, emphasizes simplicity and readability, which extends to its approach to memory management. Python’s automatic memory management and garbage collection features help prevent many common memory safety issues such as memory leaks and use-after-free errors. Additionally, Python’s dynamic typing and bounds checking on data structures like lists and strings reduce the risk of buffer overflows and out-of-bounds access, contributing to its reputation as a memory-safe language.

Go

Go, also known as Golang, is a statically typed, compiled programming language designed for simplicity and efficiency. It includes built-in concurrency and robust standard libraries. Go’s memory safety is achieved through its garbage collector, which reclaims unused memory, and its strict type system, which prevents unintended memory access. Additionally, Go’s approach to error handling and its design to avoid common pitfalls of C programming, such as pointer arithmetic, further enhance its memory safety credentials.

Swift

Swift, developed by Apple, focuses on memory safety by automatically managing memory usage and eliminating common bugs like null pointer dereferences. Its safety features are balanced with modern syntax and performance. Swift’s memory management techniques, such as automatic reference counting, help prevent leaks and ensure safe memory access.

Ada

Ada is designed with strong typing, modularity, and run-time checking features that bolster its memory safety capabilities. It’s widely used in systems that prioritize reliability and safety. By enforcing strict compilation rules and run-time checks, Ada reduces the likelihood of memory safety errors, making it suitable for high-integrity and real-time applications.

SPARK

SPARK, a formally-defined programming language based on Ada, emphasizes verification of software properties, including memory safety. Its use of formal methods allows for proving the absence of certain types of errors. Applying SPARK in software enables developers to build highly reliable applications, with guarantees on memory safety contributing to system integrity.

Which Languages Are Not Memory-Safe? 

When using these languages, it is important to consider memory safety and implement additional security measures.

C

C is a versatile programming language that has been the backbone of many operating systems, embedded systems, and applications requiring close-to-hardware manipulation. However, it is not considered memory-safe due to its need for manual memory management and its allowance for direct memory access. This flexibility gives programmers a high degree of control but also places the responsibility for memory safety directly in their hands. 

C language features, such as pointer arithmetic, direct memory access, and manual allocation and deallocation of memory, can lead to vulnerabilities like buffer overflows, use-after-free errors, and memory leaks if not carefully managed.

C++

C++ builds on C with added features like object-oriented programming, templates, and exceptions, but it inherits C’s lack of memory safety. While it offers smart pointers and containers that can help manage memory more safely, the language still allows low-level memory manipulation, which can lead to memory safety issues. 

Developers must be vigilant in managing memory, especially when using raw pointers and direct memory operations. C++’s complexity and power come with the risk of memory-related errors, making it challenging to ensure memory safety without disciplined coding practices and extensive testing.

Challenges of Ensuring Memory Safety 

There are several conditions that may make it more challenging to ensure the memory space is safe.

Legacy Code

Legacy code often lacks adherence to modern memory safety standards, and might be coded in non-memory-safe languages, making it harder to maintain and update. Addressing memory safety in legacy systems requires thorough audits, introducing automated testing, and progressively refactoring critical components with safety in mind. 

Economic and Operational Costs

Achieving memory safety can introduce economic and operational costs, including redesigning systems, training developers, and increased computation resources. These factors often deter organizations from prioritizing memory safety. It’s important to balance these costs with the potential risks and consequences of memory vulnerabilities.

Architectural Limitations

Certain system architectures and programming models may not readily support memory-safe practices, making it challenging to ensure safety. For example, languages that provide low-level memory access without built-in safeguards can introduce risks.

Overcoming these limitations often involves using abstraction layers or switching to programming languages designed with memory safety in mind. Leveraging compiler and runtime safety features can provide added layers of protection.

Best Practices to Overcome Memory Bugs in Non-Memory-Safe Languages 

Here are some of the measures that can help improve memory safety when using a non-memory-safe language like C or C++.

Use Fuzzers and Sanitizers

Fuzzing is a dynamic code analysis technique that involves automatically feeding a program with a large amount of random data (“fuzz”) to find vulnerabilities and bugs that could lead to crashes, buffer overflows, or memory leaks. Fuzzers are particularly effective in non-memory-safe languages as they can uncover the unpredictable behavior caused by unsafe memory operations. 

Sanitizers are tools integrated into compilers like GCC and Clang, designed to detect various memory errors such as use-after-free, memory leaks, and buffer overflows at runtime. Utilizing fuzzers and sanitizers in the development cycle can significantly reduce the incidence of memory bugs by identifying and allowing developers to fix them early in the software development lifecycle.

Use Static Application Security Testing (SAST) and Dynamic Application Security Testing (DAST) 

SAST and DAST are two important security testing methodologies that can help identify memory safety issues in non-memory-safe languages. SAST involves scanning the source code, bytecode, or binary code of applications for potential vulnerabilities before the software is run. This static analysis identifies problematic code patterns like buffer overflows, use-after-free errors, and integer overflows. SAST is useful for finding vulnerabilities early in the development cycle and helps developers fix issues before deployment.

DAST involves testing a running application by simulating real-world attacks to identify vulnerabilities that could lead to memory safety issues. By interacting with the application as an end user would, DAST tools can uncover flaws like race conditions or input validation errors that are not apparent through static analysis. Combining SAST and DAST allows developers to address security gaps both in code structure and in application behavior.

Use Modern Programming Idioms

In non-memory-safe languages like C and C++, adopting modern programming idioms and libraries can greatly enhance memory safety. For instance, using smart pointers in C++ (such as std::unique_ptr and std::shared_ptr) automates memory management, reducing the risk of memory leaks and dangling pointers. Containers like std::vector and std::array offer safer alternatives to raw array manipulation, guarding against buffer overflows. 

Leveraging these modern features requires staying updated with the latest language standards and practices. Encapsulating complex memory management logic into well-tested classes and functions can also abstract away many of the low-level details, making code safer and more maintainable.

Implement Exploit Mitigation Techniques 

Address Space Layout Randomization (ASLR) and Data Execution Prevention (DEP) are exploit mitigation techniques that can enhance security in applications written in non-memory-safe languages. While these techniques do not eliminate the vulnerabilities themselves, they significantly increase the difficulty of exploiting them, providing an additional layer of defense.

ASLR randomizes the memory addresses used by a program each time it runs, making it more difficult for attackers to predict the location of specific instructions or data. DEP prevents code from being executed in certain regions of memory, notably those designated for data storage, thwarting attempts to run malicious code through exploits like buffer overflows. 

Separate Privileges

Separating privileges within an application can limit the impact of a memory safety vulnerability by restricting the actions that can be performed through exploitation. This principle involves designing software components and processes to operate with the minimum set of privileges necessary for their functionality. For example, a process that does not need to write files should not run with permissions that allow file writing. 

Applying this practice in programs written in C and C++ can mitigate the consequences of memory bugs by preventing attackers from gaining higher privileges or accessing sensitive parts of the system. This approach, often part of a broader security strategy known as the principle of least privilege, adds an additional security layer.

Memory Protection for IoT Devices with Sternum

The vast majority of IoT/embedded devices use C code, and are prone to memory corruption and other operational and security vulnerabilities.
Sternum’s patented EIV™ software technologies protect from these with runtime (RASP-like) protection that deterministically prevents memory and code manipulation attempts, offering blanket protection from a broad range software weaknesses (CWEs)—including 415 and 416.

Embedding itself directly in the firmware code, EIV™ software technologies are 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 software technologies’ 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 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

JUMP TO SECTION

Enter data to download case study

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