2001-08-20
|
Protecting Systems with Libsafe
last updated August 20, 2001 |
|
As a systems administrator, one is constantly bombarded with news of new vulnerabilities and exploits. It can often be difficult to find time to follow up all these stories and act on them immediately. Amongst all these vulnerabilities, the infamous "Buffer Overflow" seems to crop up time and time again. Earlier this year, Avaya Labs released version 2.0 of a tool called "Libsafe" (freely available under the LGPL) which can be used to protect systems against some of the more common buffer overflow attacks. This article will seek to explain how Libsafe works, and briefly compare it to some other popular methods currently in use. This article will try to assume no more knowledge than is necessary for the usage of Libsafe (which is fairly minimal), but a basic understanding of C will make the examples easier to understand. Let us then first start with a quick overview of the problem. Buffer Overflows A buffer overflow is a vulnerability in a program which is usually due to a lack of bounds checking. Consider the following C source code: #include The function vuln_func() allocates space for a string of 5 characters, and then proceeds to read characters in from the user. It then prints out the string. What happens if we enter too many characters ? If we enter 10 characters, then the program exits with a "segmentation fault". A Segmentation Fault arises when a program tries to read or write to memory locations for which it does not have the necessary permissions. At first glance one might just assume that since we have only been allocated a certain amount of memory for our needs, entering a string which is too long leads to an attempt to overwrite memory which does not belong to us. Indeed, if we enter enough characters, this will be the case. So far all we appear to have is a possibility for a DoS (Denial of Service) attack by killing the program by sending it too much data. However, if one looks a little closer, a more subtle attack reveals itself; to see this, we must consider how the memory allocated to a process is arranged: When a function is called, the arguments to the function, and two pieces of data called the "Previous Frame Pointer" and the "Return Address" are stored in what is called the "Stack". For our purposes, we just need know that the stack is just a contiguous region of memory. At the bottom of the stack we have the parameters passed to the function (none in our example). Next we have the Return Address, this points to where control must return after the function is complete. Next is the previous frame pointer (which need not concern us). After this, at the top of the stack, we have space for the variables which are used within the function (string in our case). There are two key points which must be understood 1) When the function is complete (i.e. when return() is called), control will return to wherever the return address points - this should be to whichever instruction follows the original function call. 2) Data stored in the area at the top of the stack will grow downwards. This is where the exploit takes place: if we enter enough data into our program, we will overflow the area set aside for data and overwrite the Return Address (and the previous frame pointer). If we are careful in what we enter, we can set this return address to be whatever we want - instead of control returning to the next instruction, program execution will be diverted to a location of our choosing ! In many exploits, the return address is made to point to a location containing what is called "Shell Code" - the code necessary to spawn a command shell (e.g /bin/sh), allowing us to then execute arbitrary commands. We won't go into the details of how one writes and places the shell code, suffice it to say that it can be done. See http://www.phrack.org/show.php?p=49&a=14 for a more detailed explanation of how buffer overflow exploits are constructed. The main thing to bear in mind is that the attack is usually only made possible because no checks were made on the data in question - in our example, we allocated enough space for a string of length 5, but we did not check that only 5 characters were written. Clearly this attack will be most effective when carried out against a program that is running as root, since then we will be able to execute arbitrary commands as the superuser. Even if the program is running as an unprivileged user (as many servers do), a remote user will still be able gain a foothold on the system as a local user, from which it may only be a short step to gaining root access (possibly through further buffer overflows on local programs). One only has to glance at Bugtraq to see that many of the exploits posted are buffer overflows. Not only this, but many of the most famous vulnerabilities discovered have been buffer overflows all the way from Internet Worm of 1988 (buffer overflow in fingerd) to the Code Red worm of 2001 (buffer overflow in Microsoft IIS). Enter Libsafe So, how does one avoid such problems? No matter how careful you are writing your own programs, you can never be sure that everyone else sticks to the same careful practices as you. This is where Libsafe comes in handy. Libsafe (available at http://www.research.avayalabs.com/project/libsafe) is a library that intercepts calls to vulnerable functions in the standard C library at runtime, replacing these functions with safer ones that do not allow buffer overflows. For example, in our program, we have the unsafe call: gets(string); When our program is run, Libsafe will intercept the call to the gets() function in C library with its own version of gets(). This new version of gets() will compute the length of our buffer (string), say we call this max_size, and will then call fgets(string, max_size, stdin); from the standard C library. fgets() is much safer than gets() because it will only allow the stated number of characters to be read into the buffer. It should be noted that the manual page for gets(3) specifically warns against ever using gets(), and that gcc warns the user about the dangers of gets() when compiling, so there really isn't any excuse for this kind of mistake. The functions that Libsafe intercepts are: strcpy, strcat, getwd, gets, [vf]scanf, realpath, vsprintf. This may seem to be a fairly limited set of functions, but it does provide fairly good coverage of what one might consider to be the "most dangerous" functions. Installing and using Libsafe Before starting, ensure that your shared loader is version 1.8.5 or higher (e.g. /lib/ld.so.1.9.9). Installing Libsafe is very straightforward - grab the tarball, uncompress it and then $ make should build it, and if all goes well (no errors), we can then place the library in /lib with $ make install (This last step will need to be done as root.) Now that Libsafe has been built and installed, we need to ensure that it intercepts all function calls to the standard C library. We can do this in two ways. 1) We can set the environmental variable LD_PRELOAD e.g. (in bash):
$ LD_PRELOAD=/lib/libsafe.so.2
$ export LD_PRELOAD
To set this on a system-wide basis, just add this to e.g. /etc/profile (or maybe /etc/profile.local) 2) Alternatively, we can add a line to /etc/ld.so.preload
echo '/lib/libsafe.so.2' >> /etc/ld.so.preload
This will ensure that Libsafe is used for all programs, and cannot be disabled by a normal user (unlike environmental variables). When you first install Libsafe, its advisable to use the first method, since if Libsafe causes problems, one can easily unset LD_PRELOAD to stop Libsafe being used. If an attempted buffer overflow occurs, the offending program will be terminated, and a line added to /var/log/secure (depending on how you have syslog configured). Note that this termination+warning will not occur in every case - only when an actual attempt to write over the boundary is made. In our example, no warning or termination will occur since control is just passed to fgets() which will only read the allowed number of characters, so an overflow is not possible. Because Libsafe terminates processes, you may wish to ensure that these processes respawn, since for example, you wouldn't want a single attempted buffer overflow to permanently take down a web server. For processes launched from inetd this will be taken care of automatically. The first (publicly available) version of Libsafe (version 1.3) had an e-mail notification feature which would send an e-mail every time Libsafe caught a potential buffer overflow. There were some objections to this, and to the fact that this facility relied on calling /bin/mail. In version 2.0 this facility has been improved; Libsafe will now try to connect directly to your mail server on port 25 (so make sure something is there listening!) and send an e-mail to the addresses listed in /etc/libsafe.notify (or to root@localhost if this doesn't exist). This e-mail facility is not included by default - if you want this included then you'll need to add -DNOTIFY_WITH_EMAIL to the CCFLAGS line in src/Makefile. Personally, I would advise against this - just make sure that you look through your logs on a regular basis - this is generally a good idea anyway, and once you get used to what you're looking for it doesn't take too long. At this point it would be a good idea to try the sample exploits provided in the exploits/ directory with and without Libsafe to see if all is working as it should (check your logs). I had to tweak the int.sh script a little to make it work on my system, but this is fairly trivial. Problems with Libsafe At this point the reader will no doubt be wondering why Libsafe isn't included by default with all Linux distributions; unfortunately, Libsafe doesn't always work, and worse still, can even cause extra problems.
Because of these and other problems, before putting Libsafe into use system-wide, its probably best to ensure that all your programs run under it first. However, you should be aware that setuid programs will ignore LD_PRELOAD as a safety precaution (otherwise someone could replace standard functions with their own arbitrary functions). Other Solutions to the Buffer Overflow Problem Often, a buffer overflow exploit will place the shellcode on the stack itself. An obvious way to stop such exploits would be to make the stack non-executable; and this is one of the things that the Openwall patch does. The problem with this is the fact that although placing shellcode on the stack is one way to exploit a buffer overflow, it is not the only way - for example one can just jump to a suitable location in the standard C library. A non-executable stack can also lead to other problems, although the Openwall patch does provide work-arounds for these. Another solution is provided by StackGuard - a modified version of the gcc compiler. StackGuard inserts items of data called "canaries" into the stack before a function is called and then checks the integrity of these canaries after at the end of the function call - if they have been overwritten, then StackGuard terminates the process. However, the problem with StackGuard is that it requires all applications which you want protected to be compiled using StackGuard. Clearly this requires access to the source code for the program, which isn't always possible. At this point, you might suggest using both Libsafe and StackGuard, but this has its own problems due to what StackGuard does to the stack. Clearly Libsafe is easier to put in place than either of these two solutions. And according to Libsafe's authors, Libsafe has less impact on performance than StackGuard; however, you may wish to conduct your own benchmarks if you are curious. In fairness, one might well expect Libsafe to be faster since what it does is more limited than StackGuard. Format Vulnerabilities Another vulnerability that seems to have received a lot of coverage during the previous year has been the "format bug". This bug usually comes about from mistakes being made with the format string in the printf() family of functions. For example, consider the following code: #include The mistake lies in the fact that string has been passed directly to printf(), without a separate format string being provided. This means that any "%" symbols in string will be interpreted as format specifiers. So, for example, if a user enters %x as the input, then the program will just print out some data from the stack. Worse still, by entering a %n, the user can write to the stack! By careful manipulation of the input, it is possible to overwrite the return address of the current function, leading to execution of arbitrary code in a fashion similar to that of the traditional buffer overflow. An area that seems to be particularly prone to format bugs are the internationalization features which are now popular with many programs (these allow date formats, currency symbols, and the language of error messages to be altered according to locale). One of the new features in Libsafe 2.0 is the prevention of such bugs. Here, once again Libsafe intercepts the standard C library function and then performs some checks to ensure that there is no attempt being made to overwrite the stack. If such an attempt is detected, then once again, Libsafe will terminate the process and make a log entry. Conclusion Libsafe is a very useful tool - its transparency and ease of use are key advantages. That said, one should also consider the alternative solutions available depending on the particular situation at hand. It should be borne in mind that the only perfect solution is to remove the bugs in the original code itself. Without doing this, the possibility for buffer overflows will always be present. However, tools such as Libsafe are useful as a safety net to catch those undiscovered bugs which might be present, as well as giving administrators some breathing time to allow them to patch their systems once a bug has been found. One should also avoid being excessively concerned with buffer overflows - although such bugs may appear to be common, they are often in local programs which do not have any special privileges. |
