Information in this Chapter
Uninitialized/Nonvalidated/Corrupted Pointer Dereference
Memory Corruption Vulnerabilities
Integer Issues
Race Conditions
Logic Bugs (a.k.a. the Bug Grab Bag)
Introduction
Software has bugs. A bug is a malfunction in a program that makes the program produce incorrect results, behave in an undesired way, or simply crash/terminate unexpectedly. In most cases, bugs are the result of programming errors, as is the case in the following snippet of code taken from the 2.6.9 version of the Linux Kernel:
static int bluez_sock_create(struct socket *sock, int proto)
{
if (proto >= BLUEZ_MAX_PROTO)
return –EINVAL;
[…]
return bluez_proto[proto]->create(sock,proto);
}
In this code, the parameter proto is checked against a maximum value, BLUEZ_MAX_PROTO, to avoid reading past the size of the bluez_proto array later, when proto is used as an index inside the array. The problem here is that proto is a signed integer, and as such it can have a negative value. Therefore, if proto is less than 0, any memory before the bluez_proto array will be accessed. Since this memory is used as a function pointer, this bug likely will result in a crash when either attempting to dereference an unmapped address or wrongly accessing some other memory location as a consequence of executing a random sequence of bytes. The obvious way to fix this bug is to simply check if proto is less than 0 at the start of the function, and to error out if it is. (This is exactly what Linux kernel developers did in 2005 after they were notified of the issue.1 )
When they are not a consequence of a programming error, bugs almost always are a consequence of design flaws (especially when it comes to large projects, as the kernel indeed is). A design flaw, as the name suggests, is a weakness in a software program's architecture, and is fundamentally language-independent (i.e., regardless of the language used to implement the software, the security issue will still be present). A classic example of a design flaw is to rely on a weak encryption scheme or to implicitly trust some component of the architecture that an attacker could impersonate or manipulate without the need for certain privileges. We provide a detailed example of a design flaw in the “Kernel-Generated User-Land Vulnerabilities” subsection later in this chapter.
Of course, not all bugs are security bugs. In fact, bugs usually have nothing to do with security. Simply put, a bug becomes a security issue as soon as someone figures out how to gain privileges from it. Sometimes the approach used to exploit a specific bug can be generalized and reused on similar bugs. In these cases, we are referring to bug classes and exploiting techniques. The more precisely you can define and characterize these classes, the more accurate and reliable your exploiting techniques will be. This is the goal of the taxonomy we present in this chapter.
Uninitialized/Nonvalidated/Corrupted Pointer Dereference
Perhaps the most famous kernel bug class is the NULL pointer dereference. As every C manual states, a pointer is a variable that holds the address of another variable in memory. Each time the pointer is dereferenced, the value contained at the memory address it holds is retrieved. The ISO C standard2 dictates that a static, uninitialized pointer has a NULL (0x0) value, and NULL is the usual return value that indicates failure in a memory allocation function. If a kernel path attempts to dereference a NULL pointer, it will simply try to use the memory address 0x0, which likely will result in a panic condition, since usually nothing is mapped there. The number of NULL pointer dereference bugs that have been discovered in the various kernels is impressive, as a quick search on your favorite search engine will prove.
NULL pointer dereference vulnerabilities are a subset of a larger class of bug known as the uninitialized/nonvalidated/corrupted pointer dereference. This category covers all situations in which a pointer is used while its content has been corrupted, was never properly set, or was not validated enough. We know a static declared pointer is initialized to NULL, but what happens to a pointer declared as a local variable in a function? And what is the content of a pointer contained in a structure freshly allocated in memory? Until these pointers are explicitly assigned a value, they are uninitialized and their value is unspecified. Let's look at this in a little more detail.
We said that a pointer is a variable and, as with any variable, it has a size and needs to be stored in memory to be used. The size of the pointer depends on the data model the system uses and is usually directly influenced by the system architecture. The data model is usually expressed using the int, long, and pointer size notation; for example, ILP32 refers to a system in which all ints, longs, and pointers are 32 bits wide, whereas LP64 refers to a system in which longs and pointers are 64 bits wide but integers are not (in fact, integers will be 32 bits, but that's not explicitly stated). Table 2.1 provides a recap of data type sizes for each model (sizes are expressed in number of bits).
Data type | LP32 | ILP32 | LP64 | ILP64 | LLP64 |
Char | 8 | 8 | 8 | 8 | 8 |
Short | 16 | 16 | 16 | 16 | 16 |
Int | 16 | 32 | 32 | 64 | 32 |
Long | 32 | 32 | 64 | 64 | 32 |
Long long | 64 | 64 | 64 | 64 | 64 |
Pointer | 32 | 32 | 64 | 64 | 64 |
Now, let's say the ILP32 model is in place. In this case, the pointer occupies four bytes in memory. While the pointer is uninitialized, its value is whatever value resides in the memory assigned to hold the pointer variable. People already familiar with writing exploits (or who have an exploit-oriented mindset) might be wondering if it is possible to predict the value of that memory and use it to their advantage. The answer is yes, in many cases it is (or, at least, it is possible to have an idea of the range). For instance, consider a pointer declared as a local variable, as shown in the following code. This pointer will be stored on the stack, and its value will be the previous function left on the stack:
#include <stdio.h>
#include <strings.h>
void big_stack_usage() {
char big[200];
memset(big,'A', 200);
}
void ptr_un_initialized() {
char *p;
printf("Pointer value: %p ", p);
}
int main()
{
big_stack_usage();
ptr_un_initialized();
}
By compiling and executing the preceding code (remember that the hexadecimal code of A is 0x41), we get the following:
macosxbox$ gcc -o p pointer.c
macosxbox$ ./p
Pointer value: 0x41414141
macosxbox$
As you can see, the pointer allocated inside ptr_un_initialized() has, as we predicted, the value the previous function left on the stack. A range of memory that has some leftover data is usually referred to as dead memory (or a dead stack). Granted, we crafted that example, and you might think such a thing is unlikely to happen. It is indeed rare, but what about the following FreeBSD 8.0 path?3
struct ucred ucred, *ucp; [1]
[…]
refcount_init(&ucred.cr_ref, 1);
ucred.cr_uid = ip->i_uid;
ucred.cr_ngroups = 1;
ucred.cr_groups[0] = dp->i_gid; [2]
ucp = &ucred;
At [1] ucred is declared on the stack. Later, the cr_groups[0] member is assigned the value dp->i_gid. Unfortunately, struct ucred is defined as follows:
struct ucred {
u_int cr_ref; /* reference count */
[…]
gid_t *cr_groups; /* groups */
int cr_agroups; /* Available groups */
};
As you can see, cr_groups is a pointer and it has not been initialized (but it is used directly) by the previous snippet of code. That means the dp->i_gid value is written to whatever address is on the stack at the time ucred is allocated.
Moving on, a corrupted pointer is usually the consequence of some other bug, such as a buffer overflow (which we describe in the following section, “Memory Corruption Vulnerabilities”), which trashes one or more of the bytes where the pointer is stored. This situation is more common than using an uninitialized variable (with the notable exception of NULL dereferences) and usually gives the attacker some degree of control over the contents of the variable, which directly translates into a more reliable exploit.
A nonvalidated pointer issue makes the most sense in a combined user and kernel address space. As we said in Chapter 1 in such an architecture the kernel sits on top of user land and its page tables are replicated inside the page tables of all processes. Some virtual address is chosen as the limit address: this means virtual addresses above (or below) it belong to the kernel, and virtual addresses below (or above) it belong to the user process. Internal kernel functions use this address to decide if a specific pointer points to kernel land or user land. In the former case usually fewer checks are necessary, whereas in the latter case more caution must be taken before accessing it. If this check is missing (or is incorrectly applied) a user-land address might be dereferenced without the necessary amount of control.
As an example, take a look at the following Linux path:4
error = get_user(base, &iov->iov_base); [1]
[…]
if (unlikely(!base)) {
error = -EFAULT;
break;
}
[…]
sd.u.userptr = base; [2]
[…]
size = __splice_from_pipe(pipe, &sd, pipe_to_user);
[…]
static int pipe_to_user(struct pipe_inode_info *pipe, struct pipe_buffer *buf,
struct splice_desc *sd)
{
if (!fault_in_pages_writeable(sd->u.userptr, sd->len)) {
src = buf->ops->map(pipe, buf, 1);
ret = __copy_to_user_inatomic(sd->u.userptr, src + buf->offset, sd->len); [3]
buf->ops->unmap(pipe, buf, src);
[…]
}
The first part of the snippet comes from the vmsplice_to_user() function and gets the destination pointer at [1] using get_user(). That destination pointer is never validated and is passed, through [2], to __splice_from_pipe(), along with pipe_to_user() as the helper function. This function also does not perform any checks and ends up calling __copy_to_user_inatomic() at [3]. We will discuss in the rest of the book the various ways to copy, from inside kernel land, to and from user space; for now, it's enough to know that Linux functions starting with a “__” (such as __copy_to_user_inatomic()) don't perform any checks on the supplied destination (or source) user pointer. This vulnerability allows a user to pass a kernel address to the kernel, and therefore directly access (modify) kernel memory.
Thus far we have discussed dereferencing pointers, but we have not discussed the type of access performed by the kernel path that uses them. An arbitrary read occurs when the kernel attempts to read from the trashed pointer, and an arbitrary write occurs when the kernel attempts to store a value on the memory address referenced by the pointer (as was the case in the preceding example). Moreover, a controlled or partially controlled read/write occurs when the attacker has full or partial control over the address that the pointer will point to, and an uncontrolled read/write occurs when the attacker has no control over the value of the trashed pointer. Note that an attacker might be able to predict to some extent the source/destination of an uncontrolled read/write, and therefore successfully and, more importantly, reliably exploit this scenario too.
Memory Corruption Vulnerabilities
The next major bug class we will analyze covers all cases in which kernel memory is corrupted as a consequence of some misbehaving code that overwrites the kernel's contents. There are two basic types of kernel memory: the kernel stack, which is associated to each thread/process whenever it runs at the kernel level, and the kernel heap, which is used each time a kernel path needs to allocate some small object or some temporary space.
As we did for pointer corruption vulnerabilities (and as we will do throughout this chapter), we leave the details regarding exploitation of such issues for Chapter 3, (for generic approaches) and to the chapters in Part II of this book.
Kernel Stack Vulnerabilities
The first memory class we will examine is the kernel stack. Each user-land process running on a system has at least two stacks: a user-land stack and a kernel-land stack. The kernel stack enters the game each time the process traps to kernel land (i.e., each time the process requests a service from the kernel; for example, as a consequence of issuing a system call).
The generic functioning of the kernel stack is not different from the generic functioning of a typical user-land stack, and the kernel stack implements the same architectural conventions that are in place in the user-land stack. These conventions comprise the growth direction (either downward, from higher addresses to lower addresses, or vice versa), what register keeps track of its top address (generally referred to as the stack pointer), and how procedures interact with it (how local variables are saved, how parameters are passed, how nested calls are linked together, etc.).
Although the kernel- and user-land stacks are the same in terms of how they function, there are some slight differences between the two that you should be aware of. For instance, the kernel stack is usually limited in size (4KB or 8KB is a common choice on x86 architectures), hence the paradigm of using as few local variables as possible when doing kernel programming. Also, all processes' kernel stacks are part of the same virtual address space (the kernel address space), and so they start and span over different virtual addresses.
Note
Some operating systems, such as Linux, use so-called interrupt stacks. These are per-CPU stacks that get used each time the kernel has to handle some kind of interrupt (in the Linux kernel case, external hardware-generated interrupts). This particular stack is used to avoid putting too much pressure on the kernel stack size in case small (4KB for Linux) kernel stacks are used.
As you can see from this introduction, kernel stack vulnerabilities are not much different from their user-land counterparts and are usually the consequence of writing past the boundaries of a stack allocated buffer. This situation can occur as a result of:
Using one of the unsafe C functions, such as strcpy() or sprintf(). These functions keep writing to their destination buffer, regardless of its size, until a