Section 6.8 Obscure but Deadly Problems

   


6.8 Obscure but Deadly Problems

These problems are obscure and many SysAdmins are not knowledgeable about them. I must confess that when I first encountered discussion about the chroot() problem on a cracker resource page, I did not believe it. Only later when I downloaded the code to take advantage of this vulnerability did I believe it.

6.8.1 Defeating Buffer Overflow Attacks

graphics/fourdangerlevel.gif

Many of the best-known successful attacks against well-configured Linux systems were done via a buffer overflow attack. A cracker will attack a program that has a bug that prevents it from properly limiting the amount of input data to the size of the allocated buffer. Either the cracker will know where to attack by studying the code or by taking educated guesses. In many cases, the code base is common to Linux and UNIX. In other cases, given that most programs are written in C and frequently there is one clear choice of algorithm, an educated guess is not hard.

Some programmers use the evil gets() routine that fails to limit the amount of data to the size of the buffer. Such a high percentage of buffer overflow attacks are done through gets() that normally the GNU C compiler will generate a warning if you compile a program that uses it. Typically, a C program will declare buffers to be automatic. This is the default storage class for variables declared inside functions. These will be allocated on top of the stack. Above the local storage for the currently executing function is the return address for the function that called the currently executing function.

All a cracker needs to do is put some bytes in the buffer that constitute machine instructions to "take over." This takeover could be creating a set-UID shell, adding the set-UID bit to another program, or using any of a number of similar breaches. He has already computed how many bytes there are between the end of the current buffer and the return address. He writes garbage to the intervening bytes and then writes the address of the start of the buffer in the memory word that contains the return address for the calling function. When the function attempts to return, it will "return" to the cracker's code and will do the cracker's bidding. If the program was running as root, the cracker "owns" the system.

For those that know a little C, the following snippet of code will illustrate this problem. It illustrates typical C coding style that does not defend itself against bad data and so it has a buffer overflow vulnerability; it is trivial for a cracker to exploit this vulnerability. This is representative of the buffer overflow vulnerabilities that represent about half the known successful intrusions of recent Linux systems. The routine yesno() reads a line of text from the program's standard input and stores it in a buffer. It then returns true if the line started with "y" or "Y" or false otherwise. The code might look like the following:

 
 yesno(void) {          char    buf[4];          gets(buf);      /* The vulnerability. */          return *buf == 'y' || *buf == 'Y'; } 

Figure 6.2 shows the stack as yesno() is about to call gets() to read in the cracker's buffer. All the intruder needs to do is to put some instructions into the buffer, followed by the address of the start of the buffer. For simplicity, a one-word (four-byte) buffer is shown. Even this could be used in many cases to cause the program to "return" to a location that would cause security to be defeated or to alter the caller's stored register values or even the caller's automatic variables. See also "Buffer Overflows or Stamping on Memory with gets()" on page 252.

Figure 6.2. Buffer overflow stack.

graphics/06fig02.gif

As you can see in Figure 6.2, the return address and the caller's stored register values immediately follow the "automatic" variables, that is, the dynamically allocated variables, of the current routine. This allows a cracker simply to supply additional bytes of data to overwrite the return address or other critical areas of the stack with just the right values to alter the program in a way that will grant the cracker root access. Most of the buffer overflow attacks involve the cracker writing his machine instructions into a buffer and then causing the program to do a subroutine "return" instruction to that location. This is because it is easy. Attacking a CGI program through Apache is especially easy because the "%" hex escape sequences make it easy to write arbitrary binary data to a buffer. There are a number of patches to the Linux kernel available that will cause the kernel to disallow a program running code from its stack space.

Any of these patches will prevent most of the buffer overflow exploits but certainly not all of them. Certainly, these patches increase security and their use is encouraged as long as the overhead imposed does not exceed the security value. When Mandrake is asked to build a secure kernel, it will install some "Solar Designer" security patches that include the nonexecutable stack patch just described.

Most common buffer overflow attacks can be repelled with Libsafe. Its capabilities, installation, and use are discussed in "Stopping Buffer Overflows with Libsafe" on page 331.


The best way to prevent buffer overflow problems and other security bugs is to use good programming practices and to have all code audited thoroughly by someone experienced in auditing code. Even in well-written code, I can usually spot at least one bug per page of source.

