Anthropic design choice exposed 150M+ downloads, and 200K servers to complete takeover

From Features to Flaws: Understanding C/C++ and Their Unique Vulnerabilities

C++ and Their Unique Vulnerabilities
Share

C, a programming language created by Dennis Ritchie in 1972, is one of the most influential. It serves as the foundation for C++, Java, C#, and more. Many modern languages either use C syntax or are inspired by it, and essential software components such as compilers and operating system kernels (e.g., Linux) are written in C. It introduces core concepts like arrays, strings, functions, and file management that remain relevant today. C is also highly portable — C programs can run on multiple platforms with minimal changes. Due to its ability to convert well into machine code, it is frequently used in environments with limited resources. Additionally, C offers unmatched flexibility by enabling direct memory manipulation via pointers (but also bringing potential risks, as we will address in a second).

C is considered a middle-level language because it blends the low-level hardware control features of assembly with the higher-level abstractions of languages like Python or Java. Initially developed for system applications like operating systems and device drivers, C provides direct access to memory and hardware components, enabling precise control over resources. At the same time, it includes high-level constructs that make the code more readable and easier to manage. This combination of low-level efficiency and high-level structure makes C particularly effective for developing performance-critical applications and system software.

C++, created by Bjarne Stroustrup in 1979 as an extension of C, was initially called “C with Classes” and was renamed to C++ in 1983. It retained C’s strengths, like memory management, while introducing object-oriented features such as classes and objects. It also brought improved type safety, additional abstraction capabilities, and simpler code reuse and maintenance. Recent updates added modern capabilities, including smart pointers, lambda expressions, and more, making C++ a versatile choice for systems programming, real-time applications, and game development.

When Power Becomes a Problem: How C/C++ Features Can Backfire

The key strength of C and C++ is their direct memory management capabilities. Developers can control memory allocation, deallocation, and use, maximizing performance and enabling precise resource management — a crucial advantage for low-level systems programming, embedded systems, and performance-critical tasks. In contrast, languages like Python, Java, and C# handle memory automatically through garbage collection, simplifying development but limiting direct memory control. While this reduces complexity, it can be less suitable for resource-constrained or real-time applications.

Even though memory control is a very powerful feature in C and C++, it doesn’t come without drawbacks, and there is a good reason that garbage collection mechanisms were developed. Memory management has several inherent risks due to its manual and low-level nature. These risks often lead to hard-to-diagnose bugs, security vulnerabilities, and unstable applications if not handled very carefully. From resource exhaustion due to unfreed memory, through crashes or undefined behavior of the system, to overwriting existing data, poorly managed memory can cause a developer a lot of trouble. 

Common Software Vulnerabilities and How to Avoid Them

Format String Attack Example (CWE-134)

This vulnerability is basically “built-in” in C and C++ and can occur in the entire family of ‘printf’ functions (fprintf, sprintf, vprintf, etc.). These functions allow the developer to create a formatted string with a format specifier for various input types (%d for signed integers, %p for pointers, %c for chars, and more ). These format specifiers are then replaced by the values passed to the function after the string and concatenated to the full string. This makes the string dynamic and adjustable.

Because this function doesn’t enforce the use of these format specifiers and can accept the variables by themselves, the function will autonomously try to parse the whole string that was passed as a variable via user input, as a formatted string. This is problematic because the action can potentially cause a denial-of-service or allow the attacker to execute arbitrary commands and gain access to sensitive system information.

Let’s look at an application that’s vulnerable to a format string vulnerability. In the process of executing, this app saves a secret password, stores it somewhere in the stack, and then receives a user input, a name in this example, and prints a “hello” message back to the user with its name:

Now let’s play with it a little bit. Let’s start with the “expected” user behavior and see what output results:

image21

For the second run, let’s say I’m not a regular user, but a malicious attacker. To determine if there is a vulnerability, I will pass format specifiers to see how the system responds. Let’s try, for example, to pass a few %p format specifiers to see what happens:

image19

Each %p is interpreted by the printf() function as an instruction to fetch the next value from the stack, treat it as a pointer, and print it, typically as a hexadecimal address. 

These values reveal the contents of the stack, and by varying the number of %p specifiers, an attacker can probe different memory locations. This technique can leak return addresses, function pointers, and even sensitive data like passwords or cryptographic keys. 

