4

I'm not a low-level programmer, I mainly program in C# which is a managed language. Still, every now and then I read articles, news and patch notes about the most varied software talking about memory unsafety fixes, including a video (if I remember correctly about Rust and it's memory safety) in which the host says that Microsoft loses lots of money just paying people to fix memory unsafeties. It got me thinking what exactly does "memory safety" mean, what kind of code generates memory unsafety, how does scientists (or attackers) find them, how and to which extent can they exploit it and how do you fix the part of the code that has it? It would be very informative if someone could answer these questions with minimal code examples (preferably written in C).

1
  • 3
    Unsafety is the default state. We call a language "thing-safe" if we can prove that no legal programs contain "thing-faults" – Caleth Feb 21 '20 at 10:12
5

Most memory related security issues exploit buffer overflows:

  • In pure C# this appears difficult to realize.
  • However, in real-world C#, as soon as you call third party libraries or OS functions you‘re indirectly exposed.
  • Moreover, you‘re also exposed to buffer overflow in the c#/.net runtime environment. And that‘s where Microsoft‘s bug chaser enter the scene.

How are such bugs detected by an attacker?

  • Many of those originate in I/O, so network traffic, file i/o, user input and so on. So a first line of attack is to submit these feature to very large data. Then to test the different situations where such data is propagated across the application.

  • Another way is to find out which libraries and platform your code is using and see if there are known vulnerabilities in these that could be exploited.

  • Then there are certainly other tricks, but I‘m not security researcher, so I can‘t tell more than common knowledge here.

The good news is that you can do the first in your test cases and prevent such issues to be released. You can also do the second thing, and make sure your clients get the latest patched version of those libraries.

Another memory unsafety is badly handled memory allocation. This has to do with cases there is not enough memory, but the programme assumes that this will never happen:

  • In low level code this could have a similar effect than buffer overflow (writing data where it shouldn‘t be).
  • In high level code such as C#, you‘re in principle protected by exceptions. But if badly conceived exception handling is risk (e.g if you catch a general exception assuming it’s for another situation and resuming, or accidentally skipping some important verification when an exception occurs, etc).

These things are detected by an attacker by running the code in an environment with very little memory.

Fortunately you can prevent the issues by foreseeing testing exception situations as seriously as the notmal flow.

Finally memory leaking can lead to security vulnerability, either indirectly through memory exhaustion (see above), or by keeping data in memory longer than necessary, thus exposing it in case of memory dumps. Again, this is not supposed to happen in C#, but this requires tricky algorithms in the C# environment to detect for example unneeded circular references and things like that. That‘s why Microsoft has to invest so much in bug hunting.

1
  • 1
    One technique worth mentioning is fuzzing: throwing more or less random input at a program and watch if it exhibits unexpected behaviour. Can also run a program under Valgrind to report memory safety violations, e.g. buffer overflows or use after free. – amon Mar 1 '20 at 10:49
2

With C, the canonical example is the (no longer standard) gets library function:

char *gets( char *buf );

The gets function takes the address of a buffer and reads characters from standard input into that buffer until it sees a newline or end-of-file. Here’s the problem - gets doesn’t know how big the target buffer is. All it knows is the starting address.

So, if you pass it the address of a buffer that’s sized to hold 10 characters, but there’s 100 characters before the next newline, those extra characters are going to be written to the memory immediately following the end of the buffer, potentially overwriting other variables or corrupting the stack.

This is a problem with most of the C standard library - it doesn’t do any bounds checking to make sure it isn’t writing past the end of a buffer or to memory it doesn’t own. gets was especially egregious, and was deprecated after the 1999 revision and removed completely from the 2011 revision, but it’s far from the only library function with that problem.

Similarly, C doesn’t do any range checks on arithmetic types. If you try to store an input like 9999999999999999999999 to an int, it will store something, but not what you expect.

This was a conscious decision, and it’s part of what makes C code fast and compact. Bounds checks take up CPU cycles, and in the early 1970s (when C was initially developed), that mattered, especially since C was intended as a systems programming language rather than an applications programming language. Unfortunately, that means it is up to you, the programmer, to code those checks yourself, or to make sure that your buffers are always large enough for any possible input.

Newer languages like Java and C# do these bounds checks, and will throw exceptions if you try to access memory outside the bounds of an array, giving you a way to catch and recover.

2
  • 1
    While efficiency still matters, dedicating a bit more to safety and correctness became much easier to justify most of the time with hugely increased resources. – Deduplicator Feb 29 '20 at 11:48
  • @Deduplicator: Ironically, C compiler designs are going in the opposite direction. Historically, commonplace implementations would, as a form of what the authors of the Standard called "conforming language extension", offer behavioral guarantees in cases where the Standard imposed no requirements. Modern compilers, however, go out of their way not to behave meaningfully in such cases, however. – supercat Mar 13 '20 at 23:26
1

