The Art of Software Security Assessment: Identifying and Preventing Software Vulnerabilities

Now that you're familiar with the landscape of memory corruption, you need to know how to accurately assess the risk these vulnerabilities represent. A number of factors affect how exploitable a vulnerability is. By being aware of these factors, code auditors can estimate how serious a vulnerability is and the extent to which it can be exploited. Can it be used just to crash the application? Can arbitrary code be run? The only way to know for certain is to write a proof-of-concept exploit, but that approach can be far too time consuming for even a moderate-sized application assessment. Instead, you can reasonably estimate exploitability by answering a few questions about the resulting memory corruption. This approach is not as definitive as a proof-of-concept exploit, but it's far less time consuming, making it adequate for most assessments.

The Real Cost of Fixing Vulnerabilities

You might be surprised at the amount of resistance you can encounter when disclosing vulnerabilities to vendorseven vendors who specifically hired you to perform an assessment. Vendors often say that potential memory corruption bugs aren't exploitable or aren't problems for some reason or another. However, memory corruption affects an application at its most basic level, so all instances need to be given serious consideration. Indeed, history has shown that attackers and security researchers alike have come up with ingenious ways to exploit the seemingly unexploitable. The old adage "where there's a will, there's a way" comes to mind, and when it comes to compromising computer systems, there's definitely a lot of will.

Therefore, most auditors think that software vendors should treat all issues as high priority; after all, why wouldn't vendors want their code to be as secure as possible and not fix problems as quickly as they can? The truth is that there's always a price attached to fixing software bugs, including developer time, patch deployment cost, and possible product recalls or reissues. Consider, for example, the cost of distributing a vulnerability update to a widely deployed embedded system, like a smart card or cell phone. Updating these embedded systems often requires hardware modifications or some other intervention by a qualified technician. A company would be irresponsible to incur the costs associated with an update if it doesn't have a reasonable expectation that the bug is exploitable.

Where Is the Buffer Located in Memory?

The location of the buffer in memory is important; it affects what choices an attacker has when trying to seize control of the process. Variables are stored mainly in three memory areas: stack, heap, and persistent data (including static and global variables). However, different OSs often further segment these three regions or add new regions. There might be distinctions between initialized and uninitialized global data, or the system might place thread local storage (TLS) at a special location. Also, shared libraries typically have their own uninitialized and initialized data mapped into the process memory immediately after their program code. When determining exploitability, you need to keep track of where the memory corruption occurs and what special considerations apply. This task might include conducting some additional research to understand the process memory layout for a particular OS.

What Other Data Is Overwritten?

Memory corruption might not be isolated to just the variables an attacker is targeting. It can also overwrite other variables that might complicate the exploitation process. This happens commonly when trying to exploit corruption on the process stack. You already know that vulnerabilities in the stack segment are most often exploited by overwriting the saved program counter. It's not always that straightforward, however; often attackers overwrite local variables before overwriting the saved program counter, which can complicate exploitation, as shown in Listing 5-4.

Listing 5-4. Overflowing into Local Variables

int dostuff(char *login) { char *ptr = (char *)malloc(1024); char buf[1024]; ... strcpy(buf, login); ... free(ptr); return 0; }