That’s what we’re going to demonstrate next.

To exploit this vulnerability in the code, where uncontrolled %p specifiers leak stack memory, we need to understand where in the stack the password is stored, and whether it has been broken into pieces that must be put back together. My approach to testing was to brute force the stack and find the memory locations where the password is possibly stored. To demonstrate the simplicity of exploiting this attack and to prove that anyone can do this, I asked my “genius friend” ChatGPT to write a script that exploits my “very secure” C code. 

The payload the script sent includes the %<offset>$p specifier, which tells printf() to print a value from the stack as a pointer at a specific position. For example, %7$p means, “fetch the 7th argument on the stack and print it as a pointer.” 

By looping through offsets, the script walks the stack, which helps me look for memory addresses that might point to useful data. The extracted hexadecimal addresses are then passed to a function that attempts to decode them as ASCII strings and prints the result to the screen.

Let’s see the result of running this script:

Jackpot!! As you can see, my “very secret” password is exposed on my screen, even though we wouldn’t expect it to be, since it wasn’t included in the code

This simple example highlights just how dangerous format string vulnerabilities are. With nothing more than user-controlled input and a bit of stack inspection, it’s possible to expose sensitive data that was never meant to be accessible. While this example may seem trivial, the same techniques can be used on real-world applications and lead to much more dangerous results.

Mitigation:

  • When using any function that can get a formatted string, make sure to always use format specifiers and incorporate them in a constant format string, and pass user input as arguments:

Bad: printf(user_input);

Good: printf(“%s”, user_input);

  • Use safer versions of functions where available (e.g., snprintf instead of sprintf).
  • As a general best practice, whenever getting any input from a user, always validate and sanitize it to identify unexpected or malicious input.

Additional C/C++-Specific Vulnerabilities

Buffer Overflow (CWE-121)

A buffer overflow is a common and dangerous vulnerability in C and C++ that occurs when more data is written to a buffer than it can hold. Built-in functions in C and C++, like strcpy, gets, memcpy, and scanf, don’t check whether data fits within the allocated space. The absence of bounds checking shifts the responsibility for proper memory management to the developer, making buffer overflows a common cause of bugs and security risks.

Here is an example of a simple program that takes user input and stores it in a fixed-size buffer without validating the input size: 

In this case, an input larger than 10 characters will cause a buffer overflow. When control measures that check the size of data are not implemented, writing to the buffer can cause the data to overwrite adjacent memory locations. This can cause the program to crash or corrupt data in unpredictable ways.

In addition, a buffer overflow can let an attacker craft input that overwrites the return address of the current function on the stack. The attacker can then take over the program’s control flow by replacing the return address with the address of malicious code that’s under their control. The function then returns to the injected code to the attacker rather than the original caller of the function, which gives the attacker the ability to execute arbitrary commands, often with elevated privileges.

Mitigation:

  • Avoid unsafe functions like gets(), strcpy(), sprintf() that don’t perform checks on the buffer and input size. Use safer alternatives like fgets(), strncpy(), snprintf(), and memcpy_s()
  • Always validate the length of inputs and ensure buffers are large enough before writing to them.
  • As a general best practice, always validate and sanitize user input to guard against unexpected or malicious data.

Use-After-Free/Delete (CWE-416)

The memory management process requires developers to allocate the right amount of memory,  check the size of the data (to avoid overflows), and free memory when it’s no longer needed. Freeing the memory is critical for several reasons, and not doing so can lead to:

  • Unexpected behaviors
  • Memory leaks, resource exhaustion
  • Even more severe vulnerabilities like remote code execution (RCE)

“Use after free” is a type of memory error in software development that occurs when memory is referenced after it has been “freed,” which may result in undefined system behavior and can cause the program to crash. 

This example shows a pointer that has been assigned a value,  is then freed, but is called again:  

Mitigation:

  • After freeing memory with free() or delete, immediately set the pointer to NULL. This prevents accidental reuse and allows safer null checks before dereferencing.
  • In C++, prefer the use of smart pointers, which perform the whole memory management process for you and reduce the risk of use after free, double free, and dangling pointer vulnerabilities.

Double Free/Delete (CWE-415)