One client will be flying me to California to audit some recent code for their server. Even though there are thousands of units around the world with this server, operating for more than a year, there have been no reported bugs. No patches nor upgrades to the server have been required. This has been due to good programming practices (and some luck).


In the excellent book Linux Application Development, Johnson and Troan devote an entire chapter to Memory Debugging Tools. They discuss various ways to detect buffer overflows, memory leaks, and similar problems that can lead to security compromises.

6.8.2 Defeating the chroot() Vulnerability

graphics/twodangerlevel.gif

Normally, any program running on Linux has access to any file whose permissions allow it access. This is part of the "Monolithic" UNIX/Linux security model that some criticize. One "answer" that is used sometimes is the chroot() system call. Many people forget that chroot() was not intended for a general "file system prison" to drop programs into for security. Rather, it was intended to test new versions of a UNIX distribution and to prevent ordinary nonroot users from wandering where they do not belong.

The chroot() mechanism can add a substantial measure of safety for programs not running as root but only a small measure when running with root privileges, because root can break chroot() in at least two known ways. Additionally, root can cause plenty of damage without breaking out of prison. For example, root could open a socket in Promiscuous mode, monitor all data on your LAN, and then transmit interesting data to the intruder. Even if there are no device files in the chroot "prison," root easily could use the mknod() system call to create them.

