Attacking applications is a core technique for vulnerability researchers. Test engineers can spare a company from needless expense and public embarrassment by finding early exploitation points in the company’s software. This chapter reviews a variety of application attack techniques, including buffer overflows and heap overflows. It also builds on the reverse engineering knowledge gained from the previous chapters.
To exploit an overflow, you need a thorough knowledge of assembly language, C++, and the operating system you wish to attack. This chapter describes buffer overflows, traces their evolution, and even walks you through a live sample.
A buffer overflow attack deliberately enters more data than a program was written to handle. The extra data overflows the region of memory set aside to accept it, thus overwriting another region of memory that was meant to hold some of the program’s instructions. In the ideal version of this attack, the overflow values introduced become new instructions that give the attacker control of the target processor.
Buffer overflow attacks are not a new phenomenon. For example, the original Morris worm in 1988 used a buffer overflow. In fact, the issue of buffer overflow risks to computer systems has been recognized since the 1960s.
Buffer overflows result from an inherent weakness in the C++ programming language. The problem (which is inherited from C and likewise found in other languages, such as Fortran) is that C++ does not automatically perform bounds-checking when passing data. To understand this concept, consider the following sample code that illustrates how a C/C++ function returns data to the main program:
// lunch.cpp : Overflowing the stomach buffer #include <stdafx.h> #include <stdio.h> #include <string.h> void bigmac(char *p); int main(int argc, char *argv[]) { bigmac("Could you supersize that please?"); // size > 9 overflows return 0; } void bigmac(char *p) { char stomach[10]; //limit the size to 10 strcpy(stomach, p); printf(stomach); }
To test this program, you compile it using a C++ compiler. Although the program compiles without errors, when we execute it we get a program crash similar to Figure 5-1.
What happened? When this program executes, it calls the
function bigmac
and passes it the
long string “Could you supersize that please?” Unfortunately,
strcpy( )
never checks the
string’s length. This is dangerous, because in this case passing a
string longer than nine characters generates a buffer
overflow.
Like several other C++ functions, strcpy( )
is inherently weak, in that it
will write the extra characters past the variable end. This usually
results in a program crash. In this particular case, the crash was
an error in reading past the end of the statically allocated string.
In a worst-case scenario, such an overflow might allow you to
execute arbitrary code on the target system, as discussed later in
this chapter.
Buffer overflows are a leading type of security vulnerability. In order to understand how a hacker can use a buffer overflow to infiltrate or crash a computer, you need to understand exactly what a buffer is.
This section provides a basic introduction to buffers; experienced users should skip ahead to Section 5.3.
A computer program consists of code that accesses variables stored in various locations in memory. As a program is executed, each variable is assigned a specific amount of memory, determined by the type of information the variable is expected to hold. For example, a Short Integer only needs a little bit of memory, whereas a Long Integer needs more space in the computer’s memory (RAM). There are many different possible types of variables, each with its own predefined memory length. The space set aside in the memory is used to store information that the program needs for its execution. The program stores the value of a variable in this memory space, then pulls the value back out of memory when it’s needed. This virtual space is called a buffer.
A good analogy for a buffer is a categorized CD collection. You have probably seen the tall CD towers that hold about 300 CDs. Your computer’s memory is similar to a CD holder. The difference is that a computer can have millions of slots that are used to store information, compared to the relatively limited space on a CD rack. Our example CD collection consists of three main categories: Oldies, Classical, and Pop Rock (Figure 5-2). Logically, we would separate the 300 slots into 3 parts, with 100 slots for each genre of music. The bottom 100 of the CD holder is set aside for Oldies, the middle 100 is for Classical, and the top 100 contains Pop. Each slot is labeled with a number; you know where each type of music begins and ends based on the slot number.
A computer’s memory is very similar. When a program is loaded into memory, it automatically allocates chunks of memory for all the variables it has been programmed to use. However, instead of one slot per variable, each variable uses several slots. This situation is analagous to a CD set: if you wanted to store your four-CD Bach collection, you would use four consecutive slots. This piece of memory is called a buffer. Simply put, a buffer is just a chunk of computer memory that is set aside by a program to store the value of a variable so that it can call upon that value when it is needed.
Now that you have the general idea of what a buffer is, let us describe how a buffer overflow works. Note the accompanying picture of a sample buffer (Figure 5-3), which can be thought of as part of our CD rack. As you can see, this stack should have both Oldies (1-100) and Classical (101-200) CDs in the slots. For the point of this example, let us consider this to be your friend’s CD collection. Since you hate all oldies, classical, and pop rock, how can you trick your friend into playing your rock CD?
What do you know about your friend’s CD setup? You know the layout of his CD rack: the 1-100, 101-200, and 201-300 slot separation. You also know that your friend’s Oldies section (1-100) is almost full, with only 4 open slots (97-100), and you know that his Classical section is completely empty. Using this information to your advantage, you could give your friend a five-CD set of Barry Manilow (whom we’re considering an oldies singer, for the sake of this example), which has your rock CD concealed in the place of CD number five. Assuming your friend does not pay any attention to the slot number into which he places the gift, your rock CD would end up in slot 101. Now, you simply have to ask your friend if he would be so kind as to play something from his Classical collection. Your friend would check the slot numbers, see that there is one CD in the Classical section, and grab it. Much to his surprise, hard-core rock would come streaming out of the speakers instead of Beethoven.
This is similar to the way a hacker performs a buffer overflow attack on your computer. First, the hacker needs to find a program that you are running that has a buffer overflow vulnerability. Even if the hole does not allow the execution of malicious code, it will most likely crash the target computer. A hacker also needs to know the exact size of the buffer he is trying to overflow. In the CD rack case, it was just a matter of providing five CDs, which was one too many for the Oldies segment. For a computer, it is often just as easy.
Ideally, a well-written program will not allow anything to overflow: it’s the same as having three separate CD racks that have 100 slots each, instead of having one 300-slot CD rack. If your friend had three separate racks, he probably would have noticed that there was one CD too many in his Oldies collection and taken action to resolve the problem. This would have led him to discover your rock CD hidden in the gift.
The next part of a buffer overflow attack is to launch the payload . The payload is usually a command to allow remote access, or some other command that would get the hacker one step closer to owning the target computer. For example, Microsoft’s Internet Information Server had a buffer overflow vulnerability that allowed a hacker to make a copy of any file and place it in a location on the web server. This file could be anything that would allow remote access, from passwords to an executable file.
A successful buffer overflow hack is difficult to execute. However, even if the buffer overflow fails somewhere during its execution, it will most likely cause problems for the target. A failed buffer overflow attack often results in a program crash or, better yet, a computer crash. The program that originally allocated the segment of memory that was overwritten will not check to see if the data has changed. Therefore, it will attempt to use the information stored there and assume it is the same information it had placed there previously. For example, when the program goes to look for a number that is used to calculate the price of tea, and instead it gets the word “Bob”, the program will not know what to do.
This section describes a typical buffer overflow. Figure 5-4 shows an example of a stack structure after a function is called. The stack pointer points at the top of the stack, which is at the bottom in the figure.
C++ uses the area at the top of the stack in the following order: local variables, the previous frame pointer, the return address, and the arguments of the function. This data is called the frame of the function, and it represents the status of the function. The frame pointer locates the current frame, and the previous frame pointer stores the frame pointer of the calling function.
When an attacker overflows a buffer on the stack (e.g., with extra input), the buffer will grow toward the return address. The hacker is attempting to change the return address. When the function executes, the return address is popped off the stack and the new address is executed. By overwriting this address, a hacker attempts to take control of the processor. If malicious code is located at the address, it is executed with the same privilege level as the application.
Because of increased publicity, as well as the prevention techniques mentioned in the next section, buffer overflows are becoming less frequent in well-designed code. Consequently, we can expect to see heap overflow exploits becoming more common.
The heap refers to memory that is dynamically allocated by an application for variable storage. In a heap overflow , the hacker attempts to overwrite variables such as passwords, filenames, and UIDs in the heap.
What is the difference between a buffer overflow and a heap
overflow? In a buffer overflow, we are attempting to execute
machine-level commands by overwriting the return address on the stack.
In contrast, a heap overflow attempts to increase the level of system
privilege by overwriting dynamically stored application variables.
Heap overflow exploits include format bugs and malloc()
/free(
)
overwrites.
Researchers have also come to recognize a related class of
overflows known as format bugs. The vulnerability
caused by format bugs is that in C, a %n
format token exists for printf format
strings that commands printf to write back the number of bytes
formatted so far to the corresponding argument to printf, presuming
that the corresponding argument exists and is of type int *
. This can be exploited if a program
permits unfiltered user input to be passed directly as the first
argument to printf. The varargs
mechanism of C++ allows functions (e.g., printf) to accept a variable
number of arguments by “popping” as many arguments off the call stack
as they wish, trusting the early arguments to indicate how many
additional arguments (and of what type) are to be popped. The fix to
this problem is to use printf("%s",
buf)
instead of printf(buf)
.
The ideal way to prevent buffer overflows is for the programmer to follow proper programming practices. These include the following:
Always check the bounds of an array before writing it to a buffer.
Use functions that limit the number and/or format of input characters.
Avoid using dangerous C functions such as the following:
scanf( )
, strcpy( )
, strcat( )
, getwd( )
, gets(
)
, strcmp( )
,
sprintf( )
.
How can we prevent buffer overflows in practice? Programmers use
the strcpy( )
function to copy a
source string to a destination buffer. Unfortunately, the destination
array may not be large enough to handle the source string. If your
user inputs a very long source string, she will be able to force a
buffer overflow on the destination.
To prevent this error, you can specifically check each source
string for length before copying it. However, a simpler alternative is
strncpy( )
. This function is
similar to strcpy( )
, except that
in strncpy( )
only the first
n bytes of the source string are copied, which
helps to prevent a buffer overflow.
There has never been a programmer born who can code without error 100% of the time. Thus, we now examine automated tools for testing overflow conditions.
Until recently, there has been a paucity of effective tools for automated source code level auditing for buffer overflows. This is because it is horribly difficult to take into account all of the possible errors inherent in a program that is thousands of lines long.
One commercial example is PolySpace (http://www.polyspace.com), which has come up with a tool to detect buffer overflows in ANSI C applications at compilation time. While the Viewer module currently can be run on Windows, the Verifier itself requires a Linux box to run. Windows-only programmers will have to break down and install a dedicated Linux box to run PolySpace as a batch tool; the results can then be explored under Windows. If you currently do not run Linux, we recommend doing so immediately; a true security expert should be able to move between Windows and Linux with ease. However, for those who are completely Linophobic, PolySpace has started porting the Verifier engine to Windows.
Linux provides various compiler add-ons and libraries that perform runtime bounds checking in C/C++. StackGuard (http://immunix.org) is one example. StackGuard detects stack smashing attacks by protecting the return address on the stack from being altered. It places a “canary” word next to the return address when a function is called. If a buffer overflow is attempted, StackGuard detects that the canary word has been altered when the function returns. If this happens, the StackGuarded program logs an adminstrator alert and terminates.
StackGuard is implemented as a small patch to the gcc code
generator in the function_prolog()
and function_epilog( )
routines. StackGuard
utilizes function_prolog( )
to
insert canaries on the stack when functions start, then uses
function_epilog( )
to check
canary integrity when the functions exit. It can thus detect any
attempt at corrupting the return address before the function
returns.
Another useful program from immunix.org is FormatGuard, which guards against format bug exploits. FormatGuard uses the ability of C++ to distinguish macros with identical names but a different number of arguments. FormatGuard provides a macro definition of the printf function for each of anywhere from 1 to 100 arguments. Each of these macros in turn calls a safe wrapper that counts the number of % characters in the format string and rejects the call if the number of arguments does not match the number of % directives.
In order for an application to be protected with FormatGuard,
the application needs to be recompiled against the FormatGuard glibc
headers. FormatGuard is a wrapper around the following libc calls:
syslog( )
, printf( )
, fprintf( )
, sprintf( )
, snprintf( )
.
Another way to prevent buffer overflows is to make the stack and data memory nonexecutable. This is not a complete solution, as it still allows the attacker to make the code jump into unexpected positions. However, it does make exploits more difficult to execute. This solution is available in Linux in the form of a patch.
Automatic bounds-checking tools can add another layer of protection to the above techniques. Unlike C++, Perl and Java provide innate bounds checking, saving the programmer from extensive security coding. However, automatic bounds checking can also be provided for C++ using tools under various operating systems. Examples of such tools include BOWall, Compuware’s Boundschecker, and Rational’s Purify.
Now that you have reviewed buffer overflows, the following example will let you test what you have learned using a special crackme (test application).
For this example, we use a Windows-based buffer overflow crackme named weird.exe . You may download the executable from our web site at http://www.securitywarrior.com.
The Analyst first posed this little crackme, and the solution is reprinted with permission from the publisher (+Tsehp). When you run the program, you will see a command-line program asking you to enter the serial number to unlock the program (Figure 5-5). However, you do not know the serial number. You have to guess it. If you guess correctly, you get a “congratulations” message.
First try entering a serial such as “IOWNU”. Since it is incorrect, there will be no response. Hitting Return again closes the program. After trying a few guesses, you will quickly realize that there’s a better way to find the correct serial. You can try writing a program to brute force it, but that’s not very elegant. It is time to fire up our Windows reverse engineering tools. Please note that the only rule in this puzzle is that you are not allowed to perform any opcode patching on the target .exe.
Using the reverse engineering techniques from the previous chapters, we will solve this crackme and find the correct serial. The tools you need are as follows:
Knowledge of x86 assembly language
A disassembler such as IDA or W32DASM
A hex-to-ASCII converter
First, open IDA and disassemble weird.exe. We go straight to the Strings window and find the “congratulations” string (Figure 5-6). Double-clicking this takes us to the target code (Figure 5-7).
There is no hard and fast rule on how to approach cracking an application. RCE is more of an art than a science, and it often depends on luck and intuition just as much as skill and experience. In this case, we choose to start at the “congratulations” string section of code just because it looks like a promising starting point.
Our target code is as follows:
CODE:00401108 push ebp CODE:00401109 mov ebp, esp CODE:0040110B add esp, 0FFFFFFB4h ; char CODE:0040110E push offset aTheAnalystSWei ; _ _va_args CODE:00401113 call _printf ; print some text. CODE:00401118 pop ecx CODE:00401119 push offset asc_40C097 ; _ _va_args CODE:0040111E call _printf ; same CODE:00401123 pop ecx CODE:00401124 push offset aEnterYourSeria ; _ _va_args CODE:00401129 call _printf ; same again CODE:0040112E pop ecx CODE:0040112F lea eax, [ebp+s] ; buffer CODE:00401132 push eax ; s CODE:00401133 call _gets ; get entered serial CODE:00401138 pop ecx CODE:00401139 nop CODE:0040113A lea edx, [ebp+s] CODE:0040113D push edx ; s CODE:0040113E call _strlen ; get its length CODE:00401143 pop ecx CODE:00401144 mov edx, eax CODE:00401146 cmp edx, 19h ; is it less than 25? CODE:00401149 jl short loc_401182 ; yes CODE:0040114B cmp edx, 78h ; is it more than 120? CODE:0040114E jg short loc_401182 ; yes CODE:00401150 mov eax, 1 ; eax = 1 , initialize loop CODE:00401155 cmp edx, eax ; all chars done? CODE:00401157 jl short loc_40115E ; no, let's jump CODE:00401159 CODE:00401159 loc_401159: ; CODE XREF: _main+54j CODE:00401159 inc eax ; eax = eax + 1 CODE:0040115A cmp edx, eax ; all chars done? CODE:0040115C jge short loc_401159 ; no, let's loop CODE:0040115E CODE:0040115E loc_40115E: ; CODE XREF: _main+4Fj CODE:0040115E mov eax, 7A69h ; eax = 31337 CODE:00401163 test eax, eax CODE:00401165 jnz short loc_401182 ; jump quit CODE:00401167 cmp eax, 1388h CODE:0040116C jl short loc_40118 ; jump quit CODE:0040116E cmp eax, 3A98h CODE:00401173 jg short loc_401182 ; jump quit CODE:00401175 jmp short loc_401182 ; jump quit CODE:00401177 ; ------------------------------------------------------------------ CODE:00401177 push offset aWooCongrats ; _ _va_args ; good msg CODE:0040117C call _printf CODE:00401181 pop ecx CODE:00401182 CODE:00401182 loc_401182: ; CODE XREF: _main+41j CODE:00401182 ; _main+46j ... CODE:00401182 call _getch ; wait till a key is pressed CODE:00401187 xor eax, eax CODE:00401189 mov esp, ebp CODE:0040118B pop ebp CODE:0040118C retn
There is a trick in the code. It turns out that there is no way to get to the “congratulations” message! A quick look shows us that there’s no cross-reference to our congratulations, but rather some jumps that go directly to the end of the crackme. That’s odd (but then, the crackme is called weird.exe).
It turns out that the only way to solve this puzzle is by forcing a buffer overflow in order to execute the “congratulations” code. In other words, we are going to craft the serial number itself in just such a way as to force a buffer overflow into the code that we want. We are going to have to exceed the buffer in exactly the correct way to insert the serial number manually on the stack and force it to execute. Thus, the serial number itself is the payload.
The first step is to check the buffer and its size:
CODE:0040112E pop ecx CODE:0040112F lea eax, [ebp+s] ; buffer CODE:00401132 push eax ; s CODE:00401133 call _gets ; get entered serial CODE:00401138 pop ecx CODE:00401139 nop CODE:0040113A lea edx, [ebp+s] CODE:0040113D push edx ; s
This shows us that eax
is
pushed on the stack, just before a call to the gets()
function.
We can demonstrate what is happening using the following snippet of C code:
-------------------------------------------------------------------------------- #include <stdio.h> #include <string.h> #include <conio.h> #include <iostream.h> int main( ) { unsigned char name[50]; gets(name); } --------------------------------------------------------------------------------
As we can see, there is a buffer called “name”, which is 50 bytes long.
We then use gets
to input our
data into name
. We defined it as 50
characters long, but what would happen if we type in 100 characters?
That should yield a nice overflow.
We now have to check how big our buffer is. According to IDA, it is 75 characters long.
First, we look at our stack parameters:
CODE:00401108 s = byte ptr -4Ch CODE:00401108 argc = dword ptr 8 CODE:00401108 argv = dword ptr 0Ch CODE:00401108 envp = dword ptr 10h CODE:00401108 arg_11 = dword ptr 19h
Thus, we can be confident that the maximum size of the buffer is 75 characters.
Let’s test this theory, and enter something like 80 characters :
-- The analyst's weird crackme -- --------------------------------- enter your serial please: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
As expected, our program crashes nicely. No wonder, since we entered a string of 80 characters, which is five characters more than the maximum size of the buffer. Having a look at the registers, we can see that EBP = 41414141h. This is interesting. 41h is the hexadecimal ASCII value of “A”. Thus, we have just overwritten the base pointer (EBP). So far so good, but ideally, we want to overwrite EIP. Overwriting EIP allows us to execute any code we want.
Next, we try entering 84 characters, to see what happens:
-- The analyst's weird crackme -- --------------------------------- enter your serial please: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Okay, we still get a nice crash, but now we get the following:
instruction at the address 41414141h uses the memory address at 41414141h. memory cannot be read.
Thus, we see that the program tries to execute code at 41414141h.
What would happen if we replaced our return address with something besides 41414141h? Say, for example, something like the “congratulations” message address?
CODE:00401177 push offset aWooCongrats ; _ _va_args ; good boy CODE:0040117C call _printf
Thus, we know that if we put 401177 as our return address, we will have solved the crackme by printing the “congratulations” message on the screen.
However, before we do that, let us test with a tagged serial such as:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234
We see that this string crashes the program at address 34333231 (Figure 5-8).
This demonstrates that we have to reverse the byte order when delivering our payload. Why? As you can see, the address at which we have crashed is the reverse order of the hex equivalent of our ASCII serial.
Let us diverge from our example for a moment to explain this backward ordering. The reversed order is necessary on x86 processors because they are little-endian. The term “endian” originally comes from Jonathan Swift’s Gulliver’s Travels. In this satirical tale, the people of two different cities cracked their hard-boiled eggs open on different ends. One city cracked the big end of the egg, while the other city cracked the little end of the egg. This difference led to war between the two cities.
In computer processors, “big-endian” and “little-endian” refer to the byte ordering of multibyte scalar values. The big-endian format stores the most significant byte in the lowest numeric byte address, while the little-endian format stores the least significant byte in the lowest numeric byte address. Thus, when manipulating byte values, you need to know the order in which the specific processor reads and writes data in memory.
Returning to our example, the hex equivalent of 1-2-3-4 is 31-32-33-34. If we reverse 1-2-3-4 to get 4-3-2-1, that is the equivalent of reversing 31-32-33-34 to get 34-33-32-31, and we know 34333231 is the address of our crash. Thus, to successfully exploit the program, we have to also reverse the order of the memory address we want to inject. In other words, to execute 401177, we must place 771140 on the stack. We know that 771140 in ASCII is equivalent to w^Q@ ( the ^Q is Ctrl-Q).
We now try to enter it into the program:
-- The analyst's weird crackme -- --------------------------------- enter your serial please: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA w^Q@
Pressing the Return key gives us the desired “congratulations” message (Figure 5-9):
wOO! congrats ;)
You have now successfully exploited a buffer overflow to execute instructions that you injected onto the stack. In this case, you inserted a memory address that gives you access to a location in the program that you never should have been able to access.
Now that you have solved it, you can examine the source code of the Analyst’s crackme:
#include <stdio.h> #include <string.h> #include <conio.h> #include <iostream.h> int main( ){ int i,len,temp; unsigned char name[75]; unsigned long check=0; printf("-- The analyst's weird crackme -- "); printf("--------------------------------- "); printf("enter your serial please: "); gets(name); asm{ nop}; len=strlen(name); //cout << len; if (len < 25) goto theend; if (len > 120 ) goto theend; for (i=1; i <= len ; i++) { temp += name[i] ; } if (temp = 31337) goto theend; if (temp < 5000) goto theend; if (temp > 15000) goto theend; goto theend; printf("wOO! congrats ;) "); theend: getch( ); return 0;
Building Secure Software: How to Avoid Security Problems the Right Way, by John Viega and Gary McGraw. Addison-Wesley Professional, 2001.
The Analyst’s weird crackme, published by +Tsehp, 2001.
Smashing the Stack for Fun and Profit, by Aleph One. Phrack issue #49-14, November 1996. (http://www.phrack.org)
“Endian Issues,” by William Stallings. Byte.com, September 1995.