The book is organized into four parts. Part I introduces the kernel and sets out the theoretical basis on which to build the rest of the book. Part II focuses on different operating systems and describes exploits for them that target various bug classes. Part III on remote kernel exploitation analyzes the effects of the remote scenario and presents new techniques to target remote issues. It includes a step-by-step analysis of the development of a reliable, one-shot, remote exploit for a real vulnerabilitya bug affecting the SCTP subsystem found in the Linux kernel. Finally, Part IV wraps up the analysis on kernel exploitation and looks at what the future may hold.
- Covers a range of operating system families — UNIX derivatives, Mac OS X, Windows
- Details common scenarios such as generic memory corruption (stack overflow, heap overflow, etc.) issues, logical bugs and race conditions
- Delivers the reader from user-land exploitation to the world of kernel-land (OS) exploits/attacks, with a particular focus on the steps that lead to the creation of successful techniques, in order to give to the reader something more than just a set of tricks
|Sold by:||Barnes & Noble|
|File size:||4 MB|
About the Author
Massimiliano Oldani currently works as a Security Consultant at Emaze Networks. His main research topics include operating system security and kernel vulnerabilities.
Read an Excerpt
A Guide to Kernel ExploitationAttacking the Core
By Enrico Perla Massimiliano Oldani
SYNGRESSCopyright © 2011 Elsevier Inc.
All right reserved.
Chapter OneFrom User-Land to Kernel-Land Attacks
INFORMATION IN THIS CHAPTER
Introducing the Kernel and the World of Kernel Exploitation
Why Doesn't My User-Land Exploit Work Anymore?
An Exploit Writer's View of the Kernel
Open Source versus Closed Source Operating Systems
This chapter introduces our target, the kernel. After a short discussion of kernel basics, we analyze why exploit writers have shifted their attention from user-land applications to the kernel itself, and we outline the differences between a user-land and a kernel-land exploit. Then we focus on the differences between various kernels. As well as discussing the ways in which Windows kernels are different from UNIX kernels, we explore how architectural variations play a significant role in the development of kernel exploits; for instance, the same piece of code might be exploitable only on a 32-bit system and not on a 64-bit system, or only on an x86 machine and not on a SPARC. We finish the chapter with a brief discussion of the differences between kernel exploitation on open source and closed source systems.
INTRODUCING THE KERNEL AND THE WORLD OF KERNEL EXPLOITATION
We start our journey through the world of kernel exploitation with an obvious task: explaining what the kernel is and what exploitation means. When you think of a computer, most likely you think of a set of interconnected physical devices (processor, motherboard, memory, hard drive, keyboard, etc.) that let you perform simple tasks such as writing an e-mail, watching a movie, or surfing the Web. Between these bits of hardware and the applications you use every day is a layer of software that is responsible for making all of the hardware work efficiently and building an infrastructure on top of which the applications you use can work. This layer of software is the operating system, and its core is the kernel.
In modern operating systems, the kernel is responsible for the things you normally take for granted: virtual memory, hard-drive access, input/output handling, and so forth. Generally larger than most user applications, the kernel is a complex and fascinating piece of code that is usually written in a mix of assembly, the low-level machine language, and C. In addition, the kernel uses some underlying architecture properties to separate itself from the rest of the running programs. In fact, most Instruction Set Architectures (ISA) provide at least two modes of execution: a privileged mode, in which all of the machine-level instructions are fully accessible, and an unprivileged mode, in which only a subset of the instructions are accessible. Moreover, the kernel protects itself from user applications by implementing separation at the software level. When it comes to setting up the virtual memory subsystem, the kernel ensures that it can access the address space (i.e., the range of virtual memory addresses) of any process, and that no process can directly reference the kernel memory. We refer to the memory visible only to the kernel as kernel-land memory and the memory a user process sees as user-land memory. Code executing in kernel land runs with full privileges and can access any valid memory address on the system, whereas code executing in user land is subject to all the limitations we described earlier. This hardware- and software-based separation is mandatory to protect the kernel from accidental damage or tampering from a misbehaving or malicious user-land application.
Protecting the kernel from other running programs is a first step toward a secure and stable system, but this is obviously not enough: some degree of protection must exist between different user-land applications as well. Consider a typical multiuser environment. Different users expect to have a "private" area on the file system where they can store their data, and they expect that an application that they launch, such as their mail reader software, cannot be stopped, modified, or spied on by another user. Also, for a system to be usable there must be some way to recognize, add, and remove users or to limit the impact they can have on shared resources. For instance, a malicious user should not be able to consume all the space available on the file system or all the bandwidth of the system's Internet connection. This abstraction would be too expensive to implement in hardware, and therefore it is provided at the software level by the kernel.
Users are identified by a unique value, usually a number, called the userid, and one of these values is used to identify a special user with higher privileges who is "responsible" for all the administrative tasks that must be performed, such as managing other users, setting usage limits, configuring the system, and the like. In the Windows world this user is called the Administrator, whereas in the UNIX world he or she is traditionally referred to as root and is generally assigned a uid (userid) of 0. Throughout the rest of this book, we will use the common term of super user to refer to this user.
The super user is also given the power to modify the kernel itself. The reason behind this is pretty obvious: just like any other piece of software, the kernel needs to be updated; for example, to fix potential bugs or include support for new devices. A person who reaches super-user status has full control over the machine. As such, reaching this status is the goal of an attacker.
The super user is distinguished from "the rest of the (unprivileged) world" via a traditional "privilege separation" architecture. This is an all-or-nothing deal: if a user needs to perform privileged operation X, that user must be designated as the super user, and he or she can potentially execute other privileged operations besides X. As you will see, this model can be improved from a security standpoint by separating the privileges and giving to any user only the privileges he or she needs to perform a specific task. In this scenario, becoming the "super user" might not mean having full control over the system, since what really controls what a specific user-land program can or cannot do are the privileges assigned to it.
The Art of Exploitation
"I hope I managed to prove that exploiting buffer overflows should be an art." Solar Designer
Among the various ways an attacker can reach the desired status of super user, development of an exploit is the one that usually generates the most excitement. Novices often view exploitation as some sort of magic process, but no magic is involved—only creativity, cleverness, and a lot of dedication. In other words, it is an art. The idea behind exploitation is astonishingly simple: software has bugs, and bugs make the software misbehave, or incorrectly perform a task it was designed to perform properly. Exploiting a bug means turning this misbehavior into an advantage for the attacker. Not all bugs are exploitable; the ones that are, are referred to as vulnerabilities. The process of analyzing an application to determine its vulnerabilities is called auditing. It involves:
Reading the source code of the application, if available
Reversing the application binary; that is, reading the disassembly of the compiled code
Fuzzing the application interface; that is, feeding the application random or pattern-based, automatically generated input
Auditing can be performed manually or with the support of static and dynamic analysis tools. As a detailed description of the auditing process is beyond the scope of this book, if you are interested in learning more about auditing refer to the "Related Reading" section at the end of this chapter for books covering this topic.
Vulnerabilities are generally grouped under a handful of different categories. If you are a casual reader of security mailing lists, blogs, or e-zines, you no doubt have heard of buffer (stack and heap) overflows, integer overflows, format strings, and/or race conditions.
We provide a more detailed description of the aforementioned vulnerability categories in Chapter 2.
Most of the terms in the preceding paragraph are self-explanatory and a detailed understanding of their meaning is not of key importance at this point in the book. What is important to understand is that all the vulnerabilities that are part of the same category exhibit a common set of patterns and exploitation vectors. Knowing these patterns and exploitation vectors (usually referred to as exploiting techniques) is of great help during exploit development. This task can be extremely simple or amazingly challenging, and is where the exploit writer's creativity turns the exploitation process into an art form. First, an exploit must be reliable enough to be used on a reasonably wide range of vulnerable targets. An exploit that works on only a specific scenario or that just crashes the application is of little use. This so-called proof of concept (PoC) is basically an unfinished piece of work, usually written quickly and only to demonstrate the vulnerability. In addition to being reliable, an exploit must also be efficient. In other words, the exploit writer should try to reduce the use of brute forcing as much as possible, especially when it might sound alarms on the targeted machine.
Exploits can target local or remote services:
A local exploit is an attack that requires the attacker to already have access to the target machine. The goal of a local exploit is to raise the attacker's privileges and give him or her complete control over the system.
A remote exploit is an attack that targets a machine the attacker has no access to, but that he or she can reach through the network. It is a more challenging (and, to some extent, more powerful) type of exploit. As you will discover throughout this book, gathering as much information about the target as possible is a mandatory first step toward a successful exploitation, and this task is much easier to perform if the attacker already has access to the machine. The goal of a remote exploit is to give the attacker access to the remote machine. Elevation of privileges may occur as a bonus if the targeted application is running with high privileges.
If you dissect a "generic" exploit, you can see that it has three main components:
Preparatory phase Information about the target is gathered and a favorable environment is set up.
Shellcode This is a sequence of machine-level instructions that, when executed, usually lead to an elevation of privileges and/or execution of a command (e.g., a new instance of the shell). As you can see in the code snippet on the next page, the sequence of machine instructions is encoded in its hex representation to be easily manipulated by the exploit code and stored in the targeted machine's memory.
Triggering phase The shellcode is placed inside the memory of the target process (e.g., via input feeding) and the vulnerability is triggered, redirecting the target program's execution flow onto the shellcode.
char kernel_stub = "\xbe\xe8\x03\x00\x00" // mov $0x3e8,%esi "x65\x48\x8b\x04\x25\x00\x00\x00\x00" // mov %gs:0x0,%rax "\x31\xc9" // xor %ecx, %ecx (15 "\x81\xf9\x2c\x01\x00\x00" // cmp $0x12c,%ecx "\x74\x1c" // je 400af0 <stub64bit+0x38> "\x8b\x10" // mov (%rax),%edx "\x39\xf2" // cmp %esi,%edx "\x75\x0e" // jne 400ae8 <stub64bit+0x30> "\x8b\x50\x04" // mov 0x4 (%rax),%edx "\x39\xf2" // cmp %esi,%edx "\x75\x07" // jne 400ae8 <stub64bit+0x30> "\x31\xd2" // xor %edx,%edx "\x89\x50\x04" // mov %edx, 0x4(%rax) "\xeb\x08" // jmp 400af0 <stub64bit+0x38> "\x48\x83\xc0\x04" // add $0x4,%rax "\xff\xc1" // inc %ecx "\xeb\xdc" // jmp 400acc <stub64bit+0x14> "\x0f\x01\xf8" // swapgs (54 "\x48\xc7\x44\x24\x20\x2b\x00\x00\x00" // movq $0x2b, 0x20(%rsp) "\x48\xc7\x44\x24\x18\x11\x11\x11\x11" // movq $0x11111111, 0x18(%rsp) "\x48\xc7\x44\x24\x10\x46\x02\x00\x00" // movq $0x246,0x10(%rsp) "\x48\xc7\x44\x24\x08\x23\x00\x00\x00" // movq $0x23, 0x8 (%rsp)/* 23 32-bit, 33 64-bit cs */ "\x48\xc7\x04\x24\x22\x22\x22\x22" // movq $0x22222222,(%rsp) "\x48\xcf"; // iretq
One of the goals of the attacker is to increase as much as possible the chances of successful execution flow redirection to the memory area where the shellcode is stored. One naïve (and inefficient) approach is to try all the possible memory addresses: every time the attacker hits an incorrect address the program crashes, and the attacker tries again with the following value; at some point he or she eventually triggers the shellcode. This approach is called brute forcing, and it is time- and usually resource-intensive (imagine having to do that from a remote machine). Also, it is generally inelegant. As we said, a good exploit writer will resort to brute forcing only when it is necessary to achieve maximum reliability, and will always try to reduce as much as possible the maximum number of tries he or she attempts to trigger the shellcode. A very common approach in this case is to increase the number of "good addresses" that the attacker can jump to by extending the shellcode with a sequence of no operation (NOP) or NOP-like instructions in front of it. If the attacker redirects the execution flow onto the address of one of those NOP instructions, the CPU will happily just execute them one after the other, all the way up to the shellcode.
All modern architectures provide a NOP instruction that does nothing. On x86 machines, the NOP instruction is represented by the 0x90 hexadecimal opcode (operation code). A NOPlike instruction is an instruction that, if executed multiple times before the shellcode, does not affect the shellcode's behavior. For example, say your shellcode clears a general-purpose register before using it. Any instruction whose only job is to modify this register can be executed as many times as you want before the shellcode without affecting the correct execution of the shellcode itself. If all the instructions are of the same size, as is the case on Reduced Instruction Set Computer (RISC) architectures, any instruction that does not affect the shellcode can be used as a NOP. Alternatively, if the instructions are of variable sizes, as is the case on Complex Instruction Set Computer (CISC) architectures, the instruction has to be the same size as the NOP instruction (which is usually the smallest possible size). NOP-like instructions can be useful for circumventing some security configurations (e.g., some intrusion detection systems or IDSs) that try to detect an exploit by performing pattern matching on the data that reaches the application that gets protected. It is easy to imagine that a sequence of standard NOPs would not pass such a check.
You might have noticed that we made a pretty big assumption in our discussion so far: when the victim application is re-executed, its state will be exactly the same as it was before the attack. Although an attacker can successfully predict the state of an application if he or she has a deep enough understanding of the specific subsystem being targeted, obviously this does not generally occur. A skilled exploit writer will always try to lead the application to a known state during the preparatory phase of the attack. A good example of this is evident in the exploitation of memory allocators. It is likely that some of the variables that determine the sequence and outcome of memory allocations inside an application will not be under the attacker's control. However, on many occasions an attacker can force an application to take a specific path that will lead to a specific request/set of requests. By executing this specific sequence of requests multiple times, an attacker gathers more and more information to predict the exact layout of the memory allocator once he or she moves to the triggering phase.
Now let's jump to the other side of the fence: Imagine that you want to make the life of an exploit writer extremely difficult, by writing some software that will prevent a vulnerable application from being exploited. You might want to implement the following countermeasures:
Make the areas where the attacker might store the shellcode nonexecutable. In the end, if these areas are supposed to contain data, there is no reason for the application to execute code from there.
Excerpted from A Guide to Kernel Exploitation by Enrico Perla Massimiliano Oldani Copyright © 2011 by Elsevier Inc.. Excerpted by permission of SYNGRESS. All rights reserved. No part of this excerpt may be reproduced or reprinted without permission in writing from the publisher.
Excerpts are provided by Dial-A-Book Inc. solely for the personal use of visitors to this web site.
Table of Contents
Part I: A Journey to Kernel-Land Chapter 1: From User-Land to Kernel-Land Attacks Chapter 2: A Taxonomy of Kernel Vulnerabilities Chapter 3: Stairway to Successful Kernel Exploitation Part II: The UNIX Family, Mac OS X, and Windows Chapter 4: The UNIX Family Chapter 5: Mac OS X Chapter 6: Windows Part III: Remote Kernel Exploitation Chapter 7: Facing the Challenges of Remote Kernel Exploitation Chapter 8: Putting It All Together: A Linux Case Study Part IV: Final Words Chapter 9: Kernel Evolution: Future Forms of Attack and Defense