Much ado about NULL: An introduction to virtual memory
Posted in Programming on March 30th, 2010 by Nelson Elhage – 37 CommentsHere at Ksplice, we’re always keeping a very close eye on vulnerabilities that are being announced in Linux. And in the last half of last year, it was very clear that NULL pointer dereference vulnerabilities were the current big thing. Brad Spengler made it abundantly clear to anyone who was paying the least bit attention that these vulnerabilities, far more than being mere denial of service attacks, were trivially exploitable privilege escalation vulnerabilities. Some observers even dubbed 2009 the year of the kernel NULL pointer dereference.
If you’ve ever programmed in C, you’ve probably run into a NULL pointer dereference at some point. But almost certainly, all it did was crash your program with the dreaded “Segmentation Fault”. Annoying, and often painful to debug, but nothing more than a crash. So how is it that this simple programming error becomes so dangerous when it happens in the kernel? Inspired by all the fuss, this post will explore a little bit of how memory works behind the scenes on your computer. By the end of today’s installment, we’ll understand how to write a C program that reads and writes to a NULL pointer without crashing. In a future post, I’ll take it a step further and go all the way to showing how an attacker would exploit a NULL pointer dereference in the kernel to take control of a machine!
What’s in a pointer?
There’s nothing fundamentally magical about pointers in C (or assembly, if that’s your thing). A pointer is just an integer, that (with the help of the hardware) refers to a location somewhere in that big array of bits we call a computer’s memory. We can write a C program to print out a random pointer:
#include <stdio.h>
int main(int argc, char **argv) {
printf("The argv pointer = %d\n", argv);
return 0;
}
Which, if you run it on my machine, prints:
The argv pointer = 1680681096
(Pointers are conventionally written in hexadecimal, which would make that 0x642d2888, but that’s just a notational thing. They’re still just integers.)
NULL is only slightly special as a pointer value: if we look in stddef.h, we can see that it’s just defined to be the pointer with value 0. The only thing really special about NULL is that, by convention, the operating system sets things up so that NULL is an invalid pointer, and any attempts to read or write through it lead to an error, which we call a segmentation fault. However, this is just convention; to the hardware, NULL is just another possible pointer value.
But what do those integers actually mean? We need to understand a little bit more about how memory works in a modern computer. In the old days (and still on many embedded devices), a pointer value was literally an index into all of the memory on those little RAM chips in your computer:
This was true for every program, including the operating system itself. You can probably guess what goes wrong here: suppose that Microsoft Word is storing your document at address 700 in memory. Now, you’re browsing the web, and a bug in Internet Explorer causes it to start scribbling over random memory and it happens to scribble over memory around address 700. Suddenly, bam, Internet Explorer takes Word down with it. It’s actually even worse than that: a bug in IE can even take down the entire operating system.
This was widely regarded as a bad move, and so all modern hardware supports, and operating systems use, a scheme called virtual memory. What this means it that every program running on your computer has its own namespace for pointers (from 0 to 232-1, on a 32-bit machine). The value 700 means something completely different to Microsoft Word and Internet Explorer, and neither can access the other’s memory. The operating system is in charge of managing these so-called address spaces, and mapping different pieces of each program’s address space to different pieces of physical memory.
mmap(2)
One feature of this setup is that while each process has its own 232 possible addresses, not all of them need to be valid (correspond to real memory). In particular, by default, the NULL or 0 pointer does not correspond to valid memory, which is why accessing it leads to a crash.
Because each application has its own address space, however, it is free to do with it as it wants. For instance, you’re welcome to declare that NULL should be a valid address in your application. We refer to this as “mapping” the NULL page, because you’re declaring that that area of memory should map to some piece of physical memory.
On Linux (and other UNIX) systems, the function call used for mapping regions of memory is mmap(2). mmap is defined as:
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
Let’s go through those arguments in order (All of this information comes from the man page):
addr- This is the address where the application wants to map memory. If
MAP_FIXEDis not specified inflags,mmapmay select a different address if the selected one is not available or inappropriate for some reason. length- The length of the region the application wants to map. Memory can only be mapped in increments of a “page”, which is 4k (4096 bytes) on x86 processors.
prot- Short for “protection”, this argument must be a combination of one or more of the values
PROT_READ,PROT_WRITE,PROT_EXEC, orPROT_NONE, indicating whether the application should be able to read, write, execute, or none of the above, the mapped memory. flags- Controls various options about the mapping. There are a number of flags that can go here. Some interesting ones are
MAP_PRIVATE, which indicates the mapping should not be shared with any other process,MAP_ANONYMOUS, which indicates that thefdargument is irrelevant, andMAP_FIXED, which indicates that we want memory located exactly ataddr. fd- The primary use of
mmapis not just as a memory allocator, but in order to map files on disk to appear in a process’s address space, in which casefdrefers to an open file descriptor to map. Since we just want a random chunk of memory, we’re going passMAP_ANONYMOUSinflags, which indicates that we don’t want to map a file, andfdis irrelevant. offset- This argument would be used with
fdto indicate which portion of a file we wanted to map.
mmap returns the address of the new mapping, or MAP_FAILED if something went wrong.
If we just want to be able to read and write the NULL pointer, we’ll want to set addr to 0 and length to 4096, in order to map the first page of memory. We’ll need PROT_READ and PROT_WRITE to be able to read and write, and all three of the flags I mentioned. fd and offset are irrelevant; we’ll set them to -1 and 0 respectively.
Putting it all together, we get the following short C program, which successfully reads and writes through a NULL pointer without crashing!
(Note that most modern systems actually specifically disallow mapping the NULL page, out of security concerns. To run the following example on a recent Linux machine at home, you’ll need to run # echo 0 > /proc/sys/vm/mmap_min_addr as root, first.)
#include <sys/mman.h>
#include <stdio.h>
int main() {
int *ptr = NULL;
if (mmap(0, 4096, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0)
== MAP_FAILED) {
perror("Unable to mmap(NULL)");
fprintf(stderr, "Is /proc/sys/vm/mmap_min_addr non-zero?\n");
return 1;
}
printf("Dereferencing my NULL pointer yields: %d\n", *ptr);
*ptr = 17;
printf("Now it's: %d\n", *ptr);
return 0;
}
Next time, we’ll look at how a process can not only map NULL in its own address space, but can also create mappings in the kernel’s address space. And, I’ll show you how this lets an attacker use a NULL dereference in the kernel to take over the entire machine. Stay tuned!



Very nice simple description of mmap. I’m looking forward to part 2 with a real exploit!
Er…
# cat /proc/sys/vm/mmap_min_addr
4096
# gcc a.c ; ./a.out
Defererencing my NULL pointer yields: 0
Now it’s: 17
What’s happening?
Thanassis: From your prompt, I’m guessing that you’re running the test as root. Like many other restrictions in Linux/UNIX,
mmap_min_addrdoes not apply to the root user by default. If you run the demo program as an unprivileged user, I expect it will fail unless you decreasemmap_min_addr.Is there any clever use of this to help catch and gracefully report or recover from a 0 memory address dereference, so we’re not left with an outright segmentation fault? I can’t think of anything clever off hand.
Well, you can probably allocate a page at address 0 make a copy of the page for comparison, occasionally diffing the original with the current, looking for a change; but even if that’s even possible, it doesn’t sound terribly useful. I don’t think I would rely on it.
If you wanted to catch uses of a NULL pointer, you’d probably be better off using
signal(2), which lets you install a handler for segmentation faults that does something other than quit immediately. One could imagine e.g. printing a stack trace before quitting, and there’s even a utility,catchsegvthat does that for you.You could, of course, do something other than crashing or quitting, but it’s not totally clear what; Assuming that the dereference is a bug, it’s going to be tricky to figure out some way to continue that necessarily does anything useful, and who knows what other memory may or may not be corrupted.
Fantastic article. I didn’t know that mmap() could allocate memory like that. I thought it was just for memory-mapped I/O. A great explanation as usual. Looking forward to many more posts.
printf(“The argv pointer = %d\n”, argv);
Should be
printf(“The argv pointer = %p\n”, argv);
It’s true that using
%pwould have been more correct, and would avoid a warning from GCC. I deliberately chose to use%dto demonstrate the point that, under the hood, pointers are just integers, even if you’re not supposed to treat them that way or care about that fact in most code.Very neat and lucid explanation, thanks a lot! I forwarded this to my associates to have a look and get their doubts cleared about segfaults.
Great article! Are there any normal cases where the NULL page is used for anything or is it just left alone by both the kernel and user space?
Hello, I think that it’s wrong giving credits to Brad Spengler for such an old bug class. Definitely his work on Linux kernel exploitation is awesome but NULL pointer dereference vulnerabilities were being actively exploited at least since 1994.
In addition to this, I’d written a similar post [1] on my blog five months before spender released his first Linux kernel NULL pointer dereference “/dev/net/tun” exploit [2].
Nevertheless, I really enjoyed your post and how you described the whole bug although you intentionally left lots of details out. Well done!
[1]. http://xorl.wordpress.com/2009/03/11/null-just-an-address/
[2]. http://xorl.wordpress.com/2009/07/17/linux-kernel-devnettun-null-pointer-dereference/
@bzdzb: Emulators (like dosemu) usually need the NULL since they need to use the first page of the system (NULL page) for vm86 (virtual real mode) support that is required to emulate the address space of some old software. Of course, all of these are x86 specific.
@xorl: Thanks for the comment! I definitely don’t mean to give spender credit for discovering or first exploiting this bug class — as you said, this is a very old type of vulnerability, and one that should be obvious to anyone with a solid understanding of how virtual memory works and how the kernel manages memory.
However, I do think that during the hubbub about NULL pointer vulnerabilities last year, spender’s exploits and commentary did play a role in spreading awareness and concern about these classes of vulnerability, and making people wake up and realize there were problems. That’s why I linked him in this context.
xorl forgot to mention that none of the NULL pointer dereference exploits that spender wrote are his discovery
nelhage:
Consider the difference:
$ cat test.c
#include
int
main(int argc, char **argv)
{
printf(“%d %p\n”, argv, argv);
return 0;
}
$ ./test
-1079998060 0xbfa08994
$
I’m not trying to start a rant, just stating the obvious
It’s true that using %p would have been more correct
You appear to think that using %d to print a(n un-cast) pointer is somehow correct?
%p requires a void* argument. %d requires an integer. Contrary to your assertion, pointers are *not* integers in C, and should not be be swapped, in the way you imply, without a cast.
What happens in implementations where sizeof(void*) != sizeof(int)?
You appear to think that using %d to print a(n un-cast) pointer is somehow correct?
It’s correct only in that (at least on the x86 platforms almost everyone reading this blog post is probably using), it works. I agree that it’s in blatant violation of spec, and is totally unportable.
But this wasn’t a post on portable code or even really about C; It was designed to be a peek under the usual layers of abstraction at how this stuff is implemented. And while it’s true that, in C, a pointer is not an integer and should not normally be treated as one, to the underlying hardware there is no distinction — it’s just a question of which instructions you use with a value. And that’s the point I was trying to make.
I should probably have been more clear that this post was a peek at a particular implementation (Linux on the x86), and that while large parts of it carry over to other platforms, I make no guarantees that anything contained here is accurate anywhere else.
I just don’t understand why you’re persistent on using %d due to the assumption that you’re using x86, or the “usual layers of abstraction at how this is implemented” instead of %p for printing a pointer (which might be equivalent to %#lx). I re-read your last comment thrice and made no sense from it, but never the less, good post!
I’m a senior in CS at Purdue University, and I’ve taken a few classes in operating systems and computer architecture. This is a great supplemental article to the knowledge I’ve obtained, and gives me more insight into virtual memory and the kernel. Great posts, you’ve got a consistent reader!
I think the author’s use of %d in this context is perfectly valid. He’s trying to make a point about hardware at a very basic level, more basic than the abstractions provided by the C language and compilers. Pointers are just integers, in the mathematical sense. They are the sequential set of whole numbers that start at 0 that are used to ‘address’ individual locations of memory. The fact that the C language authors chose to create an abstraction via the pointer type is beyond the scope of the article.
The authors of C, in my opinion, would not have the least problem with this article or the presented sample code. One of the fundamental features of C is that you can write C code with a specific purpose in mind, even if it ‘breaks’ the language specification or causes a warning or isn’t portable. C is first of all a convenient way to write assembly code, and only secondly a way to write portable applications. There aren’t supposed to be any training wheels in C; you get exactly what you write and live with the consequences.
Very well written! Thanks for the read!
Great explanation
I must agree with PJH, Jutsu and others here. If this post is not about C then don’t write in C. If you are writing in C write correct C. On Linux Intel system “sizeof(int)” does not even equal “sizeof(char*)” so what I get is garbage:
$ cat test.c; ./test
#include
int main(int argc, char **argv) {
printf(“%%x: %x %%p: %p\n”, argv, (void *)argv);
return 0;
}
%x: ab4d7488 %p: 0x7fffab4d7488
Thus, really, why can’t you use “%p” (don’t forget about casting to “(void *)”) to shut as all up? Either that or clearly state in the post that this code is utterly wrong and may or may not work. Otherwise you’re just promoting invalid coding practices.
But besides that, what I really wanted to point out was that the claim “if we look in stddef.h, we can see that it’s just defined to be the pointer with value 0″ is misleading. What it suggest is that definition of “NULL” constant to be “0″ or “((void*)0)” mean that the representation of a null pointer are all zero bits — this is not the case. “0″ in C has a magic meaning when used in pointer context — it means a null pointer. So no matter what the internal representation of the null pointer is (it can be “0xffffffff”) “0″ will always mean a null pointer in pointer syntax in C (and in C++).
To run the sample on Mac OS X MAP_ANONYMOUS has to be replaced with MAP_ANON.
Otherwise, it gives the expected result; mmap(NULL) gives no warning.
Some people have some very strange takes on facts and history (hi xorl!).
Since someone pointed me to the comments here I’ll clear up the factual inaccuracies that have been posted. xorl claims my first kernel NULL pointer dereference bug was for /dev/net/tun (the discovery of it being exploitable was by me (contrary to jutsu’s claim), the details of which are all present in the cheddar bay source). Any time the vulnerability wasn’t discovered by me I’ve given proper credit in the initial exploit releases.
It seems like xorl is trying to gain some credit by referencing his post about NULL pointer dereferences a couple months prior to the release of cheddar bay. Maybe he didn’t read the source clearly enough (or at all) to see that I had already released the first public NULL pointer dereference exploit in the Linux kernel (for another vuln discovered by me, contrary to jutsu) in the beginning of 2007 (the exploit itself having been written in 2006), see the post here:
http://marc.info/?l=dailydave&m=117294179528847&w=2
I was generically disabling SELinux and LSM back then even too.
UDEREF exited prior to the development of my exploit, so we were certainly aware of the bugclass. What was released in 1994 was
Finally, are you just trying to find something to argue with? The post only said:
“Brad Spengler made it abundantly clear to anyone who was paying the least bit attention that these vulnerabilities, far more than being denial of service attacks, were trivially exploitable privilege escalation vulnerabilities.” Where is that giving anyone credits for a bugclass? It’s pointing out the fact that I released numerous high-profile exploits for the bugclass. The only thing I claim is releasing the first *public* NULL pointer dereference exploit for the *Linux kernel*, a claim which is undisputed.
The exploit from 1994 by 8lgm was for the userland pt_chmod binary on SCO systems where a mapping existed at NULL in processes under normal operation (and contained some non-empty data at the NULL address). This exploit is commonly mentioned, but you’d be hard-pressed to find any discussion of exploitation of the bugclass in kernel-land prior to 2007. As I mentioned in my posting with the exploit, I believe ilja was the one who killed it for the kernel (again, after UDEREF was implemented and after my exploit was written — the bugclass was known/exploited privately prior to that).
I’m not sure why these people who don’t publish their own work are such complainers about those that do; and yet they too want credit and fame. Unfortunately, “I wrote a blog post 5 months before the last exploit of yours I remember” is a lame attempt to get it. As always, I don’t expect an apology from these constant history revisionists. They obviously have other motives behind their claims, as objectiveness/factual accuracy clearly isn’t it.
-Brad