‘smash the stack’ [C programming] n. On many C implementations it is possible to corrupt the execution stack by writing past the end of an array declared auto in a routine. Code that does this is said to smash the stack, and can cause return from the routine to jump to a random address. This can produce some of the most insidious data-dependent bugs known to mankind. Variants include trash the stack, scribble the stack, mangle the stack; the term munge the stack is not used, as this is never done intentionally. See spam; see also alias bug, fandango on core, memory leak, precedence lossage, overrun screw.
—ELIAS LEVY
ALEPH ONE
SMASHING THE STACK FOR FUN AND PROFIT [ALEPH, 1996]
One essential aspect of a risk analysis is knowing the most common risks and where they are likely to be introduced. Part of this knowledge includes familiarity with the things that coders have a fair chance of doing wrong and that almost always lead to security problems. In this chapter we dissect the single biggest software security threat—the dreaded buffer overflow.
As David Wagner correctly points out, buffer overflows have been causing serious security problems for decades [Wagner, 2000]. In the most famous example, the Internet worm of 1988 made use of a buffer overflow in fingerd
to exploit thousands of machines on the Internet and cause massive headaches for administrators around the country [Eichin, 1988]. But there is more to the buffer overflow problem than ancient history. Buffer overflows accounted for more than 50% of all major security bugs resulting in CERT/CC advisories in 1999, and the data show that the problem is growing instead of shrinking. (See the sidebar Buffer Overflow; Déjà Vu All Over Again.)
Clearly, the buffer overflow problem should be relegated to the dustbin of history. The question is, Why are buffer overflow vulnerabilities still being produced? The answer: Because the recipe for disaster is surprisingly simple. Take one part bad language design (usually C and C++) and mix in two parts poor programming, and you end up with big problems. (Note that buffer overflows can happen in languages other than C and C++, although modern safe languages like Java are immune to the problem, barring some incredibly unusual programming. In any case, there are often legitimate reasons to use languages like C and C++, so learning how to avoid the pitfalls is important.)
The root cause behind buffer overflow problems is that C is inherently unsafe (as is C++).1 There are no bounds checks on array and pointer references, meaning a developer has to check the bounds (an activity that is often ignored) or risk problems. There are also a number of unsafe string operations in the standard C library, including
1. Note that unsafe is a technical term that has to do with the way a language protects memory and objects that it manipulates. For more, see [Friedman, 2001].
• strcpy()
• strcat()
• sprintf()
• gets()
• scanf()
For these reasons, it is imperative that C and C++ programmers writing security-critical code learn about the buffer overflow problem. The best defense is often a good education on the issues.
This chapter deals with buffer overflows. We begin with an overview that explains why buffer overflow is a problem. We then cover defensive programming techniques (in C), and explain why certain system calls are problematic and what to do instead. Finally we wrap up with a peek under the hood that shows how a buffer overflow attack does its dirty work on particular architectures.
Buffer overflows begin with something every program needs, someplace to put stuff. Most useful computer programs create sections in memory for information storage. The C programming language allows programmers to create storage at runtime in two different sections of memory: the stack and the heap. Generally, heap-allocated data are the kind you get when you “malloc()
” or “new
” something. Stack-allocated data usually include nonstatic local variables and any parameters passed by value. Most other things are stored in global static storage. We cover the details later in the chapter. When contiguous chunks of the same data type are allocated, the memory region is known as a buffer.
C programmers must take care when writing to buffers that they do not try to store more data in the buffer than it can hold. Just as a glass can only hold so much water, a buffer can hold only so many bits. If you put too much water in a glass, the extra water has to go somewhere. Similarly, if you try to put more data in a buffer than fit, the extra data have to go somewhere, and you may not always like where it goes. What happens is that the next contiguous chunk of memory is overwritten. When a program writes past the bounds of a buffer, this is called a buffer overflow. Because the C language is inherently unsafe, it allows programs to overflow buffers at will (or, more accurately, completely by accident). There are no runtime checks that prevent writing past the end of a buffer; meaning, programmers have to perform the check in their own code, or run into problems down the road.
Reading or writing past the end of a buffer can cause a number of diverse (often unanticipated) behaviors: (1) Programs can act in strange ways, (2) programs can fail completely, or (3) programs can proceed without any noticeable difference in execution. The side effects of overrunning a buffer depend on four important things2:
2. Whether the stack grows up (toward higher memory addresses) or down (toward lower memory addresses) is also an important consideration, because upward-growing stacks tend to be less vulnerable to overflows. However, they are also rarely used.
1. How much data are written past the buffer bounds
2. What data (if any) are overwritten when the buffer gets full and spills over
3. Whether the program attempts to read data that are overwritten during the overflow
4. What data end up replacing the memory that gets overwritten
The indeterminate behavior of programs that have overrun a buffer makes the programs particularly tricky to debug. In the worst case, a program may be overflowing a buffer and not showing any adverse side effects at all. As a result, buffer overflow problems are often invisible during standard testing. The important thing to realize about buffer overflow is that any data that happen to be allocated near the buffer can potentially be modified when the overflow occurs.
You may be thinking, “Big deal, a little spilled water never hurt anybody.” To stretch our analogy a bit, imagine that the water is spilling onto a worktable with lots of exposed electrical wiring. Depending on where the water lands, sparks could fly. Likewise, when a buffer overflows, the excess data may trample other meaningful data that the program may wish to access in the future. Sometimes, changing these other data can lead to a security problem.
In the simplest case, consider a Boolean flag allocated in memory directly after a buffer. Say that the flag determines whether the user running the program can access private files. If a malicious user can overwrite the buffer, then the value of the flag can be changed, thus providing the attacker with illegal access to private files.
Another way in which buffer overflows cause security problems is through stack-smashing attacks. Stack-smashing attacks target a specific programming fault: careless use of data buffers allocated on the program’s runtime stack (that is, local variables and function arguments). The results of a successful stack-smashing attack can be far more serious than just flipping a Boolean access control flag, as in the previous example. A creative attacker who takes advantage of a buffer overflow vulnerability through stack smashing can usually run arbitrary code. The idea is pretty straightforward: Place some attack code somewhere (for example, code that invokes a shell) and overwrite the stack in such a way that control gets passed to the attack code. We go into the details of stack smashing later.
Commonly, attackers exploit buffer overflows to get an interactive session (shell) on the machine. If the program being exploited runs with a high privilege level (such as root or administrator), then the attacker gets that privilege during the interactive session. The most spectacular buffer overflows are stack smashes that result in a superuser, or root, shell. Many exploit scripts that can be found on the Internet (we link to several on this book’s Web site) carry out stack-smashing attacks on particular architectures.
Heap overflows are generally much harder to exploit than stack overflows (although successful heap overflow attacks do exist). For this reason, some programmers never statically allocate buffers. Instead, they malloc()
or new
everything, and believe this will protect them from overflow problems. Often they are right, because there aren’t all that many people who have the expertise required to exploit heap overflows. However, dynamic buffer allocation is not intrinsically less dangerous than other approaches. Don’t rely on dynamic allocation for everything and forget about the buffer overflow problem. Dynamic allocation is not a silver bullet.
Let’s dig a bit deeper into why some kinds of buffer overflows have big security implications. A number of interesting UNIX applications need special privileges to accomplish their jobs. They may need to write to a privileged location like a mail queue directory, or open a privileged network socket. Such programs are generally suid or (setuid) root; meaning the system extends special privileges to the application on request, even if a regular old user runs the program (discussed in Chapter 8). In security, any time privilege is being granted (even temporarily), there is the potential for privilege escalation to occur. Successful buffer overflow attacks can thus be said to be carrying out the ultimate in privilege escalation. Many well-used UNIX applications, including lpr
, xterm
, and eject
, have been abused into giving up root privileges through the exploit of buffer overflow in suid regions of the code.
One common break-in technique is to find a buffer overflow in an suid root program, and then exploit the buffer overflow to snag an interactive shell. If the exploit is run while the program is running as root, then the attacker gets a root shell. With a root shell, the attacker can do pretty much anything, including viewing private data, deleting files, setting up a monitoring station, installing backdoors (with a root kit), editing logs to hide tracks, masquerading as someone else, breaking things accidentally, and so on.
Of course, many people believe that if their program is not running suid root, then they don’t have to worry about security problems in their code because the program can’t be leveraged to achieve high access levels. This idea has some merit, but is still a very risky proposition. For one thing, you never know which oblivious user is going to take your program and set the suid bit on the binary. When people can’t get something to work properly, they get desperate. We’ve seen this sort of situation lead to entire directories of programs needlessly set setuid root.
Also, anything run when an administrator is logged in as root will have root privileges, such as install programs for software packages.3 This tends not to be very applicable here; it is usually easier to get root to run your own binary than to get root to pass inputs to a flawed program with a buffer overflow, because the latter requires finding or crafting an exploit.
3. This problem is why root should not have the current directory in its command path, especially early in its command path, because root may be tricked into running a Trojan horse copy of ls
that resides in the /tmp
directory.
There can also be users of your software that have no privileges at all. This means that any successful buffer overflow attack gives them more privileges than they previously had. Usually, such attacks involve the network. For example, a buffer overflow in a network server program that can be tickled by outside users can often provide an attacker with a login on the machine. The resulting session has the privileges of the process running the compromised network service. This sort of attack happens all the time. Often, such services run as root (and often, for no really good reason other than to make use of a privileged low port). Even when such services don’t run as root, as soon as a cracker gets an interactive shell on a machine, it is usually only a matter of time before the machine is completely “owned.” That is, before the attacker has complete control over the machine, such as root access on a UNIX box or administrator access on an NT box. Such control is typically garnered by running a different exploit through the interactive shell to escalate privileges.
Protecting your code through defensive programming is the key to avoiding buffer overflow. The C standard library contains a number of calls that are highly susceptible to buffer overflow that should be avoided. The problematic string operations that do no argument checking are the worst culprits. Generally speaking, hard-and-fast rules like “avoid strcpy()
” and “never use gets()
” are not really that far off the mark. Nevertheless, even programs written today tend to make use of these calls because developers are rarely taught not to do so. Some people pick up a hint here and there, but even good hackers can screw up when using homegrown checks on the arguments to dangerous functions or when incorrectly reasoning that the use of a potentially dangerous function is “safe” in some particular case.
Never use gets()
. This function reads a line of user-typed text from the standard input and does not stop reading text until it sees an end-of-file character or a newline character. That’s right; gets()
performs no bounds checking at all. It is always possible to overflow any buffer using gets()
. As an alternative, use the method fgets()
. It can do the same things gets()
does, but it accepts a size parameter to limit the number of characters read in, thus giving you a way to prevent buffer overflows. For example, instead of the following code:
Use the following:
There are a bunch of standard functions that have real potential to get you in trouble, but not all uses of them are bad. Usually, exploiting one of these functions requires an arbitrary input to be passed to the function. This list includes
• strcpy()
• strcat()
• sprintf()
• scanf()
• sscanf()
• vfscanf()
• vsprintf()
• vscanf()
• vsscanf()
• streadd()
• strecpy()
• strtrns()
We recommend you avoid these functions if at all possible. The good news is that in most cases there are reasonable alternatives. We go over each one of them, so you can see what constitutes misuse, and how to avoid it.
The strcpy()
function copies a source string into a buffer. No specific number of characters are copied. The number of characters copied depends directly on how many characters are in the source string. If the source string happens to come from user input, and you don’t explicitly restrict its size, you could be in big trouble! If you know the size of the destination buffer, you can add an explicit check:
An easier way to accomplish the same goal is to use the strncpy()
library routine:
strncpy(dst, src, dst_size – 1);
dst[dst_size –1] = ' '; /* Always do this to be safe! */
This function doesn’t throw an error if src
is bigger than dst
; it just stops copying characters when the maximum size has been reached. Note the –1
in the call to strncpy()
. This gives us room to put a null character in at the end if src
is longer than dst
. strncpy()
doesn’t null terminate when the src
parameter is at least as long as the destination buffer.
Of course, it is possible to use strcpy()
without any potential for security problems, as can be seen in the following example:
strcpy(buf, "Hello!");
Even if this operation does overflow buf
, it only does so by a few characters. Because we know statically what those characters are, and because those characters are quite obviously harmless, there’s nothing to worry about here (unless the static storage in which the string "Hello!"
lives can be overwritten by some other means, of course).
Another way to be sure your strcpy()
does not overflow is to allocate space when you need it, making sure to allocate enough space by calling strlen()
on the source string. For example,
dst = (char *)malloc(strlen(src) + 1);
strcpy(dst, src);
The strcat()
function is very similar to strcpy()
, except it concatenates one string onto the end of a buffer. It too has a similar, safer alternative—strncat()
. Use strncat()
instead of strcat()
, if you can help it. One problem with strncat()
is that you do need to keep track of how much room is left in the destination buffer, because the function only limits the number of characters copied and doesn’t operate on the total length of the string. The following has a lot to recommend it, despite the inefficiency of the strlen
call:
strncat(dst, src, dst_size – strlen(dst) – 1);
The functions sprintf()
and vsprintf()
are versatile functions for formatting text, and storing it in a buffer. They can be used to mimic the behavior of strcpy()
in a straightforward way. This means that it is just as easy to add a buffer overflow to your program using sprintf()
and vsprintf()
as with strcpy()
. For example, consider the following code:
Code like this is encountered fairly often. It looks harmless enough. It creates a string that knows how the program was invoked. In this way, the name of the binary can change, and the program’s output automatically reflects the change. Nonetheless, there’s something seriously wrong with the code. File systems tend to restrict the name of any file to a certain number of characters. So you’d think that if your buffer is big enough to handle the longest name possible, your program would be safe, right? Just change 1024 to whatever number is right for our operating system and we’re done, no? No. We can easily subvert this restriction by writing our own little program to start the previous one:
The function execl()
starts the program named in the first argument. The second argument gets passed as argv[0]
to the called program. We can make that string as long as we want!
So how do we get around the problems with {v}sprintf()
? Unfortunately, there is no completely portable way. Some implementations provide an snprintf()
method, which allows the programmer to specify the maximum number of characters to copy into the buffer. For example, if snprintf
was available on our system, we could fix the previous example to read:
Many (but not all) versions of {v}sprintf()
come with a safer way to use the two functions. You can specify a precision for each argument in the format string itself. For example, another way to fix the broken sprintf()
is
Notice the .1000
after %
and before s
. The syntax indicates that no more than 1,000 characters should be copied from the associated variable (in this case, argv[0]
).
Another solution is to package a working version of snprintf()
along with your code. One is available at this book’s Web site.
Moving on, the scanf()
family of functions (scanf()
, sscanf()
, fscanf()
, and vfscanf()
) is also poorly designed. In this case, destination buffers can overflow. Consider the following code:
If the scanned word is larger than the size of buf
, we have an overflow condition. Fortunately, there’s an easy way around this problem. Consider the following code, which does not have a security vulnerability:
The 255
between %
and s
specifies that no more than 255 characters from argv[0]
should actually be stored in the variable buf
. The rest of the matching characters are not copied.
Next we turn to streadd()
and strecpy()
. Although not every machine has these calls to begin with, programmers who have these functions available to them should be cautious when using them. These functions translate a string that may have unreadable characters into a printable representation. For example, consider the following program:
This program prints
instead of printing all white space. The streadd()
and strecpy()
functions can be problematic if the programmer doesn’t anticipate how big the output buffer needs to be to handle the input without overflowing. If the input buffer contains a single character, say, ASCII 001 (a control-A), then it will print as four characters,