Nevertheless, it can be useful. Programs such as Apache, named (DNS), ftpd, NFS, and Samba can make use of chroot for additional security. As you know, you will need to supply a complete set of needed files under the chroot tree. These include /etc/passwd and /etc/group (fake versions), possibly /dev/tty and /dev/null, and the often forgotten /lib/*.so* files. Clearly, you will want a very minimal set of files that do not include most /dev files such as /dev/mem, /dev/hda, and /dev/sda. For the /lib files a "first cut" would be

 
 mkdir /home/foo/lib (cd /lib;tar -caf - .) | (cd /home/foo/lib;tar -xpf -) /bin/rm -rf /home/foo/lib/modules 

The simplest way to break out of a chroot prison is to use mknod() to create the device for the root file system and you have full access to it, or use mknod() to create /dev/kmem or /dev/mem. An alternative way would be to use fchdir() and chroot() in a way that really should be considered a bug in each, but which I confirmed as being present in Linux.

"But we took your advice and took the C compiler off the system," you might say. Still, if a cracker can break a program running in the chroot "prison" to generate a file with the execute bit on, she can compile the program on her own system and upload the executable. Stopping these exploits requires kernel modification to block these system calls:

 
 fchdir() mknod() 

Although I might be wrong, I do not know of an overriding need for fchdir() and it certainly should be modified to not break out of a chrooted environment. Perhaps it is used by file tree walking code to avoid certain race conditions in the use of chdir(). This might be where someone also alters the directory structure between a program's lstat() and chdir() calls. Perhaps these programs could be modified to use chdir(). Although device files are created initially with mknod(), they should not be necessary in a running system. If you do need to create new devices at some point, you could boot an alternate unmodified kernel in single-user mode to do this. Thus, disabling the mknod() system call is a possibility.

A more elegant solution is to disable these system calls only to processes that have been chrooted, returning EPERM. In 2.0 and 2.2 kernels, the following code dropped into the kernel for these system calls, typically in the /usr/src/linux/fs directory, should do this.

 
 if (current->fs->root   && current->fs->root != real_root_dev)           return EPERM; 

Note that I have not tried this technique. Additionally, I suspect it might not work for RAMDISKs or with the umsdos file system due to their possibly altering the initial current->fs->root to not correspond to real_root_dev. A minor change to test against the alternate real root device probably would resolve this problem.


6.8.3 Symlink Attack

graphics/twodangerlevel.gif

Some privileged programs will create a file in publicly writable areas without ensuring that the file does not already exist. By privileged I mean that either the program has its set-UID or set-GID bit set or it is invoked by a privileged user such as root or bin or even the account used by the class's Teaching Assistant. Such a program typically uses the creat() system call to create a temporary file in the /tmp, /usr/tmp, /var/tmp, or /var/lock directories. These "public" directories are writable by all, because any user is offered these places for their own temporary files or lock files. (These directories have the sticky bit set via

 
 chmod +t /tmp /usr/tmp /var/tmp /var/lock 

or

 
 chmod 1777 /tmp /usr/tmp /var/tmp /var/lock 

when they each are first created, and this bit prevents one user from removing another user's file even though each directory is writable by all.)

When Ken Thompson, co-inventor of UNIX, was asked if he had it to do all over again what would he change about UNIX he said, "Rename creat() to create()."


If there already is a file by the name specified, the creat() system call will truncate the existing file to zero length. Otherwise, if the file does not already exist, it will create the file and it will start out being zero length. The problem is that if a rogue can predict that such a privileged program will create such a file, he can create a symbolic link (symlink) with that name pointing to a privileged area. When the buggy program then issues the creat() call, a file of the name that the rogue selected is created (if it does not already exist) or is truncated. Understand that even if the buggy program incorporates its Process ID (PID) in the file name, all a rogue has to do is write a program (or script) that obtains its own PID and then create symlinks incorporating each of the next 100 (or 10,000) PID numbers in it.

The possibilities for intrusions are vast. If the buggy program creates the file mode 666, the rogue can have the symlink point into the area where the cron daemon looks for crontabs and then, as it were, write his own ticket. Even if the file gets created mode 600, the rogue may be able to cause the program to generate an error message that has output of the rogue's bidding.

If, for example, the buggy program takes a file name and writes in the temporary file a line as harmless as

 
 /bin/ls -ld filename 

all the rogue has to do is specify a file name of

 
 foo\n* * * * * chown root /tmp/.mysu;chmod 4777 /tmp/.mysu\nbar 

(where the \n represents a newline) after copying /bin/sh or similar to /tmp/.mysu and create a symlink pointing to /var/spool/cron/crontabs/root. (Some SysAdmins will take read access away from all the binary programs in the /bin, /usr/bin, /sbin, /usr/sbin, and /etc directories to prevent this, but all a rogue needs to do is write a short C program and compile it or even compile and uuencode it on another system and e-mail it to the target system.) He could use it for as mundane a task as truncating a log file (or several in succession) to hide his tracks after he has done some dastardly deed but was not able to give himself unlimited root power.

Besides the symlink attacks already mentioned, there are lots of other attacks that are similar in nature. Generally, these attacks depend on a program not carefully ensuring that the file that it tries to create in an unprotected directory does not already exist. Attacks are made much easier by the program also creating files with predictable names, such as a known derivation of the program's Process ID number (PID).

So what is a SysAdmin to do? Certainly, use the latest stable versions of one's chosen Linux distribution and separately installed applications. For SysAdmins particularly concerned about security, it is suggested that you use grep to search through the source of all the programs that are considered privileged (set-UID, set-GID, or invoked by privileged users) for the creat() system call. The following should work when invoked as root from the top level directory from where the source tree starts or /.

 
 find . -name '*.[hcC]' -print | \   xargs -n 50 grep 'creat *(' /dev/null 

A few useful features of this command are that it also searches in *.h files which sometimes have code as well as some C++ files ending in .C. It finds this function even if the programmer put a space in front of the left parenthesis, and by asking grep to search at least two files (any found files and /dev/null), grep will oblige by telling the name of the file. The xargs program is used to read each word of its input and append it to the arguments provided and execute this as a program. Thus grep is invoked for every 50 files found instead of every one. This increases efficiency 4900 percent.

Some C++ programmers will put source in files ending in .cpp or .cxx. These files may be checked via

 
 find . ( -name '*.cpp' -o -name '*.cxx' ) -print \   | xargs -n 50 grep 'creat *(' /dev/null 

You then will need to analyze the code to determine whether the file is being created in a public area. Really, this is quite clear in most cases. Be aware that this problem might be found in library routines in libc and elsewhere, but I expect that library problems have been corrected by now.

The fix is to replace each creat() system call with a call to open() with the appropriate parameters. The difference is the ORing in of O_EXCL which prohibits the operation if the file already exists, and O_NOFOLLOW which prohibits it if it is a symlink. The "already exists" tests whether there is an entry of that name (/tmp/foo in this case) even if it is a symbolic link pointing to a nonexistent file, which is exactly what you want. The O_NOFOLLOW flag has been supported in the kernel since 2.1.126. Specifically, each

 
 fd = creat("/tmp/foo", 0600);   /* Broken! */ 

