Buffer Overflows have been around since the very beginnings of the Von-Neuman 1 architecture. They first gained widespread notoriety in
1988 with the Morris Internet worm. Unfortunately, the same basic attack remains
effective today. Of the 17 CERT security advisories of 1999, 10 of them were directly
caused by buffer-overflow software bugs. By far the most common type of buffer overflow
attack is based on corrupting the stack.
Most modern computer systems use a stack to pass arguments to procedures and to store
local variables. A stack is a last in first out (LIFO) buffer in the high memory area of
a process image. When a program invokes a function a new "stack frame" is created. This
stack frame consists of the arguments passed to the function as well as a dynamic amount
of local variable space. The "stack pointer" is a register that holds the current
location of the top of the stack. Since this value is constantly changing as new values
are pushed onto the top of the stack, many implementations also provide a "frame pointer"
that is located near the beginning of a stack frame so that local variables can more
easily be addressed relative to this value. 1 The
return address for function calls is also stored on the stack, and this is the cause of
stack-overflow exploits since overflowing a local variable in a function can overwrite
the return address of that function, potentially allowing a malicious user to execute any
code he or she wants.
Although stack-based attacks are by far the most common, it would also be possible to
overrun the stack with a heap-based (malloc/free) attack.
The C programming language does not perform automatic bounds checking on arrays or
pointers as many other languages do. In addition, the standard C library is filled with a
handful of very dangerous functions.
The following example code contains a buffer overflow designed to overwrite the return
address and skip the instruction immediately following the function call. (Inspired by 4)
#include <stdio.h>
void manipulate(char *buffer) {
char newbuffer[80];
strcpy(newbuffer,buffer);
}
int main() {
char ch,buffer[4096];
int i=0;
while ((buffer[i++] = getchar()) != '\n') {};
i=1;
manipulate(buffer);
i=2;
printf("The value of i is : %d\n",i);
return 0;
}
Let us examine what the memory image of this process would look like if we were to
input 160 spaces into our little program before hitting return.
[XXX figure here!]
Obviously more malicious input can be devised to execute actual compiled instructions
(such as exec(/bin/sh)).
The most straightforward solution to the problem of stack-overflows is to always use
length restricted memory and string copy functions. strncpy
and strncat are part of the standard C library. These
functions accept a length value as a parameter which should be no larger than the size of
the destination buffer. These functions will then copy up to `length' bytes from the
source to the destination. However there are a number of problems with these functions.
Neither function guarantees NUL termination if the size of the input buffer is as large
as the destination. The length parameter is also used inconsistently between strncpy and
strncat so it is easy for programmers to get confused as to their proper usage. There is
also a significant performance loss compared to strcpy when
copying a short string into a large buffer since strncpy
NUL fills up the size specified.
In OpenBSD, another memory copy implementation has been created to get around these
problem. The strlcpy and strlcat functions guarantee that they will always null terminate
the destination string when given a non-zero length argument. For more information about
these functions see 6. The OpenBSD strlcpy and strlcat instructions
have been in FreeBSD since 3.3.
Unfortunately there is still a very large assortment of code in public use which
blindly copies memory around without using any of the bounded copy routines we just
discussed. Fortunately, there is another solution. Several compiler add-ons and libraries
exist to do Run-time bounds checking in C/C++.
StackGuard is one such add-on that is implemented as a small patch to the gcc code
generator. From the StackGuard
website:
"StackGuard detects and defeats stack smashing attacks by protecting the return
address on the stack from being altered. StackGuard places a "canary" word next to the
return address when a function is called. If the canary word has been altered when the
function returns, then a stack smashing attack has been attempted, and the program
responds by emitting an intruder alert into syslog, and then halts."
"StackGuard is implemented as a small patch to the gcc code generator, specifically
the function_prolog() and function_epilog() routines. function_prolog() has been enhanced
to lay down canaries on the stack when functions start, and function_epilog() checks
canary integrity when the function exits. Any attempt at corrupting the return address is
thus detected before the function returns."
Recompiling your application with StackGuard is an effective means of stopping most
buffer-overflow attacks, but it can still be compromised.
Compiler-based mechanisms are completely useless for binary-only software for which
you cannot recompile. For these situations there are a number of libraries which
re-implement the unsafe functions of the C-library (strcpy,
fscanf, getwd, etc..) and
ensure that these functions can never write past the stack pointer.
-
libsafe
-
libverify
-
libparanoia
Unfortunately these library-based defenses have a number of shortcomings. These
libraries only protect against a very small set of security related issues and they
neglect to fix the actual problem. These defenses may fail if the application was
compiled with -fomit-frame-pointer. Also, the LD_PRELOAD and LD_LIBRARY_PATH environment
variables can be overwritten/unset by the user.