This example has a small issue: Although attackers can overwrite the saved program counter, they also overwrite the ptr variable, which gets freed right before the function returns. This means attackers must overwrite ptr with a location in memory that's valid and doesn't result in a crash in the call to free(). Although this method makes it possible for attackers to exploit the call to free(), the attack method is more complicated than a simple program counter overwrite (especially if there's no user-controlled data at a static location in memory).

When evaluating the risk of buffer overflow vulnerabilities, pay special attention to any variables in the overflow path that mitigate exploit attempts. Also, remember that the compiler might reorder the variable layout during compilation, so you might need to check the binary to confirm exploitability.

Note

Sometimes more than one function return is required for a bug to be exploitable. For example, OSs running on Sun SPARC CPUs often require two function returns because of the way SPARC register windows work.

How Many Bytes Can Be Overwritten?

You need to take into account how many bytes the buffer overflows and how much control users have over the size of the overflow. Overflows of too few or too many bytes can make the exploit a lot harder. Obviously, the ideal situation for an attacker is to choose an arbitrary length of data to overflow.

Sometimes an attacker can overflow a buffer by a fixed amount, which provides fewer options, but successful exploitation is still likely. If only a small number of bytes can be overflowed, exploitability depends on what data is corrupted. If the attacker is able to corrupt only an adjacent variable in memory that's never used again, the bug is probably unexploitable. Obviously, the less memory the attacker can corrupt, the less likely it is that the bug is exploitable.

Conversely, if attackers can overflow by a fixed amount that happens to be very large, the bug invariably results in corrupting a huge part of the program's memory and will almost certainly crash the process. In some cases, when a signal handler or exception handler can be corrupted, attackers can exploit this situation and gain control of the process after an exception has occurred. The most prevalent example is large stack-based overflows in Windows, as attackers can overwrite SEH structures containing function pointers that are accessed when an exception occurs.

Additionally, some bugs can result in multiple writes to arbitrary locations in memory. Although often only one overwrite is possible, if multiple overwrites can be performed, an attacker has more leverage in choosing how to exploit the vulnerable program. For example, with format string vulnerabilities, attackers can often write to as many arbitrary locations as they choose, increasing the likelihood of successful exploitation.

Note

Sometimes a 1- or 2-byte overwrite is easier to exploit than a 4-byte overwrite. For example, say you overwrite a pointer to an object composed of several pointers followed by a buffer with data you control. In this case, the least significant byte of the pointer value could be overwritten so that the data buffer in the object is pointed to rather than the object itself. You could arbitrarily change the state of any object property and probably exploit the bug quite reliably.

What Data Can Be Used to Corrupt Memory?

Some memory corruption vulnerabilities don't allow direct control of the data used to overwrite memory. The data might be restricted based on how it's used, as with character restrictions, single-byte overwrites, or attacker-malleable calls to memset(). Listing 5-5 shows an example of a vulnerability in which memory is overwritten with data the attacker doesn't control.

Listing 5-5. Indirect Memory Corruption

int process_string(char *string) { char **tokens, *ptr; int tokencount; tokens = (char **)calloc(64, sizeof(char *)); if(!tokens) return -1; for(ptr = string; *ptr;){ int c; for(end = ptr; *end && !isspace(end); end++); c = *end; *end = '\0'; tokens[tokencount++] = ptr; ptr = (c == 0 ? end : end + 1); } ...

This code has a buffer overflow in the bolded line manipulating the tokens array. The data used to overwrite memory can't be controlled directly by attackers, but the overwritten memory includes pointers to attacker-controllable data. This could make exploitation even easier than using a standard technique. If a function pointer is overwritten, for example, attackers require no memory layout information because the function pointer can be replaced with a pointer to attacker-controlled data. However, exploitation could be more complicated if, for example, a heap block header or other complex structure is overwritten.

Off-by-one vulnerabilities are one of the most common vulnerabilities involving overwritten data that an attacker doesn't control. Listing 5-6 shows an example of an off-by-one vulnerability.

Listing 5-6. Off-by-One Overwrite

struct session { int sequence; int mac[MAX_MAC]; char *key; }; int delete_session(struct session *session) { memset(session->key, 0, KEY_SIZE); free(session->key); free(session); } int get_mac(int fd, struct session *session) { unsigned int i, n; n = read_network_integer(fd); if(n > MAX_MAC) return 1; for(i = 0; i <= n; i++) session->mac[i] = read_network_integer(fd); return 0; }

If attackers specify the length of mac to be exactly MAX_MAC, the get_mac() function reads one more element than it has allocated space for (as shown in the bolded line). In this case, the last integer read in overwrites the key variable. During the delete_session() function, the key variable is passed to memset before it's deleted, which allows attackers to overwrite an arbitrary location in memory, but only with NUL bytes. Exploiting this vulnerability is complicated because attackers can't choose what data the memory is overwritten with. In addition, the attacker-supplied memory location is subsequently freed, which means that attack would most likely be directed at the memory-management routines. Performing this attack successfully could be extremely difficult, especially in multithreaded applications.

Listings 5-5 and 5-6 show how attackers might have difficulty exploiting a vulnerability when the overwritten data can't be controlled. When examining similar issues, you need to determine what's included in the overwritten data and whether it can be controlled by attackers. Usually, attackers have fairly direct control over the data being written or can manipulate the resulting corruption to access attacker-controlled data.

Are Memory Blocks Shared?

Occasionally, bugs surface in applications in which a memory manager erroneously hands out the same block of memory more than once, even though it's still in use. When this happens, two or more independent parts of the application use the memory block with the expectation that they have exclusive access to it, when in fact they don't. These vulnerabilities are usually caused by one of two errors:

  • A bug in the memory-management code

  • The memory-management API being used incorrectly

These types of vulnerabilities can also lead to remote execution; however, determining whether memory-block-sharing vulnerabilities are exploitable is usually complicated and application specific. One reason is that attackers might not be able to accurately predict what other part of the application gets the same memory block and won't know what data to supply to perform an attack. In addition, there might be timing issues with the application, particularly multithreaded software servicing a large number of clients whenever they happen to connect. Accepting the difficulties, there are procedures for exploiting these vulnerabilities, so they shouldn't be regarded as low priority without justification.

A similar memory corruption can occur in which a memory block is allocated only once (the correct behavior), but then that memory block is handed off to two concurrently running threads with the assumption of mutually exclusive access. This type of vulnerability is largely caused by synchronization issues and is covered extensively in Chapter 13, "Synchronization and State."

What Protections Are in Place?

After you know the details of a potentially exploitable memory corruption vulnerability, you need to consider any mitigating factors that might prevent exploitation. For example, if a piece of software is going to run only on Windows XP SP2+, you know that stack cookies and SafeSEH are present, so a typical stack overflow might not be exploitable. Of course, you can't discount memory corruption just because protective measures are in place. It's quite possible that an attacker could find a way to subvert SafeSEH by using an unsafe loaded module or overwriting a function parameter to subvert stack cookies. However, you need to account for these protective measures and try to gauge the likelihood of an attacker circumventing them and reliably exploiting the system.

Категории