As with the “use-after-free/delete” vulnerability, putting the memory management process in the developer’s hands may present risks if not implemented properly. Another example of a memory management-related vulnerability is “double free.” As the name suggests, a double free vulnerability occurs when the program tries to free a memory location that has already been freed. As with the previous vulnerability, this can lead to various unexpected behaviors, memory leaks, resource exhaustion, and sometimes even more severe vulnerabilities like remote code execution.

Double-freeing memory can corrupt internal data structures and give an attacker the ability to replace function pointers or important program data with addresses that point to their own malicious code. Once corrupted pointers are dereferenced, if they are re-referenced, the program may jump to the attacker’s code.

To illustrate:

image22

Mitigation:

  • After freeing memory with free() or delete, immediately set the pointer to NULL. This prevents accidental reuse and allows safer null checks before dereferencing.
  • In C++, use of smart pointers, which perform the whole memory management process and reduce the risk of “use-after-free,” “double-free,” and dangling pointer vulnerabilities.

Null Pointer Dereference (CWE-467)

A “null dereference” occurs when a program tries to access a NULL pointer, leading to unpredictable behavior, crashes, or security vulnerabilities. In C and C++, NULL pointers are not automatically checked, which can cause program crashes or undefined behavior. These issues often result from uninitialized pointers or neglecting to check for NULL values. Null dereferences can lead to security risks like memory corruption or denial-of-service attacks. 

To prevent a null pointer dereference, developers should always check if a pointer is NULL before dereferencing it:

Mitigation:

  • Always validate pointers before use.
    • Add a check to see if a pointer is NULL before dereferencing it, especially when it comes from external input, dynamic memory allocation (like malloc or new), or function returns.

Good:

If (some_pointer != NULL){

*some_pointer = value;

}

  • Functions like malloc, calloc, and realloc can return NULL if an error occurred and they fail to run. Always verify the result before using the memory to avoid undefined behavior.

Integer Overflow/Underflow (CWE-190/CWE-191)

In C and C++, integer types have a fixed size, meaning they can only represent a limited range of values. When calculations exceed this range, two issues can arise: overflow and underflow. 

An overflow occurs when a value exceeds the maximum limit of the integer type, while underflow happens when a value falls below the minimum limit. For example, a 16-bit integer can range from −32,768 to 32,767, and assigning a value like 32,800 will cause an overflow. These issues can result from arithmetic operations or type conversions, such as casting between larger and smaller integer types or between signed and unsigned types. Integer overflows and underflows can lead to crashes, data corruption, and unpredictable behavior.


Here is an example of an overflow using the INT_MAX macro:

image9

Instead of increasing the number by 1 as intended, the number reached the integer type maximum and caused the number to “wrap,” meaning a positive number turned into a negative number due to bit manipulation (the reverse can also be true: turning a negative into a positive number). 

Attackers can exploit integer overflows/underflows in areas of the code where integer values are used to allocate memory or validate buffer sizes. If the program allocates a buffer using an unchecked integer (using malloc(unchecked_input_size)), an overflow could result in an allocation with a buffer size that is much smaller than intended. Memory corruption could result from the program writing unchecked_input_size bytes into the buffer later on, overflowing the memory and overwriting adjacent data.

Mitigation:

  • Perform explicit range checks before arithmetic operations.
    • Validate that inputs and results stay within safe bounds before performing arithmetic. This is especially critical when dealing with user input, buffer sizes, or loop counters.

Detect and Mitigate C/C++ Vulnerabilities with OX 

Although C and C++ have flaws, businesses can preserve application security while utilizing the programming languages’ best practices by finding and fixing vulnerabilities. The OX Security Unified AppSec Platform provides complete coverage over the top C/C++ risks and vulnerabilities, assuring that users get unrivaled visibility and manageability of potential flaws before they reach production. 

In addition to code scanning (SAST, IAST, secrets, IaC, and more), OX offers end-to-end traceability over the software development lifecycle, from design to runtime, so you can automatically identify and address threats in your application builds before they develop into costly, time-consuming issues

Tags:

post banner image

Run Every Security Test Your Code Needs

Pinpoint, investigate and eliminate code-level issues across the entire SDLC.

GET A PERSONALIZED DEMO
Frame 2085668530

Subscribe to Our Newsletter

Stay updated with the latest SaaS insights, tips, and news delivered straight to your inbox.

Security Starts at the Source