As the reach and deployment of computer technology expand, the risks and problems associated with pervasive use and the often breakneck speed of innovation and adoption impose more and more frequently. And while numerous security advisories are issued every day, those alerts probably represent only a tiny fraction of the faults that exist and have yet to be discovered — either by the good guys or the bad guys.
One serious problem is vulnerability, where a fault or an oversight in a software application allows unauthorized access to the computer. While some vulnerabilities are “mostly harmless†— for instance, spyware may be unwanted, but is otherwise benign — others vulnerabilities can undermine privacy and even breach security measures. A significant weakness, or exploit, can even permit a malcontent to trick a program into doing something it wasn’t designed to do.
Oddly enough, the most commonly attacked exploit, the buffer overflow, is also the oldest. Around since the infancy of computers in the 1960s, buffer overflow first gained widespread notoriety in 1988, when the first Internet worm, Morris (named after its creator), propagated by exploiting a buffer overflow vulnerability in the fingerd daemon. Some twenty years later, Internet worms like Code Red and Blaster propogated by exploiting buffer overflows, too. Today, a calculator may have more computing power than the Apollo spacecraft, but the more things change, the more they stay the same.
On to the main() Event
Since there’s no explanation better than experience, here’s is a simple body of code that’s vulnerable to a buffer overflow:
$ cat vuln.c
int main(int argc, char *argv[])
{
char buffer[10];
strcpy(buffer, argv[1]);
return 0;
}
The code expects a single command-line argument, which is then copied into the ten-character buffer, buffer. However, because the program doesn’t check the length of the command-line argument before the strcpy(), it can suffer an overflow. Compile and run the program to see what happens:
$ gcc –o vuln vuln.c
$ ./vuln AAAAAAA
$ ./vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault
$
The program crashes with a segmentation fault, which means that something important was overwritten by the unexpected extra data. You can see specifically what happened by looking at the program’s dumped core file using a debugger like gdb.
$ ulimit –c unlimited
$ ./vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
$ gdb –q –c core
Core was generated by `./vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'.
Program terminated with signal 11, Segmentation fault.
#0 0x41414141 in ?? ()
(gdb) info register eip
eip 0x41414141 0x41414141
(gdb) info stack
0x41414141: Cannot access memory at address 0x41414141
(gdb) quit
$
The ulimit command allows core dumps to get as big as needed, so this time, vuln with a large argument produces a core file. gdb reveals that the vuln program crashed because the extended instruction pointer (EIP) was set to 0×41414141. (Why 0×41414141? 0×41 is hexadecimal for the character “A.”) Since this is a memory address that cannot be accessed, the program crashed.
A little background on how a program actually works would probably be helpful here. The EIP is just a 32-bit register that holds the address of the currently executing instruction. All of the programs instructions are stored in a memory segment called the text segment. If the text segment is a book filled with instructions, then the EIP is a finger pointing to words as it reads them.
Easy enough, but oftentimes the program has to jump around in the text segment in a non-linear fashion. When a function is called, for example, the EIP has to jump to a different part of the text segment to execute the function, but afterwards it needs to be able to return to where it was to execute the next instruction. Continuing with the book metaphor, what’s needed here is some sort of bookmark, so the program can set a bookmark, jump to the function, execute it, and then return to the bookmark. This is what another section of memory called the stack is used for.
Additionally, the stack also provides each function with its own context, which allows each function to have its own local variables that only it knows about. So every time a function is called, a stack frame is created which contains the local variables for that function and the return address so the program execution can continue after the function completed. (The stack is also used to pass arguments to functions, which the function treats as local variables, too.)
Each stack frame is stored on the stack, which is a” First In, Last Out” (FILO) structure. (Imagine putting beads on a string with a knot on the end. The first bead you put on is be the last one to come off.) When a stack frame is put on the stack it is pushed on the stack; when a stack frame is removed from the stack it is popped from the stack. This structure proves to be useful when multiple functions are called within each other.
For example, consider functions.c, shown in Listing One.
If you build the code with gcc –g –o functions functions.c and run it with ./functions, you should see this:
In main
i = 3, j = 11
In function #1
i = 5, j = 11
In function #2
i = 7, j = 11
Exiting function #2
Exiting function #1
Exiting main
j is a global variable, so it's available in every function context and is stored in a completely different segment of memory. When main() is executed, a stack frame is created and pushed to the stack containing it's local variables, where i is 3. When func1() is called within main(), another stack frame is created with both the return address to get back to the next instruction in main() and func1()' s local variables. Within func1(), i is 5. Next func2() is called from func1(), which creates yet another stack frame with the return address for the next instruction in func1() and func2()' s local variables, where i is 7. After each function completes, its stack frames is popped off and the EIP is restored to continue execution where the calling function left off.
These different stack frames can be examined with gdb. Run gdb –q functions and then set breakpoints before and after each function is called to pause the program's execution and give you a chance to look at the stack frames. This is shown in Figure One.