![]() |
|
|||||||||||||||
|
||||||||||||||||
|
| Make your software behave: An anatomy of attack code | ||||
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
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 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
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:
Next, we compile this program with debugging on:
Then we run it using gdb, the GNU debugger, using the command:
Now we can look at the code in assembly format and tell how many bytes each instruction maps to, using the following command:
This gives us something like this:
We can get each byte of this function in hexadecimal one at a time by using the
The utility will show you the value of the first byte in hexadecimal. For example:
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 The two challenges Getting around the first challenge is not hard. We can statically link 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:
The
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:
Linux on Intel machines, as an ASCII string:
SPARC Solaris, in assembly:
SPARC Solaris, as an ASCII string:
SPARC SunOS, in assembly:
SPARC SunOS, as an ASCII string:
Voila -- instant exploit code! How to craft an 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 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
Related articles in the "Make your software behave" column on developerWorks:
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| About IBM | Privacy | Terms of use | Contact |