There are a number of unsafe memory anti-patterns and there can be multiple exploits for each type. At a high level what they boil down to is when an application allows access to parts of memory that it should not have. A really basic example an operating system allowing one application to directly access the working memory of another application. In that case, it would the operating system allowing 'unsafe memory' access. Such issues were no unheard of in the early days of computing but I think it's pretty safe to consider that a solved problem now.

The main one that is associated with exploits is the buffer overflow as mentioned in Cristophe's answer. A common scenario is when a program doesn't check whether an array access is within the bounds of the array. For example, if you allocate an array of 100 but allows reading or writing to the value at index 100. Since my C-skills are basically non-existent, I've copied an example from this article:

#include <stdio.h>
#include <string.h>

int main(void)
{
  char s[] = "Hello world";
  s[strlen(s)] = ’!’; // Try to append a ! to the string
  printf("%s\n", s);
  return 0;
}

This explanation follows:

In line 7, the trailing NULL character is overwritten with a !, and no new character is added. Line 8 attempts to send this string to stdout - it will probably print the string followed by some garbage, but it might crash.

This is the basic shape of the flaw. So consider now what occurs when you take user supplied input into some code that doesn't check bounds. If the user supplies more characters than the array is allocated to allow, the extra characters will be placed in the adjacent memory locations. Where exploits can get nasty is if those adjacent memory locations contain instructions. The user (or attacker in this case) can now replace those instructions with ones of their own choosing. In years past this could be really effective because if you could figure out what was in the adjacent memory, it would be the same for any instance of that application running on the same (or a similar) architecture.

That kind of thing has become a lot harder lately because compilers have buffer overflow protection and operating systems use techniques as address layout randomization which make such flaws harder to exploit.

-1

Memory Unsafety = Bad Book Keeping.

In a Garbage Collected language, the runtime provides a general and reliable book keeping for all sorts of resources from memory, to file handles.

When you forget to close a stream, delete a piece of memory, or a semaphore eventually the Garbage Collector will discover it and clean it up for you. In between the moment your code forgot about it and the garbage collector cleaning it up the resource is technically "leaked".

A Leaked resource generally has all sorts of negatives. It might be as inane as contributing to the resource cap (but otherwise not causing any issues), or it might have some nastier consequences such as permanently blocking a thread in the case of a semaphore, or denying read/write access to that document the user just saved from your program.

Now Garbage Collecting is a heavy-weight (though pretty reliable) way to eventually clean things up. That isn't always the best thing though.

Consider a server that has to open and close 1,000,000+ network connections per second. If the programmer carelessly forget to close() each connection the server would quickly run out of socket handles. A GC could release them, but there is no guarantee that it could release one now, nor is there a speed guarantee (a necessity on a heavy weight server). Obviously a Non-GC solution is needed here.

The obvious solution is for the programmer to simply call close() - but programmers make mistakes. Not that a mistake won't just happen, in 200K+ Lines of code its almost a dead certainty - leaving just what and where.

How do we check to see if the programmer really did write close()? Enter the Static-Analyser. Its kind of a semi-compiler it can read the source and check how it was written. It can check that if a socket was connected, that on every conceivable execution path, that a corresponding close() was called (among other useful checks).

The downside is that the analyser needs to be aware of these usage protocols. It is easy enough to sneak things through. Consider placing the network socket functions in a simple object wrapper, and compiling it as a third-party library. Reading the source how would the analyser infer that the wrapper object close() needs to be called? That can be solved by also reading those third-party libraries, a not trivial task (unless those libraries can be forced into a higher level language).

Actually these checks are so useful that it might make sense to just build them into the actual compiler, so that no code is generated that was not either marked as unsafe, or checks out correctly. This is what Rust is doing with regard to new and delete on memory allocations.

3
  • ... a general reliable bookkeeping for memory, and all other, resources, which are generally not fungible, get a basic safety-net, allowing eventual cleanup. The way you wrote it, it looks like GC can handle non-memory well enough, even if your fineprint takes that back later. – Deduplicator Feb 21 '20 at 7:00
  • @Deduplicator Well, yes a GC'ed Language is generally designed to provide the GC with enough information to allow it to execute cleanup operations, which supports non-memory resource cleanup. This is particularly true in the .Net/Java worlds. I would argue that a language which does not provide that is not a GC'ed language, even if a GC with some level of capability has been written for it. I'm not seeing how I walk that back later though, a GC like any algorithm has malevolent use cases. The pain could be endured, or another way found. Still doesn't change the generality or reliability. – Kain0_0 Feb 21 '20 at 7:40
  • wrt memory safety the primary value of GC or RAII is that the free() or close() operation happens implicitly when the object stops being visible/reachable, thus preventing or reducing use-after-free vulnerabilities. I'm currently working on a paper about defending against malloc()-based memory safety violations. They are only exploitable iff the application shows certain patterns, e.g. double frees, use after free, or buffer overflows. – amon Mar 1 '20 at 10:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.