Chapter 8. Buffer Overflows and Stack and Heap Manipulation

Buffer overflows (also referred to as buffer overruns) are a specific class of array and numeric bounds errors. They are one of the longest-running, costliest security vulnerabilities known to affect computer software. Understanding the core concepts behind buffer overflows can be fun and rewarding.

What is a buffer overflow, exactly? In simple terms, most buffer overflows take place when the input is larger than the space allocated for it, but it is written there anyhow and memory is overwritten outside the allocated location. When the data is written, it can overwrite other data in memory. In some cases, overflows result from incorrect handling of mathematical operations or attempts to use or free memory after the memory has already been unallocated. At first (casual) glance, overflowing the buffer can seem like a relatively harmless bug. However, overflows are almost always harmful. Buffer overflows typically result in enabling attackers to run whatever code they want to take control of the target computer.

Note

Programming languages that allow for direct memory access (such as assembly, C, or C++) and those that do not provide bounds checking on buffers and numeric operations are particularly vulnerable to overflow attacks. Even languages such as Java (Java Native Interface) and C# (which contains unsafe code blocks and unmanaged application programming interface calls) include provisions for running external components that can be vulnerable to overflows. For some languages, risk of an overflow is reduced considerably by remaining within the language because the language helps perform memory validation on behalf of the programmer. But using safer languages should not substitute for the good programming practice of validating untrusted input. (Consider, for example, an overrun in the Netscape 4 Java Runtime engine. For more information, see http://secunia.com/advisories/7605/.)

Buffer overruns have been around for a long time. In November 1988, Robert Morris launched a computer program that used known buffer overruns in UNIX services to locate and infect other computers. This became known as the Morris Worm. The Morris Worm attracted attention to security problems such as buffer overruns, so the problems have gone away, right? Wrong.

In January 2003, what began with sending a single little User Datagram Protocol (UDP) network packet reached epic proportions within minutes. Planes were grounded. People in some areas picked up their phones to no dial tone. ATMs stopped working. Networks across the globe and even portions of the Internet shut down. The president of the United States was notified. This wasn’t a joke: it was called SQL Slammer and Sapphire. Then the costly mess had to be cleaned up, and questions surfaced. What had happened? Microsoft SQL Server contained a buffer overflow security vulnerability that was remotely exploitable—and someone actually exploited it. For all of the damage SQL Slammer actually caused, its primary action was to infect a vulnerable machine and then turn around and randomly try to infect other IP addresses. Just imagine what would have happened if the intentions of the worm’s author were more sinister.

More Info

For more information about how this particular overflow works, see http://www.blackhat.com/presentations/bh-usa-02/bh-us-02-litchfield-oracle.pdf.

