dealing with vulnerabilities

NSA urges shift to memory safe programming languages

Neal Ziring, a Technical Director at the National Security Agency (NSA), has been dropping some truth bombs:

“Memory management issues have been exploited for decades and are still entirely too common today,”

We wholeheartedly agree. Poor memory management has been the root cause for way too many vulnerabilities, for way too long. And that’s before you consider all the non-exploitable errors and crashes that could have been avoided by using memory safe languages (and other protections) when developing software.

So, if it’s been true for so long, why are we writing about it now? Becuase the NSA has published a Cybersecurity Information Sheet that provides guidance on how to protect against software memory safety issues.

The underlying reason is that many popular programming languages, such as C and C++, provide a lot of freedom and flexibility in memory management. That sounds good, but it relies heavily on the programmers writing and maintaining the code to do the right thing and perform the needed checks on memory references.

Trusting programmers to get it right…has not been good for security.

The NSA information sheet advises organizations to consider making a strategic shift from programming languages that provide little or no inherent memory protection, to a memory safe language where possible.

Memory issue examples

So, what are these memory issues?

If you ever read our posts describing security vulnerabilities you will see a lot of phrases like “buffer overflow”, “failure to release memory”, “use after free”, “memory corruption”, and “memory leak”. These are all memory management issues.

We’ve reproduced a few examples below, from InitialCommit.com (you can see more on the page we’ve linked to.) In a real program—we hope—these errors would be harder to spot.

Not freeing memory after allocation

In the first example, the variable memory is used to store the output of the C

malloc

function, which allocates memory. However, the memory allocated to memory the first time it is used is never released.

If memory is continually allocated like this and never freed, an attacker might be able to use it to perform a denial-of-service attack on the software by causing it to run out of memory.


int main() {
int *memory;

// Allocate 200 ints.
    memory = malloc(200 * sizeof(int));

// Allocate 100 more ints.
    // ERROR: This will compile, but will leave the previously
    // allocated memory hanging, with no way to access it.
    memory = malloc(100 * sizeof(int));

    // Free second block of 100 ints.
    // The first block is not freed.
    free(memory);

return 0;
}

Buffer overflow

In the second example, the variable memory is an array with ten elements. Computer languages typically count from 0, so the array’s first index is 0 and its last index is 9, not 10. The

for

loop is supposed to run once for each index, but it starts at 0 and stops at 10, performing one more action than it’s supposed to. Because the memory for the eleventh operation hasn’t been allocated, it will overwrite an adjacent bit of memory that may belong to another program.

Attackers can use buffer overflows to disrupt the operation of other programs, causing them to malfunction, expose secrets, or even run malicious code.

int main() {
// 10 element array, with indexes 0…9
int memory[10];
    int idx;

    // Perform operations on memory
    // ERROR: It looks like it could be correct, but the loop
    // actually executes with the range 0 … 10, not 0 … 9.

    // This overruns the buffer by one index.
    for (idx = 0; idx <= 10; ++idx)
            memory[idx] = idx;

return 0;
}

Memory issues like these are endemic to much of the code we rely on. Both attackers and defenders try to find the flaws using fuzzing techniques that run code with a huge variety of unusual inputs, to see if they trigger issues accessing, writing, allocating or deallocating memory. They then investigate the error to see how serious the problem is, and if it can be used to perform remote code execution (RCE), elevation of privilege (EoP), or other useful exploits.

Being memory safe

Memory safe languages

Memory safe languages like C#, Go, Java, Ruby, Rust, and Swift can manage memory automatically instead of relying on the programmer to do it. Using these languages can help enormously, but there are still risks. Some tasks may work better with manual memory management, so memory-unsafe classes or functions exist that allow the programmer more freedom in exchange for greater risk. Memory safe languages may also use libraries written in memory-unsafe languages.

The infamous HeartBleed vulnerability was an example of how memory mismanagement in one building block can have serious consequences for others.

Application security testing

It is not easy to shift a software development infrastructure from one computer language to another, so there are methods to harden memory-unsafe languages. Static and dynamic application security testing (SAST and DAST) can identify memory use issues in software.

Static analysis examines the source code to find potential security issues. Dynamic analysis can only identify issues with code that is on the execution path when the tool is run, so it may miss routines and functions that are not executed during testing.

Anti-exploitation features

Memory safety can also be improved by features that focus on limiting where code can be executed in memory, and making memory layout unpredictable. This makes it harder for the threat actors to turn unexpected behavior into something they can exploit.

There are several widely accepted methods that act as anti-exploitation features. For example, Address Space Layout Randomization (ASLR) and Data Execution Prevention (DEP) add unpredictability to where items are located in memory and prevent data from being executed as code. Control Flow Guard (CFG), restricts where code can be executed.