should be replaced with

 
 fd = open("/tmp/foo", O_CREAT|O_WRONLY|O_TRUNC|O_EXCL|O_NOFOLLOW, 0600); 

Certainly, creat() calls with different file names and permissions (0600) will need to be corrected too. As you know, you cannot simply precede the creat() call with an lstat() call that checks that the file (symlink) does not already exist because there always will be a window of time between the lstat() and creat() calls that a rogue could make use of.

You will want to ask any vendors that do not provide source code if their programs (and any libraries) are immune to this problem. I suggest getting the answer in writing. Even in early 2000 VMware still had this vulnerability.

How did this exploit come to pass? Why have programmers not been guarding against this all along?

In the early days of UNIX, there were no symlinks and hence this exploit did not exist nor was there any way to create ordinary files except via the creat() system call. Thus, the programming techniques and the stage were set for this exploit. (Symlinks were added to UNIX by those wild guys at Berkeley because an ordinary link [hard link] could not link across file systems. Those that attempted to got the dreaded errno 18 Cross-device link.)

Also, because symlinks, also known as soft links, do not exist in most other operating systems people who learned C on these other systems were not taught of this danger.


6.8.4 The lost+found=hole Problem

graphics/onedangerlevel.gif

Presently Red Hat, Mandrake, and Slackware all create the lost+found directories mode 755; some other distributions probably do too. Because the Linux ext2 file system is so robust it rarely is inconsistent (corrupt) following a crash. Even if fsck must correct a problem, usually it is a temporary pipe file or an inode in the process of being removed and fsck finishes the process.

On very rare occasions a directory file itself might get destroyed. The only link that a file normally has to the file system is its containing directory. This directory stores each file's inode (information node) number in the directory's data along with the last component of the file's name.

In this event, fsck will detect the directory's lost files because they will have a link count of 1 but their inode numbers do not appear in each directory. This situation is the sole reason for the lost+found directories. It is very much like a company's lost and found department because that is where fsck will put these files.

The problem is that a file may be allowed to have permissions indicating the world (or even those in the same group) can access it because it is in a directory that forbids access. Once such a file gets moved to the lost+found directory at the base of its file system, anyone can access it.

The solution is to issue the following command. It assumes that all mounted file systems are mounted directly on / or under /mnt:

 
 chmod 700 /lost+found* /*/lost+found /mnt/*/lost+found 

If individual users have lost files, you can move them back to the individual users' accounts so that they can access them.

6.8.5 The rm -r Race

graphics/twodangerlevel.gif

There is a race condition in the rm program when it is asked to do a recursive rm of a directory tree with -r. If an ordinary malicious user has write access to the directory tree, the user can add a symbolic link pointing to somewhere else, such as /, and rm will pick up at the new spot, for example, /. The race is for a entry, say, bar, in a directory tree that is being removed. The rm program will do an lstat("bar", buf) on it and if it is a directory, rm will do a chdir("bar"). If a malicious ordinary user can do the following between the lstat() and chdir() operations, that user wins the race and you will be spending the night restoring from backup.

 
 mv bar haha ln -s / bar 

This likely affects chmod, chown, and chgrp too when the -R or --recursive flags are used. This author has inspected the source for the glibc ftw() (file tree walk) function and confirms that it is a problem for any program that uses ftw(). The window is very small and so it may take a thousand attempts for a malicious user to win the race, but during the removal of a large tree there might be a hundred chances for him. If your system frequently does this operation, however, your risk will be higher. If an ordinary user can initiate such an operation by root on demand, eventually he will win.

The fix is to first use chmod and possibly chown on the top-level directory to be operated on so that only root has access. Then issue the desired command with -r and set the permissions back, if applicable. To do a reasonably safe recursive remove on /tmp/foo, issue the following commands:

 
 chown root /tmp/foo; chmod 700 /tmp/foo rm -rf /tmp/foo 

For absolute safety, do

 
 find /tmp/foo -type d -exec fuser '{}' \; 

after the chmod in case a rogue program has already done a chdir into this directory.


   
Top


Real World Linux Security Prentice Hall Ptr Open Source Technology Series
Real World Linux Security Prentice Hall Ptr Open Source Technology Series
ISBN: N/A
EAN: N/A
Year: 2002
Pages: 260

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