Consider another buffer overrun, this time in a Web server. According to Microsoft Security Bulletin MS01-033 (http://www.microsoft.com/technet/security/bulletin/MS01-033.mspx), “idq.dll contains an unchecked buffer in a section of code that handles input URLs. An attacker who could establish a web session with a server on which idq.dll is installed could conduct a buffer overrun attack and execute code on the web server. Idq.dll runs in the System context, so exploiting the vulnerability would give the attacker complete control of the server and allow him to take any desired action on it.” Sure enough, this overrun was exploited. Much like SQL Slammer, what was to be dubbed Code Red used the overflow to spread from machine to machine. Unlike SQL Slammer, however, Code Red also defaced the main Web site page and tried to execute a distributed denial of service attack on the http://www.whitehouse.gov domain (see http://www.eeye.com/html/Research/Advisories/AL20010717.html).

The attackers didn’t stop at spreading the worm and defacing Web sites. Another variant appeared, called Code Red II, which also opened up a number of additional security holes on systems, allowing others to run code with SYSTEM (equivalent to root) privileges even if the original overflow was patched (see http://www.eeye.com/html/Research/Advisories/AL20010804.html). Code Red II was so successful at opening up a machine to further attacks that other worms such as Nimda later used the vulnerabilities opened up by Code Red II to spread (see http://www.microsoft.com/technet/security/alerts/info/nimda.mspx).

Overrun vulnerabilities are not limited to network attacks or to particular platforms, devices, or vendors. Consider, for instance, a bug in both MacOS and Microsoft Windows iTunes clients that allows maliciously crafted URLs in playlist files to run code of an attacker’s choice on the victim’s machine when the playlist is processed (http://www.kb.cert.org/vuls/id/377368). Another example is a bug that occurs when specifying a filename longer than 98 characters in the tbtftp utility provided in Affix, a Bluetooth protocol stack written for Linux (http://www.digitalmunition.com/DMA%5B2005-0712a%5D.txt). In mid-2005, an overrun was reported in ZLIB, a compression library broadly used in the computer industry. The ZLIB compression library is used for compressing many kinds of data and is popular on many platforms and in many applications because it is royalty-free and fairly feature-rich. In this particular case, an attacker could specify the compressed data, and on decompression the ZLIB routine would write data out of bounds (http://www.kb.cert.org/vuls/id/680620). Buffers overruns regularly make headlines because they are serious issues that affect all computer users and software developers.

Because overruns are such effective tools in an attacker’s arsenal, it is interesting delving into how to find them. Finding buffer overflows isn’t just about inserting long strings in places and looking for crashes. This chapter details different types of overflows and how they work, shows some places to look for overflows, digs in for deep coverage of how to find overflows, covers determining overflow exploitability, and finishes off by discussing a few special topics. The testing approach details both structural (white box) and functional (black box) testing techniques and provides optional walkthroughs for those who are interested in expanding their knowledge even more.

Warning

Attackers are clever and will likely try actions the tester didn’t think were important to test—so thoroughness in testing is a good thing. New tools and techniques are continuing to emerge that will change what attackers have available to them long after the press used to print this page grows cold. The cost of shipping these bugs is often more than the cost of doing a decent job of finding and fixing them in the first place. It pays to budget accordingly and be resourceful in tackling overflows.

Before this chapter digs in and gets specific, take a minute to brainstorm about what your features do. Consider what sorts of places attackers can specify the data. What would be the most valuable prize? If the application takes data files from untrusted places and parses them, the data files would be great places to test for overruns. For network applications, look at what the target application receives over the wire as a big source of potential overruns. Another area for overruns is public (or private) application programming interfaces (APIs) that can be reused. A lot of the security work done with a given parameter on each API might depend on the API’s design—APIs designed to take unsigned buffer lengths are much clearer for the caller to use and hence less prone to buffer overflows. Some system data should not be trusted, such as Clipboard data that could come, say, through the Web browser from a malicious Web server using the scriptable Clipboard commands. Any code that runs with high privileges or has access to important information would be a good target because using the overflow would enable attackers to run commands using those high privileges or obtain the details of the sensitive information. Some examples of good targets might include authentication code, code that protects private cryptography keys, and other sensitive operations. Fasten your seatbelt and begin to think more about how attackers can maliciously shove data into the software to unleash its full potential.

Understanding How Overflows Work

This section describes several different overflows, how they work, and basic walkthroughs to demonstrate that they are serious security issues.

Suppose you are testing an application that takes a stock symbol and looks up the current trading price. You enter a few common stock symbols and verify the right price is returned for each stock. Next, you try getting prices on stock on different exchanges to make sure the application works, but you don’t stop when you have tested valid input. You continue to test a few nonexistent symbols to see what happens. In addition to the null case, invalid characters, and all of the other basic test cases, you try a long string. After you input the long string, the service is unresponsive. What happened? When you look on the server you see the service crashed and created a crash log. You restart the service, send in the long string again, and down it goes again (crash). You report the behavior as a bug and mention the bug over lunch. One person says that it’s a cool way to shut down the service. Another person overhears the conversation, interrupts, and comments that you can probably do a lot more than just taking down the service—the bug is quite likely a buffer overflow and someone can use it to take control of the server. But how can that happen? Let’s examine how overflows work and why this long input bug is much more serious than just a crash.

Although many overflows occur when the program receives more data than it expects, in fact there are many different kinds of overflows. It is important to distinguish between different classes of overflows to be able to develop good test cases to identify specific types of overflows:

  • Stack overflows. Stack overflows are overflows that occur when data is written past the end of buffers allocated on the stack.

  • Integer overflows. Integer overflows occur when a specific data type or CPU register meant to hold values within a certain range is assigned a value outside that range. Integer overflows often lead to buffer overflows for cases in which integer overflows occur when computing the size of the memory to allocate.

  • Heap overruns. Heap overruns occur when data is written outside of space that was allocated for it on the heap.

  • Format string attacks. Format string attacks occur when the %n parameter of the format string is used to write data outside the target buffer. This particular attack is covered in Chapter 9.

Stack overflows, integer overflows, and heap overruns are discussed in this chapter. Format string attacks, however, are discussed in Chapter 9.

Note

Throughout this chapter we use Microsoft Visual Studio as the primary debugger, source code editor, and binary file editor. You should use the tools you are most comfortable with, provided they can do the tasks described in this chapter. Although the computer processor register names and sizes often vary between processor types and manufacturers, this text doesn’t delve into the many processor-specific issues that can arise when actually exploiting or fixing bugs—that’s not what this book is about. For consistency, the text refers to processor registers and sizes consistent with Intel-compatible 32-bit processors; it is certainly the case that other processors generally have the same issues despite their registers’ size or nomenclature.

Stack Overflows

To understand stack overflows, let’s first examine how the stack works. The stack works like short-term memory—it stores information needed by the computer to process a given function. When calling a function, the program first places (or pushes) the various parameters needed to call that function on the stack. The computer processor keeps track of the current location on the stack with a special processor register called the extended stack pointer (ESP). This step is shown in Figure 8-1. In the figure, the arguments for the function were already pushed onto the stack. Notice that in these stack layouts, lower elements on the layouts correspond to higher memory addresses.

A stack just before a function is called

Figure 8-1. A stack just before a function is called

The actual function call then places the return address on the stack. The return address reminds the computer processor where to run the next code when it is finished running the code in this particular function. This is illustrated in Figure 8-2. Notice how the stack pointer decreases every time something is pushed onto the stack.

Once the function is running, it usually places additional data on the stack. The stack then looks like the one shown in Figure 8-3, where, once called, the function begins to push local variables onto the stack. Like the return address in Figure 8-2, pushing more onto the stack decreases the value of the stack pointer.

The return address placed on the stack

Figure 8-2. The return address placed on the stack

The function pushing local variables onto the stack

Figure 8-3. The function pushing local variables onto the stack

Some of this data might include a buffer that an attacker can potentially overflow. Normal testing and usage input are shown in Figure 8-4. When one of the function’s local variables, a buffer, is overwritten with normal input, the input is usually copied sequentially into the buffer as shown.

Once the function is finished running, it removes (or pops off) the local variables from the stack. (Note that the values contained within the functions are typically still in memory.)

Important

Look at the input in Figure 8-4. Suppose some of this data included a buffer that an attacker could potentially overflow. In that case, other values on the stack would be overwritten also, as shown in Figure 8-5.

Then the computer processor runs the code at the return address so the caller can resume where it left off. What happens in the overflow case, however? If the input is longer than the space provided by the stack buffer but the function copies the data anyway, other stack variables and the return address are overwritten. See Figure 8-5 for a view of how the stack looks for the overflow case.

Input copied sequentially into the buffer

Figure 8-4. Input copied sequentially into the buffer

Overwriting data outside the allocated space

Figure 8-5. Overwriting data outside the allocated space

When the data is overwritten past the allocated space, it overwrites other stack variables as well as the return address. What can an attacker do with the return address to try to exploit this? For a hint, see Figure 8-6. If the attacker can control the long data, the attacker can specify long data that includes a return address or other items on the stack crafted to enable the attacker to control the machine. Notice in the figure how the attacker can adjust where in the input string the new return address appears so that it overwrites the old return address in the correct location on the stack.

Inserting the return address supplied by the attacker

Figure 8-6. Inserting the return address supplied by the attacker

This attack can overwrite the return address to point to anywhere in memory. The attacker can also send in malicious code as part of this input. Then the attacker can overwrite the return address and point it at the malicious code that is in memory. What happens when the function returns? Just like in the normal case, the variables are popped off the stack (the stack pointer ESP moves to the return address).

Then the computer processor looks at the return address listed (now overwritten) and begins to run code as specified by the attacker instead of the code that was originally listed. If attackers can somehow direct the processor to run code anywhere, they certainly can get part of their data run as code. Once the code runs, the victim is no longer 100 percent in control of the computer.

More Info

The paper “Smashing the Stack for Fun and Profit” at http://www.phrack.org/show.php?p=49&a=14 provides a good discussion of the basics of how a stack and stack overflows work.

So what if an attacker can’t overwrite the stored address? It turns out there are other interesting pieces of data besides the return address that attackers can overwrite to produce exploitable overruns. The extended instruction pointer (EIP) is a register the processor uses to keep track of which instruction is to be run next. The stack frame pointer (EBP) is another processor register that eventually controls ESP and subsequently EIP. Exception handler routines are usually pushed onto the stack, and if attackers can overwrite them, they can also likely create an exception that can subsequently run their code. If there is a structure that contains a function pointer that is called after the overrun, that works as well.

Integer Overflows

Essentially, an integer overflow (also called a numeric overflow) results when the numeric data type designated to handle an operation fails to handle the data in an expected way when input numerically extends beyond the space available for that data type. For example, suppose we use a 1-dozen egg carton to store data, and we want to add 9 eggs plus 9 eggs. The egg carton holds only 12 eggs, so the groups of 9 eggs fit individually, but when the computer processor tries to add them together and store them in the egg carton, it has 6 eggs left over and cannot figure out how to fit the data.

Similarly, if a programmer tells the computer to store a number in a short integer data type, what happens when the calculation 25,000 + 25,000 is done? The short integer data type is a signed data type that holds 2 bytes (16 bits) worth of data, as illustrated in Figure 8-7 and Figure 8-8. In Figure 8-7, each zero represents one bit in this unsigned short data representation. In Figure 8-8, the leftmost zero represents the high-order bit in the signed short data representation. Like the unsigned short shown in Figure 8-7, the signed short data type uses 2 bytes of memory. The fact it is signed means the high-order bit (leading bit on the left in Figure 8-8) indicates whether the number is positive or negative. This system of storing numbers shown in Figure 8-8 is called two’s complement.

Unsigned short data representation

Figure 8-7. Unsigned short data representation

Signed short data representation

Figure 8-8. Signed short data representation

With 15 bits, 32,768 (2^15) numbers can be represented. By using the leading bit, we can double this because we can represent positive and negative numbers and zero. As you can see in Table 8-1, a given signed data type can represent one more negative numbers than positive because zero has the leading bit cleared.

Table 8-1. Signed Short Number Limits

Binary

Hexadecimal

Decimal

Comment

0000 0000 0000 0000

0x0000

0

 

0111 1111 1111 1111

0x7FFF

32767

Largest positive number

1000 0000 0000 0000

0x8000

–32768

Largest negative number

1111 1111 1111 1111

0xFFFF

–1

 

What does the following code print out?

short int a = 25000;              
short int b = 25000;              
short int c = a + b;              
char sz[50];                      
itoa(c,sz,10);                    
printf("%s",sz);                  

It prints out –15536 because 25,000 plus 25,000 is too big a number to fit in 15 bits, so the math overflows and changes the sign. You’ll see another example of how this applies to memory allocation later, but in the meantime let’s say an attacker wants to try to get some free merchandise from an online jeweler. The jeweler advertises one item for $2,500.00. The attacker orders 17,180 of the item—costing a grand total of $327.04! How did that happen? Well, let’s take a look at how the merchant implemented its shopping cart.

//Cost per item                                             
unsigned long ItemCostPennies= 250000;                      
//Simulated input from user                                 
//Number of items                                           
unsigned long NumberOrdered= 17180;                         
//Compute the cost.                                         
double TotalCost = ((double)                                
   (ItemCostPennies * NumberOrdered)) / 100;                
printf("Transaction will cost $%.2f",TotalCost);            

The malicious input comes in as unsigned long values, and the total cost is then computed and charged to the customer’s credit card. The first thing the computer processor does is take the ItemCostPennies (250000) and multiply by the quantity ordered (17180). This gives 4,295,000,000. Because both ItemCosePennies and NumberOrdered are long data types, the computer processor tries to store the result of the multiplication in a long. An unsigned long data type (4 bytes, or 32 bits) can hold only 2^32 (4,294,967,296) unique values.

Figure 8-9 shows what happens when the computer tries to process the binary equivalent of 4,295,000,000 using an unsigned long data type with only 32 bits of space. Notice that the binary equivalent of 4,295,000,000 uses 33 bits, so the leading bit doesn’t fit. The computer processor drops the leading bit and has a remaining 32,704 left over that it carries forward. That figure is converted to a double (approximately 32,704.000000000002) and is divided by 100 (approximately 327.04000000000002). Just think: if the merchant’s shopping cart code didn’t represent the pennies, the attacker would have had to order 100 times as many items to accomplish the same result.

Using an unsigned long data type with only 32 bits of space to process the binary equivalent of 4,295,000,000

Figure 8-9. Using an unsigned long data type with only 32 bits of space to process the binary equivalent of 4,295,000,000

The problem isn’t just limited to obvious numerical operations. It can also take place with memory allocation—after all, the size of the allocation is based on numbers.

What happens when buffer allocations are done based on the results of these errors? Allocate 32,704 bytes and copy 4,295,000,000 bytes into the buffer? That’s a recipe for an overrun. Follow along with the next program as we consider another example. Using Microsoft Visual Studio, start a new C++ Win32 console application project. (Note: Please use debug to follow along because retail isn’t as clear.)

#include <iostream>                                                                   
#include <tchar.h>                                                                    
#include <conio.h>                                                                    
                                                                                      
//Prints the data to the console window                                               
//Callers note: iLength needs to include                                              
// the '' (null) terminator.                                                        
void PrintIt(char *szBuffer, short iLength) {                                         
                                                                                      
   //If there is too much data, let's just quit.                                      
   if (iLength>2048) return;                                                          
   char szBufferCopy[2048];                                                           
   //Otherwise, we can copy the data.                                                 
   memcpy(szBufferCopy,szBuffer,iLength);                                             
   //And send it out to the console window,                                           
   //waiting for a key press.                                                         
   printf("%s
Press the key of your choosing");                                      
   printf(" to continue. . .",szBufferCopy);                                          
   while (_kbhit()) {_getch();} //flush input buffer                                  
   while (!_kbhit()); //wait for keyboard input                                       
   printf("
");                                                                      
}                                                                                     
int _tmain(int argc, _TCHAR* argv[]) {                                                
   //Allocate room for the incoming data.                                             
   const unsigned short uiLength = 26;                                                
   char *szInputFileLen = (char*)malloc(uiLength);                                    
   if (!szInputFileLen) return(1);                                                    
                                                                                      
   //Grab the data (simulated).                                                       
   memcpy(szInputFileLen, "Simulated untrusted data.", uiLength);                     
                                                                                      
   PrintIt(szInputFileLen,uiLength);                                                  
   free(szInputFileLen);                                                              
   return 0;                                                                          
}                                                                                     

Assume you are examining an application containing code such as the previous listing. Untrusted input comes into the variable szInputFileLen, which is simulated using the following lines in function _tmain:

//Grab the data (simulated).                                                           
memcpy(szInputFileLen,                                                                 
   "Simulated untrusted data.",uiLength);                                              

Run the program as shown in the following graphic, and watch what happens to see how it works.

Using an unsigned long data type with only 32 bits of space to process the binary equivalent of 4,295,000,000

Pressing any key on the keyboard ends the simple program. If this really is untrusted data, it might be larger. Simulate this larger data by filling up szInputFileLen with 0x61 bytes. To do this quickly, and for the sake of illustration, add a memset function call to fill up the memory with 0x61 bytes (0x61 is a lowercase letter a).

int _tmain(int argc, _TCHAR* argv[]) {                                 
   //Allocate room for the incoming data.                              
   //Note that 35000 is too big for the                                
   //unsigned short data type.                                         
   const unsigned short uiLength = 35000;                              
   char *szInputFileLen = (char*)malloc(uiLength);                     
   if (!szInputFileLen) return(1);                                     
                                                                       
   //Grab the data (simulated).                                        
   memset(szInputFileLen,0x61,uiLength-1);                             
   //Add the null to terminate the string.                             
   szInputFileLen[uiLength-1] = '';                                  
                                                                       
   PrintIt(szInputFileLen,uiLength);                                   
   free(szInputFileLen);                                               
   return 0;                                                           
}                                                                      

Don’t forget the ending null and to adjust the PrintIt function’s parameter. You are now simulating a long string of 34,999 a characters (0x61) followed by a null. The following graphic shows what happens when this code is run:

Using an unsigned long data type with only 32 bits of space to process the binary equivalent of 4,295,000,000

The warning message shown in the graphic claims the program is trying to read at memory location 0x61616161, which is the data you entered. When you click the Break button, the debugger shows that you are actually trying to run code at address 0x61616161.

Note

When breaking into the debugger, Visual Studio might present you with other dialog boxes. One dialog box appears, for example, when Visual Studio is unable to find the source code corresponding to the current value of EIP. To follow along with the examples presented in this book, you should click the Show Disassembly button when encountering this dialog box. For more information about a specific dialog box you encounter, refer to the Visual Studio documentation.

How did that happen? Let’s step through this carefully in the debugger. Put the input focus on the memset call, and press the F9 key to set a breakpoint. Restart (press the F5 key) and run until the memset call, which you can see in the screen below. Once the breakpoint is triggered, you can select menus Debug, Windows, and Autos to automatically view variables of interest.

More Info

A debug breakpoint is a place in the application where execution halts, allowing you to look at code, variables, registers, memory, the call stack, and so forth. When the breakpoint location is reached, you can subsequently tell the debugger to step, step into, continue the program, or stop the program. For more information about using Visual Studio as a debugger, see http://msdn.microsoft.com/library/en-us/vcug98/html/_asug_home_page.3a_.debugger.asp.

More Info

Notice (in the preceding screen shot of the code) that uiLength is set to 35000. The program is about to fill the szInputFileLen buffer with 34,999 a characters (0x61) and a null. So far, everything seems good. Step two more times (press the F10 key twice), and inspect what is sent into the PrintIt call as shown in the following graphic:

More Info

uiLength is still 35000. What about szInputFileLen? To find the value of szInputFileLen, do the following.

  1. Select the Debug menu.

  2. Click the Windows menu.

  3. Click the Memory menu.

  4. Select Memory1.

  5. Type the name of the variable, szInputFileLen, and press Enter.

The memory window now shows szInputFileLen.

More Info

Now, let’s see how long the string of 0x61 bytes is to confirm the memset call worked and check the position of the trailing null. The end of the copied data is somewhere around szInputFileLen + uiLength. One way to check out what the end looks like is to look at the memory window starting a few addresses sooner. szInputFileLen[uiLength] is the element just past the memset data write. szInputFileLen[uiLength-10h] references an earlier element. The memory window wants an address, so &(szInputFileLen[uiLength-10h]) actually represents an address that will show us a few (16 decimal) of the trailing a bytes to confirm what the end of the memset and subsequent operations look like in memory.

More Info

This looks pretty good—the 0x61 bytes all copied OK, and the null is in the right place. Stepping into PrintIt (press the F11 key) reveals a problem, however:

More Info

Because iLength is signed and PrintIt checks only the upper bound, this could get interesting quickly. Stepping twice takes you to the memcpy function.

What happens when memcpy gets a –30536 for the third parameter, iLength? You can find out because this is debug—step into this function as shown in the graphic:

More Info

Look down a few lines and continue stepping to the following line:

More Info

This line transfers the number of bytes to copy into the ECX register. Step and you can check up on the value of ECX.

More Info

ECX is set to 0xFFFF88B8! That’s not the 35000 the programmer had in mind.

Note

This example could continue, and we could show an exploit, but typically it is sufficient to control the instruction pointer register to prove we can control the computer. Those interested in exploits already have many places to look (such as http://www.metasploit.com). The focus of this book is finding the bugs, not exploiting them. For didactic purposes, there is a complete walkthrough of a proof-of-concept exploit in Chapter 9.

So what does all of this mean? Well, when testing for buffer overflows and integer overruns, think about the limits of the data types being used and try to go beyond the limits. Think about signed and unsigned data values, especially. In practice, it is critical to construct test data that has a length of various powers of two, and slowly grow buffers from each. This can be hard, especially for 32-bit signed and unsigned values and larger. Despite the difficulty, attackers are good at performing these exploits. Those responsible for testing need to look for these issues so that products don’t ship with these bugs.

More Info

The article “Basic Integer Overflows” (http://www.phrack.org/show.php?p=60&a=10) describes some integer overflow issues. For programmers looking to avoid making these coding mistakes, the article “Integer Handling with the C++ SafeInt Class” (http://msdn.microsoft.com/library/en-us/dncode/html/secure01142004.asp) can help.

Heap Overruns

In addition to allocating memory on the stack, as was previously described, memory can be allocated on a heap. Unlike the stack, the heap does not allocate memory linearly. The heap also doesn’t store return addresses for functions. The heap tends to be somewhat less predictable than the stack at the first casual glance. A full discussion of the heap is beyond the scope of this chapter. You can find more information about Win32 heaps at http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dngenlib/html/msdn_heapmm.asp.

Many people have said that overwriting heap buffers is not exploitable for particular cases but were later proved wrong by real attackers. The truth is that they didn’t know how to exploit the particular situation.

Important

Just because someone cannot think of a way to accomplish an attack doesn’t mean the attack cannot be done.

One way to exploit the heap is by overwriting a function pointer that happens to be allocated on the heap. Another way is by freeing overrun heap memory next to an unused heap block. There might be other ways to exploit the heap, depending on how the heap implements heap control blocks and metadata about the allocations. For example, some heap implementations can be exploited if the free function is called too many times.

More Info

One good example of a double free bug is the “LBL traceroute Exploit” described at http://synnergy.net/downloads/exploits/traceroute-exp.txt.

Looking at the heap is less straightforward than examining the stack is. The main reason isn’t so much that the heap is harder to understand, but rather that so many different heaps are used, each heap varies somewhat from each other heap, and some debuggers use their own instrumented heaps that behave differently from the runtime heap. If you are lucky, the heap might even tell you when you hit an overflow.

It should be noted that many (although not all) heap overflow exploits somehow also take advantage of the structure of the stack, return addresses, exception handlers on the stack, or data on the stack to complete the exploit. Perhaps the main reason is because the location of the data on the heap is typically not as predictable as on the stack. Attackers have a number of strategies at their disposal for attempting to manipulate how the heap allocates memory, but the specifics are beyond the scope of this book.

More Info

There are some fairly good resources on heap overruns out there, such as these:

One example of a heap overrun vulnerability with a description of how it works is “JPEG COM Marker Processing Vulnerability in Netscape Browsers,” which can be found at http://nvd.nist.gov/nvd.cfm?cvename=CVE-2000-0655.

Other Attacks

Attackers can exploit various other permutations on the simple buffer overflow. One worth calling out happens when the program does not properly validate the offset referenced in the data before using the offset to compute where to write a value. A simple case might occur when there is enough memory allocated for, say, 10 items in a list, but the program then reads in more than 10 items. In this example, the reference to each new item is implicitly 11, 12, and so on. Sometimes the reference is more explicit, perhaps when the coordinates are specifically called out.

Once you understand what overruns are, the next step is understanding how to find them.

Testing for Overruns: Where to Look for Cases

In the grand scheme of things, there are a ton of test cases out there. Where do you get the biggest impact for your testing effort? You really want to find as many overflows as you can, but it is important to look for issues in some places more than in others. If attackers want an overflow to pay off, they need to somehow get the payload in the right place in the system. If the attacker has to convince the victim to type in the payload, the exploit probably won’t work well for them—they might as well settle for asking the victim to run an executable.

So where should you look first? Prioritizing testing is one area where doing a good job of threat modeling can really pay off. The threat models and data flow diagrams can help you discern which areas of the functionality have the most exposure so you can better plan your testing. Two key areas to focus on are the entry points and trust boundaries. Let’s take a look at several places to look for data and overruns, including these:

  • On the network

  • In documents and files

  • In information shared between users with higher and lower privileges

  • In programmable interfaces

Caution

Be wary of misevaluating whether an attack vector is feasible. Sometimes protected channels don’t really protect against attacks. For example, many Secure Sockets Layer (SSL) servers will grant a protected channel to whomever asks for one; so in this case just because the exploit would have to happen over SSL doesn’t really protect the server against attack.

Network

The network is a given and has been the source of a number of attacks such as Code Red and SQL Slammer. Even so, sometimes network traffic might not be the best first place to look.

Consider, for example, how bad it is if you already need a secure channel open and to be authenticated as an administrator before you can leverage an overflow that owns the server. It’s no big deal—in this case, you already can do whatever you want as the administrator. The focus should be on network traffic that is the most reachable for legitimate attack scenarios. This would include trying to elevate privileges or hijack other user accounts, anonymous requests, server responses, and Web-based attacks where an attacker might fool the administrator into sending malicious data over the network without realizing it.

One area to pay particular attention to is callbacks. The client calls the server and the server then calls the client back with information, and often the client simply accepts the data. This is particularly dangerous over the firewall because often the firewall lets an attacker right in when the attacker gives a network-borne response to a request from a target client inside the firewall.

Documents and Files

When users double-click a file in an e-mail message or open a file in an editor, can they trust that the file’s contents won’t harm their computer? Buffer overflows when parsing files are fairly common. Some data files are more important to scour than others.

Information Shared Between Users with Higher and Lower Privileges

One particularly attractive place to look for overflows is when information is shared among users who have different privileges. If an attacker can cause a log event to take place that overflows when the administrator uses administration tools, the overflow pays a huge dividend. If you know of an overflow in a network diagnostic tool the administrator uses, you can play with the network a bit to bait the trap, and then unplug your neighbor’s network tap and wait for the administrator to start the tool. (Be careful—administrators know a few tricks, too.) Some tools used by network administrators, such as NetMon, are not network administrator–friendly and will even announce their presence on the network so attackers know exactly when to nail them.

As an attacker, you want to do cool and interesting things with the overflow, right? If you find an overflow in code running with guest permissions, it is much less interesting than is a system-level thread with much broader permissions. Using this argument to not fix an overflow often can prove deadly. Why? If you start running code in a thread with reduced privileges, you might be able to call functions like RevertToSelf or ImpersonateLoggedOnUser to elevate privileges in some cases where the thread is impersonating a particular low-privileged user. You might be able to manipulate another thread with higher privileges in the same process somehow (there are a lot of ways to accomplish this, from shatter attacks to named object hijacking to writing to shared memory or the other thread’s stack). The general rule of thumb is to consider the sum total of all privileges the process is allowed at all times as the privileges accessible to attackers when they overflow any part of that process. For example, if a remote procedure call (RPC) server calls the RpcImpersonateClient function with 50 client users, an overflow on the server could potentially run arbitrary code over time as all 50 users as well as the security context of the main process.

Programmable Interfaces

Programmable interfaces present an opportunity for overflows that usually boils down to assumptions. Often, the programmer who created the interface doesn’t document his or her assumptions, and the programmer who uses the interface makes different assumptions. Let’s consider the example of an interface that processes a file. If the interface contains a method ReadFile(char *szFileName) and it is documented that MAX_PATH is the maximum size of the filename, what happens when MAX_PATH changes from 256 to 260 for a different compiler? Figure 8-10 shows one example of problems that can arise when documentation is vague and interface or function declarations change over time. In the figure, on the left, the caller is assuming MAX_PATH is defined one way, whereas the older component on the right is assuming it is defined differently.

Problems that can arise when interface or function declarations change over time

Figure 8-10. Problems that can arise when interface or function declarations change over time

Is the overflow caused by the problem shown in Figure 8-10 the ReadFile function’s fault for not doing data validation? Although the new component can fix this by making sure no more than 256 bytes are passed in, that won’t fix existing callers. At this point, it would make sense to fix ReadFile, but what if the same programmers didn’t write that code? The only way to fix the problem in that case is to audit the code to look for other callers of ReadFile to make sure they are not vulnerable to the same issue and warn developers to be careful when using this ReadFile function.

Over time, people are discovering a lot of functions that are unsafe or promote unsafe coding practices inasmuch as using them tends to result in buffer overflows. These functions have such an incredibly bad record that we can often successfully look for overruns by finding these functions and analyzing how they work and from where the data used in them comes. General guidelines for creating new APIs are as follows:

  • The API itself should either take a length for the data or a buffer size as a parameter.

  • The API should assume all parameters coming in are untrusted and do its own validation.

  • It should be very clear whether the length is a count of bytes or characters. In the case of characters, it is also worth clarifying the type of characters (multibyte, single byte, Unicode, and so forth) and how big each character is.

  • The API should never assume the buffer allocated is longer than the data in it without a specific parameter telling it otherwise.

  • Assumptions should be documented with great care. Imagine if the ReadFile API had documented the length requirement as 256 (and put MAX_PATH in parentheses); it would have helped developers a bit.

  • API testers should hammer the API directly with good test cases.

Black Box (Functional) Testing

Now that we have considered some of the different forms overflows might take and where to look for them, let’s turn our attention to how to find them in what we are testing once we identify a feature to test. To begin with, we focus on the process of examining what the data that the application expects looks like. By using expected data as a template, along with your understanding of how the system works and how overflows work, you can then dig more deeply into methods to construct meaningful data for use in finding overflows.

Determining What Data Is Expected

The first item to understand about the entry point under consideration is what data the system expects and in which format the data is expected to be. If you want to do a thorough job of breaking the system, you have to break down and test each of the separate pieces of data in appropriate ways.

A variety of different ways can help you begin to understand what data is expected:

  • Data format standards and/or specifications

  • Talking with people who know (programmers and designers)

  • The source code for the application (original or disassembled)

  • Analyzing the binary data format

Understanding how to analyze the actual data format is a valuable skill because specifications are not always complete, error-free, or up-to-date, and people seldom know all of the details. The only consistently reliable and credible sources of information about the formats of the data are the source code, the actual compiled binary, valid chunks of data consumed by them, and proven exploits. Many security bugs are caused by different interpretations of the specification or standard and the assumptions that following vague standards engender.

Important

Attackers don’t necessarily have to understand the whole data format to get some good test cases, but testers do need to understand the whole data format to nail all of the good test cases. To exploit the system attackers need testers to miss only one case.

Many good bugs are found by attackers hypothesizing what the code looks like, and then constructing data input experiments that either prove or disprove the hypothesis, enabling them to further refine their attack. Consider, for instance, the scenario in which a Web browser has a flaw in how it parses the HTML returned from the server. Perhaps that Web browser fixes the flaw. A sharp attacker would realize that other Web browsers need to write code to do something similar. Maybe the other browsers have the same bug. It can be very embarrassing for a company to fix a bug in one piece of code only to have the attacker try the same exploit someplace else and find that code vulnerable.

Using Data You Recognize

Start out using data that you recognize, such as all A bytes (0x41), that matches the characteristics of the data being replaced. If it is numeric data, for example, use numeric replacements you can recognize (all number 2s, for instance). The reason for using data you recognize is so you can later tell whether you see your data in CPU registers, on the stack, and in places it shouldn’t be.

Tip

To determine whether all of your test data was copied, vary the last four bytes or so, still in a recognizable way. For example, aaaaaaaaaaaaa...aaaaaaaAAAA enables you to tell quickly whether the whole string was copied without having to count the long string later on.

Knowing the Limits and Bounds

One of the main issues when testing for buffer overflows is trying to figure out how long of a string to try. You can employ a number of strategies to determine this.

Asking or Reading the Code

The first approach is the most direct. If you ask the programmer, he or she might tell you an incorrect answer or might not know the answer. Asking developers can give you some hints, but you shouldn’t rely on that method exclusively.

Reading the code is only slightly better because, in reading the code, it is easy to misread the real boundaries, although you might notice some overruns in the code you are reading. Properly determining the boundaries while reading the code requires understanding the system state (with respect to that data) for every equivalence class, in every environment, for the whole code base (every caller) that applies to that data. That’s appropriate to tackle for an in-depth code review, but for penetration testing all you should try to get from developers and the code is a clear idea of the maximum size this data is intended to be. We always ask, think about, and compare the length in bytes; otherwise, there is too much ambiguity and confusion.

Trying the Maximum Intended Allowable Lengths

If you do know how long data can be, you need to try the maximum allowed length, as well as one byte longer, to get a feel for the accuracy of your information and whether the code allows enough memory for the trailing null. If the actual allocated space is larger and we know its size, it is worth hitting that boundary as well, looking to see whether the allowed length is enforced even given a larger buffer.

Using Common Limits

A lot of defined numerical limits are well known because shared code, such as C header files, defines the limits. These limits can help you out. You can use an Internet search engine or programmers’ references or look in the include files for well-known or commonly used limits. As you look for overflows, build up a list of common limits to keep in mind given the kinds of data the application uses. Some of the more common limits might include the following (look around and add your own to the list):

//from stdlib.h                                                                        
/* Sizes for buffers used by the _makepath() and _splitpath() functions                
 * Note that the sizes include space for 0-terminator.                                 
 */                                                                                    
#define _MAX_PATH  260 /* max. length of full path */                                  
#define _MAX_DRIVE 3  /* max. length of drive component */                             
#define _MAX_DIR  256 /* max. length of path component */                              
#define _MAX_FNAME 256 /* max. length of filename component */                         
#define _MAX_EXT  256 /* max. length of extension component */                         
                                                                                       
//from WinGDI.h                                                                        
/* Logical Font */                                                                     
#define LF_FACESIZE     32                                                             
#define LF_FULLFACESIZE   64                                                           
                                                                                       
//from wininet.h                                                                       
// maximum field lengths (arbitrary)                                                   
//                                                                                     
#define INTERNET_MAX_HOST_NAME_LENGTH  256                                             
#define INTERNET_MAX_USER_NAME_LENGTH  128                                             
#define INTERNET_MAX_PASSWORD_LENGTH  128                                              
#define INTERNET_MAX_PORT_NUMBER_LENGTH 5      // INTERNET_PORT is unsigned short.     
#define INTERNET_MAX_PORT_NUMBER_VALUE 65535    // maximum unsigned short value        
#define INTERNET_MAX_PATH_LENGTH    2048                                               
#define INTERNET_MAX_SCHEME_LENGTH   32     // longest protocol name length            
#define INTERNET_MAX_URL_LENGTH     (INTERNET_MAX_SCHEME_LENGTH                       
   + sizeof("://")                                                                    
   + INTERNET_MAX_PATH_LENGTH)                                                         
                                                                                       
//from winbase.h                                                                       
#define OFS_MAXPATHNAME 128                                                            
#ifndef _MAC                                                                           
#define MAX_COMPUTERNAME_LENGTH 15                                                     
#else                                                                                  
#define MAX_COMPUTERNAME_LENGTH 31                                                     
#endif                                                                                 
                                                                                       
//from wincrypt.h                                                                      
#define CERT_CHAIN_MAX_AIA_URL_COUNT_IN_CERT_DEFAULT        5                          
#define CERT_CHAIN_MAX_AIA_URL_RETRIEVAL_COUNT_PER_CHAIN_DEFAULT  10                   
#define CERT_CHAIN_MAX_AIA_URL_RETRIEVAL_BYTE_COUNT_DEFAULT     100000                 
#define CERT_CHAIN_MAX_AIA_URL_RETRIEVAL_CERT_COUNT_DEFAULT     10                     

Slowly Growing the Input

Once you know the limit, you must slowly grow the string or the number of items. The main reason you must slowly increase the string is because in stack overruns it isn’t clear how much data resides between the buffer being overrun and the interesting items to overwrite on the stack. Growing the string slowly means trying one more byte/character with each successive test case. The most efficient way of testing this scenario, of course, is to write a script or tool that can do the work for you. But the test automation must be able to discern changes in the application’s response accurately to determine where the real boundaries are successfully.

Using an Iterative Approach

When you get a new feature from the programmers and begin looking for overruns, one of the first steps you can take is to try to assess what the intended maximum lengths of various data components are. The programmers should absolutely know attackers will look for this and keep that in the back of their minds as they write code.

Once you identify all of the data you control as an attacker, systematically break down the data into appropriate chunks. For each chunk, try expected boundaries, one byte over, two bytes over, four bytes over, and in this way grow the string. If you get the same response for all of the inputs, moving on to the next chunk of data seems reasonable. For the riskiest data, write automation to grow the string slowly, and gauge application responses for different lengths of input. If you get different responses, focus on the exact boundary where the difference occurs and perhaps you will discover a region in between where an overflow is exploitable.

Consider the case in which the program allocates 20 bytes, and the programmer tries to validate that the input is 20 bytes or fewer. Then the programmer tells the computer to copy the input into the 20-byte buffer and append a null byte immediately following the copied data. If the input data is 19 or fewer bytes, all works well. If the input data is 20 bytes, the null appended is written past the end of the allocated buffer. If the input data is 21 or more bytes long, the data is correctly validated and rejected. In this case, the only test case that would find the bug is specifying exactly 20 bytes. Drilling in on boundaries like these is important. Don’t just throw hundreds or thousands of bytes in a buffer and call it good or you’ll miss important cases like this one.

It isn’t unusual for comparison errors to cause overruns. Comparison errors occur when the programmer mistakenly uses the wrong comparison operator: a less than comparison operator (<) should have been used instead of the less than or equal to comparison operator (<=). Overrunning the boundary by 1 byte and 1 character is all that is necessary for many overflows to be exploitable.

Maintaining Overall Data Integrity

The main point of purposefully constructing test cases is to drill deeply into the application functionality and start to poke. If you make a key to open a lock and the key is constructed such that the end doesn’t even fit in the keyhole, you cannot tell whether the key otherwise would have opened the lock. In the same way, you must be careful to distinguish between whether the basic validations the program performs on the data are still true about the test data. If your testing is merely confirming that those validations are still functional, your tests are of little value in finding real weaknesses in the deeper algorithms.

This section includes a few examples of what to watch out for when constructing test data. Maintaining data integrity warrants looking at each of the following instances within the overall data and considering how the data needs to change to maintain validity.

Encodings/Compression/Encryption

If you know the data is in the file but do not find it in plain text, the data is likely encoded, compressed, or encrypted. We refer to data not immediately available as encumbered data. One of the main tasks here would be to figure out which parts of the data are encumbered and which parts are not. From there, to alter anything encumbered would require appropriate tools. Usually, common libraries, API function calls, or freeware/open source resources are available. So even if the tools do not already exist, developing them might be easier than it sounds at first. Occasionally, a vendor provides tools that can be helpful on its support site or in a software development kit (SDK).

Perhaps one of the most universally applicable approaches to working with encumbered data is to use the application as the tool: you can run the native application in a debugger and view and alter the data in memory before it is encumbered. Using the application itself has some problems, however. First, you need to determine where and when that application makes the call to encrypt, encode, or compress its data before storing it in a file or sending it over the wire. If you don’t have the source code, disassembling the binary and running tools such as strace (http://sourceforge.net/project/showfiles.php?group_id=2861 or http://www.bindview.com/Services/RAZOR/Utilities/Windows/strace_readme.cfm) and APIMon (http://www.microsoft.com/downloads/details.aspx?familyid=49ae8576-9bb9-4126-9761-ba8011fabf38&displaylang=en) are attractive options. Then you need to set a breakpoint in the debugger to tamper with the data in memory before the application encumbers the data. It seldom is that simple, however. Usually the application won’t let you create the bad data for a variety of reasons:

  • Data validity checks fail—these must be removed.

  • Sufficient space to grow the buffer adequately has not been allocated—you need to allocate more by figuring out how the allocation happens and making sure more space is allocated.

  • The data is no longer consumable by the routine used to encumber the data—you need to modify the encumbering routine (perhaps as a separate program) to handle the test data. For example, the data is null terminated and you introduce a null byte or character in the middle of the data. In this case, you would need to fool the routine into thinking the data really was longer than the inserted null is in places where it computes or uses the length of the data.

  • Other metadata about the encumbered blob—for example, the length of the blob or other characteristics that are checked when the blob is subsequently processed—is no longer valid—those characteristics might have to be updated to fool the data validation check.

  • The application includes checks to detect whether a debugger is attached to prevent attackers from generating bad data and analyzing how the application works. As discussed in Chapter 17, applications that include checks to prevent reverse engineering can be defeated.

Compound Documents

If you look at the data and see the Root Entry, such as in the file shown in Figure 8-11, the data is likely to be formatted as an Object Linking and Embedding (OLE) DocFile, also called a structured storage file. This text is present in OLE DocFile compound storage files when they are viewed with a binary editor.

The Root Entry text

Figure 8-11. The Root Entry text

Although testers can write code to create test cases (see StgCreateStorageEx and related APIs), a handy tool from eTree can be used to edit these files (go to http://www.etree.com/tech/freestuff/edoc/index.html).

Offsets/Sizes

It is not at all uncommon for structures that contain the length of data, how many items there are to read, or the offset to another piece of the data to be written into the file. For these situations, several cases are worth trying:

  • When growing the data, you might also need to grow the size or offsets specified in the file to maintain overall file integrity.

  • Specify offsets and lengths that are huge (0xFFFFFFFF for the unsigned 32-bit case, for example, or 0x7FFF for the signed 16-bit case).

  • Determine which piece of input is responsible for the memory allocation. For example, if there is more than one place where the data length is specified, specify large amounts of data and watch the amount allocated in System Monitor (perfmon.msc) or by setting a conditional breakpoint on the memory allocation routine. By doing so, you can determine which numbers need to be altered for test case generation.

References

Sometimes the data includes references to other pieces of data, and the references must be valid for the processing of the file to continue.

One example of this is in valid Extensible Markup Language (XML) syntax, where a corresponding closing XML tag is expected for every open tag. If the closing tag is not encountered, the parent and child XML node relationship is messed up or the file fails during parsing, and the data is never really processed deeply by the application of interest.

Another example is a database in which the name of a table or field is present in a query. If the parsing of the query checks for the existence of the table and field, the parser might stop unless the table or field is also present, of the right type, and so forth.

Fixed-Width Fields

Many data files have fixed field widths, which might have to be respected for the parser to interpret subsequent data in a meaningful way.

Consider, for example, a phone number parser that lists _8005551212 as a chunk of data. Someone more familiar with the parser might realize that the data is really two pieces: the 3-digit area code (800), and the 7-digit phone number (5551212). Each piece has its meaning, and perhaps making these fields longer doesn’t even get past the parser. How would you try to overrun, say, a list of phone numbers? You could try integer overflows and providing too many phone numbers, but generally fixed-width data is harder to overflow unless the width is also specified in the document. It can be worth inserting all zeros, nonnumeric data, or expressions that might expand to fill more space when evaluated and setting the sign on data (refer to the section Integer Overflows earlier in this chapter).

Limited Values (Enumerations)

Often, particular values are acceptable and all other values are not. Knowing what type of information is present matters: consider HTTP, which has a certain number of valid commands: GET, POST, and HEAD, for example. In the case of credit card numbers, perhaps the data parser takes only the digits 0 to 9. Other times, there is an enumeration of values. In the database case, perhaps the first byte of data is 0 for database name, 1 for table name, 2 for field name, 3 for query name. If it is not 0, 1, 2, or 3, the field takes some other code path that isn’t well defined, or perhaps the parsing stops at that point. In that case, you would want to ensure the data is 0, 1, 2, or 3 to test the first code path and some other value to test the other code paths. If the character size limit on the table name is 32, overflowing this limit would be the target when the enumeration is 1. If the database name could be only 16 characters long, when the enumeration is 0 you would have a different boundary to check against for overruns.

Dependencies

Many times there are optional chunks of data. If an HTML document contains a table, the <table> tag is present, and so are <tr> and <td> usually. If there is a <tr> or a <td>, the <table> element is expected to define the overall properties of the table as a whole.

Suppose in the database example the database definition input parser sees the type of the field and expects certain data to be present about the contents. Likely, the parser infers a specific format from the type as well. Numeric data will probably be within a certain range, of a certain length; character data (strings) will either be null terminated or have a length listed with them, and so forth.

In the embedded content case, a single null byte (0x00) might be sitting someplace to indicate there is no embedded content. When that value is changed, the application begins to infer other data about the embedded object should be present. To make any real use of changing that byte, a better understanding of how the embedded data is represented for that case is warranted. Sure, you can go around and change the byte and see what happens and perhaps find a few basic issues—but to really dig in and overflow metadata about the embedded object or data within the object itself and encounter overruns in the deeper underlying parsing algorithms for the object, you need to understand how that object is stored.

Delimiters

Don’t just focus on the data: manipulating the delimiters and format of the data is important. For example, when trying to overflow strings that are paths, trying a lot of path separators is different from simply trying other characters. When overflowing a Simple Object Access Protocol (SOAP) request, it’s fair game to try to overflow the SOAPAction header or put in a long attribute name.

If you see the data quoted, you should try to knock off the end quotation mark. Carriage return/line feed (CR/LF) combinations (0x0D0A) should be bumped down to just one of the two or none—really see what happens when a list like {x,y,z} is changed to {x,yaz} or {x,,,,}. Adding extra delimiters can be interesting. Also, try leaving off the final brace. Don’t forget nested cases, too {{}}.

Null values are a special case covered in the following section.

Strategies for Transforming Normal Data into Overruns

In addition to maintaining data integrity, you can employ a number of strategies to take existing data and turn it into interesting test cases:

  • Replacing null values

  • Inserting data

  • Overwriting data

  • Adjusting string lengths

  • Understanding more complex data structures

Replacing Null Values

Null values are great. They can indicate a lot of different things: flags that are not set, zero, an empty string, the end of a string, or filler. One good brute force test case is to replace the nulls (one, two, or four at a time, depending on the expected size of the data) with 0xFF, 0xFFFFFFFF, or other valid data. Why is this interesting? If the null indicates the end of a string and it is replaced with 0xFF, the string becomes longer without disturbing any offsets or other data. If the null indicates length, 0xFF is a good case to try for integer overruns. Note that sets of three null bytes often can indicate the null bytes are really part of a 4-byte value. For this case, editing the nulls is really just changing the value from one nonzero value to another nonzero value.

Inserting vs. Overwriting

When you generate test cases using a well-understood data format, how to adjust the remaining data to accommodate the overflowed value is clear. If, on the other hand, you do not understand the data format well, it becomes interesting to try the following different cases on strings in the binary data:

  • Overwriting data

  • Inserting data

  • Replacing data

To overwrite means to change existing data within and/or after the data without extending the overall length of the data. In this method, some data is overwritten. This is particularly valuable as an approach if earlier parts of the file might reference later parts by location.

Inserting implies increasing the length, which is useful in several cases. The first case is when you are fairly certain no reference by location will be affected. Another case is when there are specific delimiters of interest. Inserting data is generally worth trying, in any event.

Replacing data is done by removing the existing data or a portion thereof and then inserting the test case data in its place. Replacing data is particularly useful in data formats with delimiters, such as XML and other text-based formats, null-terminated strings, and so forth.

Inserting is particularly useful when the beginning or end of the data needs to be preserved. For example, if you can overflow the filename but need to preserve the extension as .pem to hit a particular place of interest in the code path, inserting data prior to the .pem extension is the method of choice for initial test cases. Figure 8-12, Figure 8-13, and Figure 8-14 show examples of using a binary editor to insert and overwrite additional data to grow the string present in the original data.

In Figure 8-12, the file contains valid binary data in the format the program expects, but some of it looks interesting to test for overflows. Figure 8-13 shows data that was created by taking ordinary input (shown in Figure 8-12) and inserting data before the .pem extension to lengthen the string using a binary editor. The test data shows only 16 bytes inserted; in practice, the quantity of data inserted would vary. Figure 8-14 shows data that was created by taking ordinary input (shown in Figure 8-12) and overwriting data to lengthen the string using a binary editor. Notice that in Figure 8-13 the path $UserCerts is still present, as is the extension (.pem), unlike in Figure 8-14.

Binary data in the format the program expects

Figure 8-12. Binary data in the format the program expects

Inserting data to lengthen the string using a binary editor

Figure 8-13. Inserting data to lengthen the string using a binary editor

Overwriting data to lengthen the string using a binary editor

Figure 8-14. Overwriting data to lengthen the string using a binary editor

Most binary and text editors have facilities for performing insertion, overwriting, and replacing of arbitrary data.

Adjusting String Lengths

It is fairly typical for the length of the data to be specified in the data itself. The length data can take one of two forms: it can be text data, such as Content-Length: 5678 in an HTTP packet, or it can be binary data, such as 0x000001F2 located in the binary file.

The main approach to testing string lengths is to alter the length specified by lowering it in the hopes that the program uses the length to allocate memory and copies all of the data anyhow, overflowing the buffer. Another case worth trying is the integer overrun case where perhaps the length is stored in a variable of a certain size. By specifying a larger length, it might be possible to convince the program to allocate a small amount of memory and copy the bits. A third case is to specify large sizes in the attempt to get the memory allocation to fail. If the allocation fails, perhaps the program doesn’t check for this case and overwrites, or perhaps the program frees the unallocated buffer.

Recognizing Data Structures

It is important to understand the significance of each piece of data to do a reasonably thorough job of assessing the program for buffer overruns of various types. In the cases where the data format is documented (http://www.wotsit.org lists many formats) or you have access to the code that parses or emits the data, it is relatively easy to understand the format of the data. When you do not understand the data, however, you can employ several strategies to gain insight.

A discussion of analyzing file formats and binaries is included in Chapter 17, but for overflows it isn’t unusual to see patterns that involve the length preceding the data or the length of a structure appearing prior to the structure.

Testing Both Primary and Secondary Actions

Some code runs more frequently than other code. Primary actions are actions taken on untrusted data immediately, and always take place. Secondary actions take place after primary actions and might not actually take place. You still need to test secondary actions because they can be exploitable as well. For example, you might postulate that incoming network traffic on a port (primary action) that causes a server to overflow is worse than an overflow when printing a document (secondary action) in a word processor. Although the exposure is greater in the former case, the printing bug is still severe and warrants fixing. Perhaps users experience no problems when they open the document (primary action), but when they save the document (secondary action) there is a buffer overrun. In the network server case, maybe the first network request creates a file on the server and the second requires the file to be present but overflows. Maybe the buffer overflow occurs when parsing the data from the backend database, but the code that first puts the data therein is free from overflows. Upon reflection, the test matrix here is huge: consider every place you inject data, grow the strings slowly, and test the full functionality of features that might use the data. Whew—how do you accomplish this?

Prioritizing Test Cases

The immediate issue arises of how to prioritize. The prioritization question is answered by the data flow and gaining some sense of how much handling of the data is done in the code paths available to the attacker. For converting to one format, perhaps the data is handled quite a bit, whereas for saving in a different format, it isn’t handled much and so there is less risk. You need to assess the risk and weigh the alternatives of trying one piece of data in more places versus trying out more pieces of data in fewer locations, just like with every other type of testing. We already discussed one key indicator of risk (how much the data is handled), but it turns out there are at least a few more indicators of high risk:

  • Poor development practices.

  • Borrowed code.

  • Pressure on the development team to meet the due date instead of focus on quality.

  • The programming language used. Languages such as C, C++, and assembly are inherently riskier than are such languages as Java and C#.

  • No static or runtime overrun analysis tools used, or results aren’t investigated.

  • Little or no code review by qualified people.

  • New developers.

  • Poor design (didn’t specify boundaries and limits, hard to determine data types, and so forth).

  • Code has a history of other overflows.

  • Lack of secure coding practices in place, perhaps missing opportunities to reduce mistakes. Examples of insecure coding practices include not using secure string classes, relying on null termination rather than explicitly specifying the maximum sizes of the target buffers, and failing to specify the string length explicitly rather than by using null termination or parsing.

  • Programmers have little or no awareness of security issues.

  • Existing security testing coverage of the code is poor.

From a purely technical point of view, you can also look at how many copies of the data are needed to get the job done. The more copies needed, the more likely there will be an overflow. The more the data is parsed, the more likely there will be an overflow. So as you examine which features and functionality pose the greatest risk, carefully consider which types of actions (parsing, copying, converting, inserting/appending, or sending unvalidated data to other components) cause the overflows.

What to Look For

All the test cases in the world will never find a single overrun if the observer is unaware of what overruns look like. Some people think overruns cause crashes. Although that might be true for some cases, it is often true that overruns do not cause crashes.

Important

Any exploitable overrun in the hands of a skilled attacker who does not wish to be caught will almost never crash. If you ever hear “it didn’t crash, so it is not exploitable,” with no additional analysis, you know the person making that claim does not understand overruns.

In addition to crashes, overruns can throw other exceptions that are handled or cause memory spikes and other unexpected behavior.

Learn programmer lingo. Sometimes “random heap corruption” bugs turn out to be exploitable heap overruns. They can be very hard to track down, but are usually worth pursuing.

Crashes

In general, if ordinary input works fine and long input or other test cases targeted at overruns crash the program, a buffer overrun is indicated until code analysis mathematically proves otherwise. It is typically far less expensive to fix these problems than it is to prove they are not exploitable. Many programmers have “proved” certain cases were not exploitable only to have an attacker exploit the situation by violating incorrect assumptions (the programmer’s proof was not mathematically robust for all input in all operating environments).

When a crash occurs, a number of factors indicate an exploitable condition. Look at the CPU registers. If the attacker’s data is all or part of EIP, EBP, or ESP, the overrun is considered always exploitable. If the attacker’s data is all or part of one or more of the other CPU registers, the bug is very likely to be exploitable, although only further analysis can really determine whether that is so. If EIP, ESP, or EBP is pointing to memory the process does not own, the case is extremely likely to be exploitable—the program lost control because of the input.

Look at the stack. If the stack is corrupted, you’ve likely got an exploitable overrun. If the stack isn’t corrupted, you can use it to help analyze what happened. If the overrun happens during a free or delete operation or a Component Object Model (COM) pObject->Release(); call, a double free bug might well be present. If the stack indicates some sort of memory copy or move condition, it is likely to be an exploitable heap overrun.

Consider the type of exception that was thrown. Write access violations (write AVs) occur when the program attempts to write to memory it does not own or memory that is marked execute-only or read-only. Read access violations (read AVs) occur when the program attempts to read memory to which it does not have Read access. Although people might correctly say that write AVs are more likely to be exploitable than read AVs are, many read AVs are exploitable in subtle ways. Look at the code about to be run. If the code about to be run is mov [ecx], [eax], for example, and an attacker can influence ECX or EAX, likely an exploit is possible.

One particularly common pattern is for an instance of a class to be allocated on the heap, and then later freed. When the class instance is freed, the pointer to the class instance is set to null. Then the program uses the class and causes a read AV referencing a very low memory address. It turns out these are exploitable in certain race conditions (refer to Chapter 13, for more information about race conditions) if there is a context switch, for example, between deleting the object and setting the now unused object (class) pointer to null. In the meantime, if another thread allocates the same heap location and fills it with the attacker’s data, what appears to have been a crash turns out instead to be an exploitable security vulnerability because the function pointer is located on the heap in the attacker’s data.

Exceptions

In most examples in this book, we identify overruns by entering long data and watching for unhandled exceptions and crashes, but some programming teams have a methodology whereby they use exception handling excessively to handle error cases in general. Not only do exception handlers make it somewhat harder to notice buffer overruns, it turns out the exception handling routines can be useful for attackers. Let’s examine how simple exception handlers work to see them in action. Consider the following program.

int _tmain(int argc, _TCHAR* argv[]) {               
   try {                                             
      throw 2;                                       
   }                                                 
   catch ( ... ) {                                   
      printf("Exceptional Code.
");                 
   }                                                 
   return 0;                                         
}                                                    

Note

This walkthrough is done using ship/release (not debug).

To follow along, set a breakpoint on the _tmain function, start the program, and when the breakpoint hits you can right-click the code window and select Go To Disassembly from the shortcut menu. Note that you can also enable code bytes by right-clicking the Disassembly window and selecting Show Code Bytes from the shortcut menu if it is not selected already.

Switching to disassembly shows us the following:

Exceptions

So what is all of this? Well, it appears register EBP is being properly set up and the old value is pushed onto the stack; then comes offset __ehhandler$_main (406F80h). This means that the address where the exception handler code is located is pushed onto the stack. Let’s look at 406F80h in the Disassembly window to confirm:

Exceptions

This seems like interesting code because it looks likely all exception handlers might use this location. Put a breakpoint on 0x00406F80 while you are there.

Back in the main function’s disassembly, step in the debugger a few times until you are just past the push offset __ehhandler$_main instruction:

Exceptions

In the Memory window, type ESP to look at the stack:

Exceptions

Notice the 80 6F 40 00 in memory. This is stored in little endian notation (meaning it is stored backward). The memory, when converted to an address, is 0x00406F80. Address 0x00406F80 is on the stack. Now run, and the breakpoint triggers when the exception occurs. Stop the debugger for now. This is interesting because it turns out to be somewhat handy to overwrite the exception handler if your data cannot be long enough to reach the return address of the function or you can easily trigger an exception. Sometimes, to fix a bug, the programmer might simply use try-catch blocks rather than addressing the issue. This does not stop buffer overflows from occurring, and it does not stop them from being exploitable.

Let’s consider an example of exception handlers and the associated overflows and some issues to be on the lookout for when testing.

Consider the following program.

#include <iostream>                                                         
#include <tchar.h>                                                          
void Pizza(char *szHotDogs, char *szUntrustedData)                          
{                                                                           
   try                                                                      
   {                                                                        
   size_t DataLength = strlen(szUntrustedData);                             
      //messed-up code here...                                              
      mbstowcs((wchar_t*)(szHotDogs - DataLength),                          
         szUntrustedData, DataLength + 1); //Bad News                       
      throw 2;                                                              
   }                                                                        
   catch( ... )                                                             
   {                                                                        
   }                                                                        
}                                                                           
                                                                            
int _tmain(int argc, _TCHAR* argv[])                                        
{                                                                           
   char szFoo[21];                                                          
   char *szUntrustedData = (char*)malloc(201);                              
   if (!szUntrustedData) return 1;                                          
                                                                            
   //Simulate loading untrusted data from somewhere.                        
   memcpy(szUntrustedData,"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"   
      "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"      
      "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"      
      "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",201);                             
   if (strlen(szUntrustedData) > 200)                                       
   {                                                                        
      free(szUntrustedData);                                                
      return 1;                                                             
   }                                                                        
                                                                            
   //Do something interesting with the data.                                
   Pizza(szFoo,szUntrustedData);                                            
   free(szUntrustedData);                                                   
   printf("Run completed successfully.");                                   
   return 0;                                                                
}                                                                           

Go ahead and run the program (use release, not debug). No crash, but the program results in something suspicious. Look at the debug output window.

First-chance exception at 0x00610061 in HuntingChapter8.exe:       
0xC0000005: Access violation reading location 0x00610061.          

But the debugger didn’t stop! The program didn’t crash either. If you hadn’t been in the debugger, you would not have gotten any clues that this exception occurred because the program caught and handled the exception. As a tester interested in catching these exceptions, how could you have caught the exception in the debugger?

  1. In Microsoft Visual C, on the Debug menu, click Exceptions. The Exceptions dialog box appears.

  2. Expand the Win32 Exceptions node.

  3. Select c0000005 Access Violation.

  4. Choose Break Into The Debugger in the When The Exception Is Thrown box.

  5. Dismiss the dialog box, and run the program again.

What happens?

Exceptions

This time the debugger stops. When you [Break], the Disassembly window tells the whole story:

Exceptions

Gee, your data was aaaaaaaa...aaaa, and the letter a is 0x0061 in Unicode (UCS-2), so this really looks a lot like your overlong data.

Suppose you want to break at this exception handler but not other exception handlers?

Let’s step through and make some observations. Stop the current program first, and then step into the program from the start. You can put a breakpoint on Pizza and run.

Exceptions

Now that you are at the breakpoint, look around a bit. The stack is a good place to start. Look at ESP in memory.

Exceptions

Because you are just inside the function, 0x004010EC is probably the return address. Check in the Disassembly window.

Exceptions

The call to Pizza was made at address 0x004010E7, and the return address will be 0x004010EC. Now that you confirmed the return address, look at the current instruction pointer in the Disassembly window to see what’s up ahead.

Exceptions

OK, do you see the exception handler? Step in the debugger until that is on the stack.

Exceptions

Now you can look at the stack again to see where the exception handler is on the stack.

Exceptions

The exception handler is 0x00407150. Find that in the Disassembly window and put a breakpoint on it.

Exceptions

Once the breakpoint is set, you can disable breaking on access violation exceptions and this particular exception will still break in the debugger as long as the breakpoint is set.

The key concepts are these:

  • Just because an exception is handled doesn’t mean it’s exploitable.

  • Not all overflows appear as crashes.

  • You can turn on exception handling in the debugger to see other overruns.

  • You can find and set a breakpoint on a specific exception handler.

Memory Spikes

Memory spikes are sudden, large allocations of memory. Using a tool that can monitor memory usage can be valuable in finding memory spikes. If you replace a null with 0x00FFFFFF and notice the application trying to allocate a huge amount of memory, it is clear an attacker can directly manipulate how much memory is in use by the program. Although at first glance the bug seems like a denial of service issue, there might be an exploitable overrun of some sort if the memory cannot be allocated, for example. To keep track of how much memory a particular process uses, use the tools discussed in Chapter 14.

Important

When you see an application use up a large amount of resources, your first thought might be “performance bug”—but if you were fuzzing or trying long input, the memory consumption might really indicate an exploitable overflow condition.

Sometimes the bug isn’t more than a performance issue, but consider two cases that have security issues:

  • When memory is allocated based on the length the attacker specifies but the success of the memory allocation is not verified before writing.

  • When the attacker can convince the program to allocate less memory than is actually used.

Changes in Behavior

Sometimes an exception handler handles the exception caused by the overflow, or the overflow occurs during a memory reading operation and not a writing operation. Sometimes this is worse than others, but we should take a quick look at an example of one way to find issues when there are no memory spikes or exceptions.

First, assume a client/server application is called serv2 (included on the book’s companion Web site). Let’s look at all of the interfaces for overflows.

The same code works as both a client and a server. To use as a server, just run serv2.exe. To use as a client, run serv2.exe with the command you want to send to the other client—it uses a loopback socket to simulate a client/server application on just your test machine.

All we know about the server is that it fetches records, and that it supports commands ? and GET (let’s keep it simple). The SDK includes a utility to submit these commands remotely for testing. So let’s start up the server. Note that the user input is in bold type. At the command prompt, type serv2.exe as the command:

E:Chapter8CodeServ2Debug>serv2.exe

Note

Serv2.exe is both a client and a server. From this point forward, all uses of serv2.exe are as a client. For it to work, another copy of serv2.exe must be running as a server (with no command-line arguments specified).

The server (serv2.exe with no parameters) sits there waiting for someone to connect and send commands. In a separate command window, start sending commands, such as the following:

E:Chapter8CodeServ2Debug>serv2.exe "?"
GET Record#

The only documented command is the GET command. But try a few others.

E:Chapter8CodeServ2Debug>serv2.exe "GET 5"
Invalid record number.

Maybe they start with a bigger number.

E:Chapter8CodeServ2Debug>serv2.exe "GET 100"
Record number too long.

OK, the application is obviously doing some validation. What, exactly, is a valid record? After playing around with it, you will find that the only input this application seems to take is GET 0, GET 1, and GET 2.

E:Chapter8CodeServ2Debug>serv2.exe "GET 2"
$85.79 is the value of record 2

You can try letters, other commands, and long input, all to no avail. Time to move on, right? Wrong. Remember how you got the client from the developer? Take a look at what the client is actually sending to the server—remember that not every client is written using the same assumptions as the server.

To do this, in a network application, you could run a network sniffer (refer to Chapter 3, and Appendix A, for more information) to see what the client and the server send each other. That’s a good approach, but here you want to dig more deeply to discover some additional options for breaking things.

Fire up the debugger of choice (use NTSD for now) and put a breakpoint on the send API in Winsock:

E:Chapter8CodeServ2Debug>ntsd serv2.exe "GET 0"

Note

NTSD is a console-based application debugger included with the Windows operating system. The latest version is also available at http://www.microsoft.com/whdc/devtools/debugging/default.mspx.

Once the NTSD window appears, you can set a breakpoint on WS:

0:000> bp WS2_32!send

(Ignore the error saying the debugger could not find symbols.) You can type in the bl (list breakpoints) command to see whether the breakpoint is set properly:

0:000> bl
 0 e 71ab428a     0001 (0001)  0:*** WS2_32!send

Now enter the g (go) command. The breakpoint hits, so now you can look at the stack to see what is actually being sent:

0:000> g                                        
Breakpoint 0 hit                                
WS2_32!send:                                    
71ab428a 8bff             mov     edi,edi       

Troubleshooting

If you do not hit the breakpoint and you see a “connect() failed” message, it indicates the serv2 server is not running. To start the serv2 server, in a second console window run another copy of serv2.exe with no command-line parameters and keep it running while you try again.

Use the d esp command to dump the top of the stack to see the arguments sent to the send API:

0:000> d esp                                                                       
0012fbb8  a0 1f 41 00 a4 07 00 00-fe 12 38 00 06 00 00 00  ..A.......8.....        
0012fbc8  00 00 00 00 00 00 00 00-ec f7 07 00 00 40 fd 7f  .............@..        

To understand this, you need to look up the declaration for the function send. Fortunately, it is documented on Microsoft Developer Network (MSDN).

int send(SOCKET s, const char* buf, int len, int flags);                       

Tip

Microsoft provides a lot of technical information about Microsoft technologies on MSDN. To look up information, go to http://msdn.microsoft.com, and then search for a technology or function by name. Other companies provide similar sites for technologies they work with or support.

Look at the stack (the output from the d esp command). Remember what you learned earlier about the stack? Because this is a 32-bit system, A0 1F 41 00 is probably the return address, although it is stored backward in little endian notation (so the real return address is 0x0041F1A0). The parameters of the send API can be deduced as follows:

  • The A4 07 00 00 is 0x000007A4 and is the socket, corresponding to the SOCKET s parameter of the send API.

  • The FE 12 38 00 is 0x003812FE and is probably a pointer to the buffer sent, corresponding to the const char* buf parameter of the send API.

  • The 06 00 00 00 is 0x00000006 and is the length of the data sent, corresponding to the int len parameter of the send API.

  • The 00 00 00 00 is 0x00000000 and is the int flags portion of the send API.

Dump (output) the memory to see the buffer being sent. As mentioned in the second bullet in the preceding list, you can see that this buffer is located at 0x003812FE:

0:000> d 003812FE
003812fe  47 45 54 20 30 00 fd fd-fd fd ab ab ab ab ab ab  GET 0...........

You can see from the first six bytes of the hexadecimal dump that the client serv2.exe is sending 47 45 54 20 30 00 as the buffer. This is the GET 0 command with a trailing null byte. So what happens if you overwrite that byte with 0xFF? You can write your own client or Perl script to send this without a null byte (that might be more practical, especially if you want to retest this case later), but because you are in the debugger, just do it there.

The 0x00 you wish to replace is five bytes past 0x003812FE.

0:000> e (003812FE + 5)
00381303 00 FF
00381304 fd          (NOTE: Just press ENTER here).
0:000> d 003812FE
003812fe  47 45 54 20 30 ff fd fd-fd fd ab ab ab ab ab ab  GET 0...........

You can edit memory by typing e (003812FE + 5); then it will prompt you with 00 and you can type FF to replace the null. When it presents you with fd, simply press Enter to finish. A quick d 003812FE (see earlier) confirms you have made the right changes:

0:000> g
Record number too long.

When you go again, you get the familiar response from the server. Maybe instead of replacing the null byte you can make the data length shorter. A quick q command enables you to exit the debugger to try again:

E:Chapter8CodeServ2Debug>ntsd serv2.exe "GET 0"
0:000> bp WS2_32!send
0:000> g

The debugger quickly hits the breakpoint:

Breakpoint 0 hit

Examine the stack again.

0:000> d esp
0012fbb8  a0 1f 41 00 a4 07 00 00-fe 12 38 00 06 00 00 00  ..A.......8.....

This time, you can try to tweak the length to exclude the null byte because the server complained the input was too long/invalid (the server didn’t like the 0xFF, even though that was a good test case).

0:000> e esp+c
0012fbc4 06 05
0012fbc5 00

The length was 6, now it is 5. Figure 8-15 shows what happens when you send this case (press G+Enter). You are able to read other sensitive information from the server because the server assumes the client will send a null-terminated string. There is no way to test this case without crafting custom data on the network.

Excluding the null byte

Figure 8-15. Excluding the null byte

Whoa! Notice that? You read a lot of junk out of memory someplace you shouldn’t have seen because the server code assumed the client would always send the null. Your testing paid off—how big might the limit on that credit card be, and what other information can you get from memory (aside from a criminal record)?

It is always worth checking nulls and lengths that go over the wire to see what might come back. In this case, you got all the goods, and they all displayed in the application’s window (the console here). What if the server sent the goods and the client application just didn’t display them? Specifically, what if it worked as follows:

  1. Client sends the request with no null terminator.

  2. Server has the bug you have found here but returns “$85.79 is the value of the record 0[null]N434-...” to the client.

  3. Now, the client has the full information but only displays up to the null in the user interface (UI).

Would you have known there was a bug? How can you really find out what is actually being returned to the client from the server? It can pay huge dividends for you to understand what actually goes over the wire and to test at that level.

Runtime Tools

Fortunately, you won’t have to break out the debugger every time you want to test a certain case. A number of runtime tools available can assist your testing efforts.

Bounds Checker

BoundsChecker, available at http://www.compuware.com/products/devpartner/visualc.htm, allows compilation of an instrumented version of the binary and does bounds checking on a particular set of APIs.

Debugger

Keep in mind that the debugger can be very useful at trapping certain types of exceptions. Refer to the section titled Exceptions earlier in this chapter for more information. Note also that Appendix A lists many popular debuggers in addition to those explicitly used in this chapter.

Gflags.exe

The utility called Gflags ships in the support ools folder on the installation CDs for Microsoft Windows 2000, Windows XP, and Windows Server 2003 (see http://www.microsoft.com/technet/prodtechnol/windowsserver2003/library/TechRef/b6af1963-3b75-42f2-860f-aff9354aefde.mspx for more details). The Gflags utility enables the tester to manipulate how a given executable is loaded and how the heap is managed.

To use Gflags.exe to test for heap overflows, see the walkthrough Heap overruns and Gflags.exe.

Fuzzing

Remember from Chapter 4, that fuzzing is the act of crafting arbitrary data and using it in testing the application. Fuzzing finds other bugs but is particularly effective at finding overflows.

Although fuzzing can produce a fair amount of success at the outset, some of the requirements for successful longer-term fuzzing include the following:

  • High number of iterations.

  • Fuzzing interesting data while keeping the overall data format intact.

  • Automated ability to determine when there is a read AV, write AV, or other case of interest.

  • Ability to weed out duplicate bugs efficiently.

  • A record of the data that caused the problem for reproducibility. For network requests, this might include more than one transaction.

  • Automated ability to get the application in the correct state, where it applies.

Important

When fuzzing identifies bugs, don’t call it a day and stop—fuzzing is actually pointing out weak areas in the product that warrant further attention through manual testing and code review. Fuzzing can help prioritize which areas are most important to code review.

White Box Testing

In addition to black box testing for overflows, it is important to do code analysis and review. A number of approaches can be used to review the code for overflows:

  • Manual linear review. In manual linear review, the code is reviewed by class or file. The main advantage to this is the ability to track review coverage. The main disadvantages of this method include spending time reviewing code that is never called or never called by an attacker’s data and some difficulty in validating how callers use the code without extra research.

  • Following the input. In input tracing review, the code is reviewed starting at the entry point of the data (the API that reads the network bytes, the file, the infrared port, or other input mechanism). The code is then reviewed, often in the debugger, to follow the data as it is copied, parsed, and output. The primary advantage of this method is that it tends to give higher code review coverage to more exploitable scenarios. One main disadvantage is that it is hard to track which code is reviewed in the process.

  • Looking for known dangerous functions. In looking for known dangerous functions, the strategy is to take functions or other code constructs that are known to have caused problems in the past and to audit their use in the application. Although this can be an effective way to identify copies of known common issues, it isn’t a thorough approach. For example, looking for the strcpy function might find bugs, but it will probably miss loops and other equivalent code that might still have overflows.

  • Automated code review. In automated code review, the strategy is to employ a tool that can analyze the code and point out overruns. Although a number of these tools exist and some are improving in quality, most have fairly sketchy coverage and produce a rather high incidence of false positives. Of all tools, clearly the compiler is in the best position to do analysis of the source code itself. Microsoft Visual Studio 2005 with proper build flags, for example, gives compiler warnings for a number of functions that have been deprecated in favor of more secure versions.

Note

There are advantages and disadvantages to reviewing other programmers’ code versus reviewing your own. The main advantage in reviewing your own code is the fact that you are most familiar with it and hence you don’t have to research how it works. Conversely, it is that research and different perspective of a new reviewer that is an advantage in spotting cases where you made the same incorrect assumption writing the code as in reviewing it. In any case, all critical code should be reviewed by programmers who understand buffer overruns and are familiar with how they look in code.

A number of code analysis utilities are beginning to emerge, but two worth mentioning are LCLint and Prefast:

Things to Look For

Programmers write overruns without realizing it—and they are looking at the code while they write it. The question then arises, how does looking at the code help find overruns? It doesn’t—the key to finding overruns is to stop looking at the code itself and stop trying to make things work. Start looking at how the code handles the data, and start trying to make things break. Instead of asking, “How does this function work?” and “What does this function do?” you should start asking, “How can this function be broken if an attacker reverse engineers it?” and “What assumptions doesn’t this function validate that it should?”

Important

How can anyone claim they have thoroughly reviewed or there are no overruns in a set of programming code unless that person first understands what the code does? When you are reviewing code for overruns and encounter functions or references you aren’t familiar with, look up how these unfamiliar elements work rather than assuming they are fine as is.

Although we cannot present an encyclopedic algorithm for reviewing code to identify overruns, we can direct you to a few areas to focus on, which include places where data is copied, allocated, parsed, expanded, and freed.

Data Copying

Any time there is a data copy being performed, ask these questions:

  • How long could the actual input data potentially be?

  • What indicates the size of the data? How reliable is that indication? Are sizes specified in bytes or characters? Is there enough room for a null character at the end of the data?

  • Is there any check to make sure the destination buffer actually was allocated?

  • Are counts of bytes and characters signed or unsigned? Have appropriate checks been done to ensure no integer overflows are possible?

The following code is vulnerable. Can you spot why?

//Function copies a chunk of ANSI data                                 
//  and makes sure it is null terminated.                              
//Returns true if the operation succeeds.                              
//Note: This function contains a security bug.                         
bool SecureCopyString(char *pDestBuff, size_t DestBuffSizeBytes,       
   const char *pSrcBuff, size_t SourceBuffSizeBytes)                   
{                                                                      
   if ((!pDestBuff) || (!pSrcBuff) ||                                  
      (DestBuffSizeBytes < SourceBuffSizeBytes) ||                     
      (DestBuffSizeBytes==0) || (SourceBuffSizeBytes==0))              
   {                                                                   
      return false;                                                    
   }                                                                   
   memcpy(pDestBuff,pSrcBuff,SourceBuffSizeBytes);                     
   //Does it need to be null terminated?                               
   if (*(pDestBuff + SourceBuffSizeBytes - 1) != '')                 
   {                                                                   
      *(pDestBuff + SourceBuffSizeBytes) = '';                       
   }                                                                   
   return true;                                                        
}                                                                      

The null byte is sometimes written one byte past the end of the allocated buffer.

More Info

In general, even overruns that overflow the target buffer by one byte are exploitable. For more information about circumstances when similar bugs are exploitable, see “The Frame Pointer Overwrite” (http://phrack.org/phrack/55/P55-08).

Duplicate Lengths or Size Data

If there is more than one place where the size of the data is stored, analyze whether the allocation and the copy routines use the correct sizes. Can you spot the problem in the following code? Hint: There is at least one.

typedef struct structString                                                      
{                                                                                
   wchar_t *pData;                                                               
   size_t ulDataLength;                                                          
} PACKETSTRING;                                                                  
typedef struct structField                                                       
{                                                                                
   size_t FieldSize;                                                             
   PACKETSTRING Data;                                                            
} PACKETFIELD, *LPPACKETFIELD;                                                   
LPPACKETFIELD CopyPacketField(const LPPACKETFIELD pSrcField)                     
{                                                                                
   if (!pSrcField) return NULL;                                                  
   if (pSrcField->FieldSize <                                                    
      (sizeof(PACKETFIELD) + pSrcField->Data.ulDataLength))                      
   {                                                                             
      return NULL;                                                               
   }                                                                             
   LPPACKETFIELD fldReturn = (LPPACKETFIELD)malloc(pSrcField->FieldSize);        
   if (!fldReturn) return NULL;                                                  
   memcpy(fldReturn,pSrcField,sizeof(PACKETFIELD));                              
   fldReturn->Data.pData = (wchar_t*)(fldReturn + 1);                            
   wmemcpy(fldReturn->Data.pData, pSrcField->Data.pData,                         
   pSrcField->Data.ulDataLength);                                                
   return fldReturn;                                                             
}                                                                                

The allocated memory is based on pSrcField->FieldSize, whereas the actual amount of data copied is pSrcField->Data.ulDataLength. The data length check accidentally fails to multiply pSrcField->Data.ulDataLength by sizeof(wchar_t), so it doesn’t allocate enough memory. Can you spot another issue? What happens if pSrcField->Data.ulDataLength + sizeof(PACKETFIELD) overflows? If pSrcField->FieldSize is sufficiently small (less than the overflowed pSrcField->Data.ulDataLength + sizeof(PACKETFIELD)), a large amount of memory will be copied into a small buffer.

How about this code?

#define min(a,b)            (((a) < (b)) ? (a) : (b))                            
bool CopyBuffer(char *pDestBuff, int DestBuffSize,                               
   const char *pSrcBuff, int SrcBuffSize)                                        
{                                                                                
   if ((!pDestBuff)||(!pSrcBuff)) return false;                                  
   if (DestBuffSize<=0) return false;                                            
   if (SrcBuffSize<0) SrcBuffSize=min(-SrcBuffSize,DestBuffSize);                
   if (SrcBuffSize > DestBuffSize) return false;                                 
   memcpy(pDestBuff,pSrcBuff,SrcBuffSize);                                       
   return true;                                                                  
}                                                                                

This code has a bug when SrcBuffSize is exactly –2147483648. In a nutshell, –2147483648 looks like “1000 0000 0000 0000 0000 0000 0000 0000” in binary. To compute the negative of a signed data type, each bit is inverted (0111 1111 1111 1111 1111 1111 1111 1111), which yields positive 2147483647, and then the value is incremented by one, which overflows the most significant bit (leftmost), resulting in the original negative number. The (SrcBuffSize > DestBuffSize) upper bounds check passes because SrcBuffSize is negative. When memcpy is finally called, this huge negative number is converted back into its unsigned equivalent positive 2147483648, and that’s how many bytes the computer tries to copy.

Parsers

Parsers that accept input from untrusted sources are particularly vulnerable to attack. It really pays to understand how your parsers work as well as the parsers your program relies on. It is amazing how often the parser programmer assumes the data is validated or input only in a certain format but the parser caller assumes the parser is robust against attacker-supplied data. A good general rule of thumb for parsers that are opaque to code analysis is to assume the parser is exploitable until proved otherwise.

In-Place Expansion of Data

One special case of overflows involves expansion of data. Examples of this include ANSI to Unicode, relative path expansion, and various encoding and decoding and decompression operations.

ANSI/OEM to and from Unicode

The primary mistakes programmers make when converting from ANSI to UCS-2 (Unicode) include failure to null terminate the destination buffer with a full wide null character (two bytes) and calling the malloc function to allocate memory and passing in a character count instead of a byte count.

The main issue to look for in converting from UCS-2 to ANSI is the accidental assumption that all ANSI conversions will be half as large in memory as their UCS-2 counterparts. When it comes to UCS-2 characters with Double Byte Character Set (DBCS) ANSI equivalents, both forms use two bytes per character. Malicious input with UCS-2 input that converts to DBCS can lead to overruns.

Note

When we found our first overflow, we were testing a product that used secured Microsoft Jet databases. We attempted to enter a correct long DBCS password, and the product refused to open the database. At first, we thought this was a regular functionality bug. When the developers investigated, however, they discovered that the bug was an exploitable overrun. The programmer had assumed the conversion from Unicode to ANSI would generate a password half as long, so only half of the memory was allocated. When we tried to enter the DBCS password, the conversion that took place wrote past the end of the allocated space because the DBCS characters in their ANSI form each used two bytes, not one.

More Info

For more information about encodings, see Chapter 12, and http://www.microsoft.com/typography/unicode/cs.htm.

Relative Path Expansion

Sometimes paths specified simply as ./foo.exe or the short c:progra~1 or tokens %temp%foo.tmp are expanded to their full glory and there isn’t enough space allocated.

Encoding or Decoding

The URL http://www.contoso.com/#%&#$) might be expanded to http://www.contoso.com/%23%25%26%23%24%29, which increases its length some. Perhaps the logic the programmer used was the following:

  1. Look at the URL specified and determine its length.

  2. Is that length too long? If so, stop.

  3. If not, URL escape the input (this can potentially expand the URL up to three or more times its length).

If before step 3 the programmer didn’t check that the buffer used to expand the URL was large enough, the expansion might overflow when it takes place.

Failing to Null Terminate

To many, failure to end a string with the null byte might seem like a trivial bug. In practice, however, it hides very effectively. Consider, for example, that functions such as strncpy and RegQueryValueEx claim to end the string returned with a null—most of the time but not always. To review code effectively, be on the lookout for cases where the developer makes an incorrect assumption about the function the program calls.

Failing to Reset Freed Pointers

It is generally good practice to reset unused pointers when you are finished with them. That way, other code that tries to write to the memory referenced by the pointer will not reference a new allocation on the heap or stack instead. Failing to reset unused pointers can also lead to memory leaks and double free bugs.

Overflow Exploitability

In the process of investigating buffer overruns and trying to exploit them a number of specific situations arise that present interesting cases. Although we are not trying to present a complete analysis of the topic of exploitability, some discussion is warranted because it is easy to make the wrong assumption about the exploitability of an overrun. The general rule of thumb is that if you can own one byte (or perhaps even fewer bits in some cases) of critical registers, you can usually—through persistence and cleverness—find a way to exploit the overrun.

Why is it important to determine whether an overflow is serious, or how serious it is likely to be? One of the problems with nearly every automated approach to finding overflows is that the approach tends to generate many potential candidate issues, several of which actually aren’t necessarily more serious than ordinary crashes or hangs are. You might end up reporting 100 issues and all but 3 are really duplicates of the same issue or a failure on the programmer’s part to check for null before dereferencing a pointer. Part of your job is to narrow down the number of issues by interpreting how serious the problems really are so that the important issues are prioritized appropriately.

One thing programmers often say is, “Show me the exploit”—and all too often the virus writers have more time on their hands and are all too willing. We have seen overflows in which the programmer thought a particular overflow wasn’t exploitable because it was able to be overflowed by only one byte (into EBP), and it would always be null. Eventually, the overflow was shown to be exploitable. If it isn’t clear whether the bug is exploitable, often it is easier to fix the issue than it is to tell how exploitable the bug is.

Note

Note that arbitrary null byte overwrites (writing a null byte anywhere in memory) are typically exploitable. Some ways attackers might opt to exploit them include overwriting return addresses, base stack pointers, exception handlers, vtable entries; changing the values of variables in memory (changing true to false); and trimming strings.

Suppose you have a crash and want to know how exploitable it is. The first thing to look for is whether EIP or EBP were controlled in any manner. If so, the overflow is exploitable. The next step is to look at the code or disassembly to see whether the cause of the exception can be identified. If so, that will often clarify how serious the issue is.

Some crashes/exceptions are not directly exploitable, but sometimes the input that generated the crash can be changed to cause a different code path or different conditions that would wind up being exploitable. Such a case is Pizza, an app that reads an untrusted input file and takes an order for a pizza.

The format of the input file is as follows:

  • 1 byte—crust

  • 1 byte—size

  • First byte—size of the topping name, followed by the topping

A sample file looks something like the one shown in Figure 8-19.

A binary editor’s view of a sample Meat.Pizza file

Figure 8-19. A binary editor’s view of a sample Meat.Pizza file

Running with the Meat.Pizza file results in the following:

E:Chapter8CodePizza>ReleasePizza.exe Meat.Pizza
Reading pizza file. Thick crust. Medium. Sounds delicious!

After editing the file some and retrying, you might discover a crash with the OverHeated.Pizza input file shown in Figure 8-20.

OverHeated.Pizza, which causes Pizza.exe to crash

Figure 8-20. OverHeated.Pizza, which causes Pizza.exe to crash

When you debug the crash, you see this dialog box:

OverHeated.Pizza, which causes Pizza.exe to crash

Look at the registers.

OverHeated.Pizza, which causes Pizza.exe to crash

Where is the current point of execution?

OverHeated.Pizza, which causes Pizza.exe to crash

Is this an overrun? Possibly, because this happens only with long data. At first, you might carelessly think this is not exploitable because you are simply writing a null value someplace in memory.

At this point it isn’t clear whether this is exploitable. You could take three approaches to clarify:

  • Try changing the content of the data without changing the length to see if you can control where in memory this value is written. This doesn’t seem like a valuable approach because it probably isn’t very important if you can write the 0x00 someplace else.

  • Follow the disassembly up to see how ESI got its value.

  • Look at the code and debug the crash.

Satisfy your curiosity on the first point, and plug in the Try1.pizza input file in Figure 8-21. Notice you are using different long input to try to determine how the input influences what occurs when Pizza.exe crashes.

Changing the content of the input data

Figure 8-21. Changing the content of the input data

When you run the file in Figure 8-21 the following appears when you debug:

Changing the content of the input data

Aha! Last time you used aaaaaaa...aaaa and crashed trying to write to 0x9EB19D95; this time you used bbbbbbbb...bbbb and crashed trying to write to 0x9DB09C93. Those are different places. Hey, wait a minute! 0x9EB19D95 minus 0x9DB09C93 is 0x01010102, which is very close to how much you changed the data! Apparently, you can control where you write this data.

Now turn your attention to the second point mentioned earlier and follow the disassembly up to see how ESI got its value. ESI points to invalid memory when the program crashes. So where is ESI incorrectly set? When you look to find where ESI is changed most recently prior to the crash, you’ll see this line of code revealed in the Disassembly window:

0040104C 2B 33            sub         esi,dword ptr [ebx]

This code takes what EBX points to and subtracts it from ESI. By looking at the disassembly, you can see that EBX doesn’t change between this instruction and the crash, so the current value should work for your investigation. Look in the Memory window to see what EBX points to:

Changing the content of the input data

Somehow the program tried to do math on what it thought was a number but which was actually part of the input string. It looks like either you are overwriting data in memory that you should not be overwriting or the file parser was expecting a number instead of the data. However, the file format doesn’t have 4-byte lengths, so it is most likely the former case.

What happens if attackers can make the data change so that they can work around this crash? What would they change the data to? Well, adding ESI plus [EBX] would give the value of ESI prior to the sub assembly instruction at 0x0040104C:

Changing the content of the input data

This memory looks like it might be on the stack from the addresses; look farther up in memory from there to see if you can find out what the sub subtraction might have been.

Changing the content of the input data

Presto! It looks like all 34 letter b characters are present, so you can figure out that the location in the data you need to overwrite is EBX (the value pulled to subtract) minus 0x0012FEC0 (the start of the data). The data will have to look like bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbXXXXbbbb, where XXXX is the value subtracted from ESI. Now, what value will you subtract? Well, 0x0012FEC0 isn’t a bad choice because it is in your data and you wouldn’t disturb other data. ESI was 0x0012FEF5 as calculated earlier, so you need to subtract (0x0012FEF5 – 0x0012FEC0), or 0x35.

You can try to modify the input:

Changing the content of the input data

When you run with this (Try2.pizza), the debugger presents the following dialog box:

Changing the content of the input data

This is clearly exploitable. So what happened?

With stack overflows, the architecture of the overrun is as follows.

Function foo()                                                                             
{                                                                                          
//Overrun happens here.                                                                    
//Other code runs.                                                                         
//Overrun is exploited when the function returns or some other key event takes place.      
}                                                                                          

Sometimes the other code that runs after the overrun uses values on the stack that are overwritten by the overrun. If no exception handler also is overrun, the application might crash in ways that aren’t clearly overruns. Remember, when it isn’t clear whether the overrun is exploitable, the main thing to focus on is an analysis of the code.

In general, when long input crashes and short input does not crash, you should consider the scenario likely to be exploitable unless good source code analysis proves otherwise. Good source code analysis usually costs more than fixing the overrun.

Unicode Data

Sometimes in the course of looking for an overflow, you might find an overflow where you control the right CPU registers to exploit the overflow but your data is Unicode-encoded (UCS-2) (http://www.unicode.org). If the input is a long string aaaaaaa, instead of 0x61616161 being overwritten you might see 0x61006100 or 0x00610061. Although these cases are still fairly easy to exploit, programmers sometimes mistakenly assume otherwise because every other byte is 0x00.

Despite the fact that you can use fancy means to successfully exploit the data even if every other byte is a zero, sometimes you can inject the payload directly into the UCS-2 data, which generally does not require every other byte to be a zero. This works because often Unicode and ASCII data both are stored in the file, or either is accepted. If the program notices the data is Unicode, it does not convert the data. Say, for example, you had data that looked as shown in Figure 8-22 when saved in a file.

Example of Unicode data

Figure 8-22. Example of Unicode data

Why not simply replace the UCS-2 data with the exploit string? No rules suggest that the zeros must be preserved. In this case, UCS-2 is a dream for attackers because single null characters don’t end the string; 0x0000 must end the string. As shown in Figure 8-23, notice that Unicode data does not necessarily have to contain null bytes.

Example of exploited Unicode

Figure 8-23. Example of exploited Unicode

More Info

For more information, refer to “Creating Arbitrary Shellcode in Unicode Expanded Strings” (http://www.nextgenss.com/papers/unicodebo.pdf).

Filtered Data

Sometimes when you discover an overflow, the argument might well be, “We don’t need to fix that bug because only a handful of characters will ever make it through that network protocol to the weak application.” Perhaps. But in many cases, there is an associated encoding mechanism to represent arbitrary data in that subset of characters.

UCS Transformation Format 8 (UTF-8) and other encodings provide for another way to encode the exploit such that there are no null bytes. Often, the data attackers provide can supply a characterization to the parser about how that data is formatted. This is covered in greater detail in Chapter 12. Suppose you are testing a popular antivirus product and examine the product to discover an overrun in how it processes the Content-Disposition e-mail header. If you want to send null data as part of the exploit, you might be able to do just that because Multipurpose Internet Mail Extensions (MIME) allows you to encode the data any way you please as follows:

=?encoding?q?data?=

where you can hex-escape unprintable characters by using leading equal signs (=). For example, a space, hex 0x20, would be represented as =20. If you like, you could then encode the entire exploit in UTF-8 by properly escaping all of the characters to work around problems in getting the right bits to the vulnerable program.

Other encodings could be interesting, such as base-64 and uuencoding, depending on what the target program supports.

Additional Topics

There are two additional topics to cover here. Some overflows don’t allow for code execution but still result in issues nonetheless. Also, a few ramifications of the /GS compiler switch are worth noting. The following sections discuss some of these types of bugs.

Noncode Execution Overflows Can Be Serious, Too

Sometimes attackers find other ways to exploit overflows besides getting their code to run, and not all serious overflows throw exceptions. Certain overflows do not allow attackers to take control, but might instead allow them to read or manipulate extra data. Such is the case of Logon.exe, a utility that enables administrators to log on to a service. Because the password is cryptographically random each time, it is pretty hard to guess. Logging on without knowing the password requires either looking in memory (we assume this is off limits) or being crafty.

Let’s see how this works. Note that the text in bold type is user input for the walkthrough.

E:Chapter8CodeLogonDebug>Logon.exe
USAGE: Logon.exe <username> <password>

Try entering bogus parameters:

E:Chapter8CodeLogonDebug>Logon.exe User Password
Access Denied.

Then start trying long strings:

E:Chapter8CodeLogonDebug>Logon.exe aaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Access Denied.
E:Chapter8CodeLogonDebug>Logon.exe aaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaa a
Access Denied.
E:Chapter8CodeLogonDebug>Logon.exe a aaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaa
Welcome!! You are now logged in as a.

That’s a bit strange—the service let you log on by using all letter a characters. Check whether it happens again:

E:Chapter8CodeLogonDebug>Logon.exe a ddddddddd
dddddddddddddddddddddddddddddddddddddddddddddddddd
dddddddddddddddddddddddddddddddddddddddddddddddddd
dddddddddddddddddddddddddddddddddddddddddddddddddd
dddddddd
Welcome!! You are now logged in as a.

Using a different password with the same user name still worked! So you must file a bug report about this behavior because the program allows you to log on if you specify a long password, regardless of whether the password is correct. Let’s look at why this is happening.

The class is defined as follows.

#define CREDENTIAL_LENGTH 64                              
class Login {                                             
public:                                                   
   Login();                                               
   void ClearCreds();                                     
   bool IsLoggedIn();                                     
   bool TryCreds(char *Username, char *Password);         
   virtual ~Login();                                      
                                                          
private:                                                  
   char UserName[CREDENTIAL_LENGTH];                      
   char PassPhrase[CREDENTIAL_LENGTH];                    
   char CorrectPassPhrase[CREDENTIAL_LENGTH];             
   char Buffer[512];                                      
};                                                        

What is interesting about this is that the PassPhrase and CorrectPassPhrase are stored sequentially in memory. Look at the code that checks whether the password is correct:

bool Password::IsLoggedIn()                                             
{                                                                       
   return(0==memcmp(PassPhrase,CorrectPassPhrase,CREDENTIAL_LENGTH));   
}                                                                       

That looks good. How about the caller?

bool Login::TryCreds(char *User, char *Password)
{
   FillMemory(UserName,CREDENTIAL_LENGTH,0x00);
   strcpy(UserName,User);
   FillMemory(PassPhrase,CREDENTIAL_LENGTH,0x00);
   strcpy(PassPhrase,Password);
   return IsLoggedIn();
}

Aha! The strcpy(PassPhrase,Password); code looks suspicious. What would happen if this were to overflow the PassPhrase[] buffer? It would start to set the CorrectPassPhrase[] buffer because it comes right afterward in memory. If Password contained 2 * CREDENTIAL_LENGTH bytes, and the first half matched the second half, the function IsLoggedIn check would return true regardless of the real CorrectPassPhrase.

Fixing this is fairly easy: simply check the length of the input and fail if it is too large.

bool Login::TryCreds(char *User, char *Password)                        
{                                                                       
   if ((strlen(User) < CREDENTIAL_LENGTH) &&                            
      (strlen(Password) < CREDENTIAL_LENGTH))                           
   {                                                                    
      FillMemory(UserName,CREDENTIAL_LENGTH,0x00);                      
      strcpy(UserName,User);                                            
      FillMemory(PassPhrase,CREDENTIAL_LENGTH,0x00);                    
      strcpy(PassPhrase,Password);                                      
      return IsLoggedIn();                                              
   }                                                                    
   else                                                                 
   {                                                                    
      return false;                                                     
   }                                                                    
}                                                                       

Trying out the fixed version shows that the bug is fixed for this case. Note that both the user name and password had to be validated because the same overflow existed for the user name, but it was not discovered immediately.

/GS Compiler Switch

In a nutshell, the /GS compiler switch for some functions places a value between stack variables and critical values (including the stored value of EBP and the address to return to) on the stack. Right before using these values, the cookie is then used to determine whether an overrun occurred. If an overrun occurred, it stops rather than running the attacker’s code. (See http://msdn.microsoft.com/library/en-us/dv_vstechart/html/vctchCompilerSecurityChecksInDepth.asp for details.)

That’s nice—but if the Microsoft VS.NET C runtime library is not included in the compilation, the value of the cookie used by the /GS compiler switch will not be random, and attackers can guess what the cookie value will be, meaning the /GS switch will not offer any real protection to victims. A call to __security_init_cookie in seccinit.c (which shows the cookie value to be dependent on a variety of factors, from how long the machine has been running to the current thread and process ID) can remedy the problem.

Testing Whether the Binary Was Compiled Using /GS

One way you can tell whether the cookie is random is to examine the behavior at run time to check the cookie value. If you have debug symbols, you can run the retail binary in the debugger and query the __security_cookie value directly.

To start, launch the application GSWindowsApp, launch the debugger and attach to the process, and then break in the debugger. You can then view the Watch window and add a watch on &__security_cookie. Figure 8-24 shows an example of this.

The value of the /GS security cookie

Figure 8-24. The value of the /GS security cookie

Close the application, run it again, reattach the debugger. Figure 8-25 shows what you get the next time. Compare the values of the cookie and where the cookie is located in memory with those shown in Figure 8-24. The cookie is stored at the same place both times for this application, but the value changes.

The value of the /GS security cookie and its location in memory

Figure 8-25. The value of the /GS security cookie and its location in memory

Note

Think about it: will the security cookie always be stored in the same location for a particular application? Could that create some potential for issues?

If the security cookie value had been the same for both cases, you would flag that as a major issue.

The next question becomes: how can you tell whether the /GS switch was used to compile the binary?

Warning

Even retail binaries compiled without the /GS switch on set a value for the __security_cookie.

One way to tell is to set a breakpoint on the __security_check_cookie function. If the EXE is linked with libraries that were compiled with the /GS switch turned on or it loads dependencies with the switch turned on (such as msvcrt), you will see some hits with this regardless. The application will function fine and the hits will be occasional, if at all. You can examine the call stack to see whether the application (code your developers wrote) is on the top of the call stack or not to determine whether your code called this __security_check_cookie function.

Figure 8-26 shows a case where the code calls the __security_check_cookie function.

Code actually checking the cookie

Figure 8-26. Code actually checking the cookie

By checking out the caller in Figure 8-26, you can tell which code called the security check handler and thus whether this particular hit on the breakpoint was caused by your code or another component’s code. In this case, our code is revealed as causing the hit. Note this is valid to do only with the nonoverflow test data—overflow test data corrupts the call stack and the caller cannot be trusted in the call stack window for that case.

Note

A number of people have researched the /GS switch and have found that it really is only a defense-in-depth measure, and that there are ways around it. David Litchfield published some ways around the cookie at http://www.blackhat.com/presentations/bh-federal-03/bhfed-03-litchfield.pdf.

/GS Information Disclosure Vulnerability

When you test a /GS-compiled binary for overruns and encounter the /GS dialog box shown in Figure 8-27, you have a fairly decent indication a buffer overrun has occurred. First, even though /GS aborts the program, these overruns should still be fixed. Programmers could copy the code or enough changes might occur to the function and the /GS switch might not protect victims against the (now advertised) overrun any longer.

Consider this scenario briefly: you are testing or using the product, and you see the dialog box shown in Figure 8-27. This dialog box appears any time the /GS stack checks fail. If it appears, the same test case typically results in an exploitable condition for the same code built without the /GS switch.

The /GS dialog box

Figure 8-27. The /GS dialog box

Picture what happens when users get the message shown in the figure. On the surface, that’s a good thing, right? Users are protected. The programmer should fix the bug.

But consider this: what happens if a malicious user sees this message and investigates only to find the problem exists in previous versions of the application that were not compiled with the /GS switch turned on? It really pays to take care of these overruns.

Testing Tips

Testing for overflows in your application is well worth the effort. Here are a few tips for testing.

  • Remember to look for overruns where the attacker can get data: network data, files and documents, information shared between users, and programmable interfaces.

  • Using data you can recognize helps when you are analyzing whether a particular overrun is exploitable.

  • Learn how to determine the bounds of data by reading the code, asking people for information, using commonly defined lengths, employing the iterative approach, and watching for changes in behavior.

  • When you construct test cases, don’t forget to maintain overall data integrity by adjusting for various structural and format considerations, and make sure your test cases strike deeply within the program’s functionality when warranted.

  • Keep an eye out for secondary actions or program state dependencies that might render the code exploitable.

  • Watch for exceptions, crashes, and other changes in behavior. Remember, when you enter long data and the application shows you extra memory or behaves oddly, the situation is often worth investigating and is exploitable.

  • Use the debugger to help find handled exceptions. Remember that handled exceptions can be exploitable also.

  • Use fuzzing and other runtime tools to help you find overruns and identify areas on which to focus future code review and testing efforts.

  • Try different strategies, including replacing null characters, inserting data, overwriting data, and adjusting string lengths.

  • Looking at the code is an important part of finding buffer overruns. When you look at the code for overruns, stop trying to create the code and start trying to break it.

  • Key areas on which to focus code review include unsafe functions, data entry points and data flow, and places where data is copied and parsed.

Summary

Buffer and integer overruns are some of the costliest security vulnerabilities known to affect computer software, and learning how to find them is fun, rewarding, and important. This chapter introduces the concepts behind overflows, details strategies for taking normal expected data and creating targeted test cases, explains what the signs and symptoms of overflows are, and gives tips and approaches for code review. Several walkthroughs can help you see and sense how different kinds of overflows respond to test cases.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset