ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


Securing Systems with chroot

by Emmanuel Dreyfus
01/30/2003

Buffer Overflows

One popular technique crackers use to compromise machines is exploiting buffer overflows. Buffer overflows are programming bugs which often plague software written with the C language, which makes such mistakes easy to make. Here is an example:
int getfilename(void)
{
   /* 
   * dos filenames are 8 chars + one dot + 3 chars + trailing nul char
   * => total 13 chars
   */
   char tmp[13]; 

   (void)scanf("%s", tmp);
   /* more code, more bugs... */
   return 0;
}

So what happens? The programmer has expected the user to supply a DOS filename, which should fit in a 13 character buffer. Unfortunately, he has failed to validate user input. If the user types a filename which is 25 bytes long, scanf() will copy all the 25 bytes in the buffer, which is only 13 bytes long. The extraneous data will overwrite memory locations near the buffer.

Local variables such as the buffer used in this example are allocated on a memory area called the stack. The processor maintains the stack, using a register known as the stack pointer to keep track of the memory location of the top of the stack. The stack pointer moves with every function call, and some or all of the following data is pushed on the stack: the current contents of the CPU's registers, the arguments to the function, and a placeholder for the function's return value. Additionally, a placeholder for local variables is created.

On a function return, the stack pointer moves down, and the CPU registers are restored from the values stored on stack. Thus the calling function gets its context restored and is not disturbed by the actions of the called function did.

What is overwritten when our buffer overflows? First, other local variables, the return value of the function, the function's arguments...then, the saved CPU registers, including the address to which the CPU should return when it completes the called function. A buffer overflow corrupts this value, and the function will return somewhere else in memory. The odds are against finding valid code there. The program will terminate on an illegal instruction, an address error, or perhaps a segmentation fault.

Related Reading

Practical UNIX and Internet Security
By Simson Garfinkel, Gene Spafford, Alan Schwartz

Exploiting Buffer Overflows

If the user happens to be a clever cracker, he could enter data which is valid machine code, overwriting the return address with the address of his code. We end up with the situation where the user has successfully tricked our program into executing his code.

This is a real problem if the program is running under a privileged user ID, such as the system superuser (root). An attacker could execute his code with the privileges of a superuser. The next step for our cracker is to include an exec() system call and execute a command such as /bin/sh with superuser privileges to gain full control of the machine.

There are various places in the system where such a situation can arise: setuid binaries (su, passwd, pppd), privileged programs which parse user files (sendmail, cron), and network daemons which answer user requests (sshd, named, ntpd, and so on). Problems in network daemons are critical since they mean that a remote user can compromise the system over the Internet.

Enhancing security by using chroot jails

Securing programs which take some user input is not easy. The errors are not always as obvious as a buffer overflow. Additionally, since the complexity of network services is always increasing, many programs use third party libraries which provide services such as compression or cryptography. Security problems in those libraries can compromise the security of any program that uses them, something that developers cannot necessarily anticipate.

One solution is to admit that network daemons are inherently insecure. Therefore, the best option is to reduce the consequences of a security exploitation.

The first measure is to run the service as a non-privileged user. That way, an intruder who breaks into the machine will not have full control. Daemons such as innd (Internet Network News Daemon) and ircd (Internet Relay Chat Daemon) use this behavior by default.

This is a good first measure to tighten system security, but it is not perfect. Once the intruder has a shell access with the privileges of the compromised daemon, even though it is unprivileged, he can still affect the compromised daemon and look for local security flaws, such as might exist in other setuid binaries.

An additional measure is to chroot the daemon. Chrooting is a verb named after the chroot(2) system call, which is used to change the root of the filesystem as seen by the calling process.

When a process requests to chroot to a given directory, any future system calls issued by the process will see that directory as the filesystem root. It becomes impossible to access files and binaries outside the tree rooted on the new root directory. This environment is known as a chroot jail.

It is possible to experiment with chrooting easily. Just create a tree with a few binaries, including root's shell as listed in /etc/passwd:

/tmp/bin/sh
/tmp/bin/ls

If these binaries are dynamically linked on your system (this is the case on most Linux distributions), make sure you include the dynamic linker and the required libraries (typically /lib/ld.so.1 and /lib/libc.so.1).

You can then try the chroot(1) command, which just performs the chroot(2) system call and runs a shell. Only root can use this command:

# ls /
bin     etc     dev     home    sbin    tmp     usr     var
# chroot /tmp
# ls /
bin
# ls /bin
sh      ls

Once you are in the chrooted shell, you only have access to the chrooted area. There is no way to escape it; you are in the jail. Other processes still see the normal filesystem root as the root of the filesystem.

However, there are some ways of getting out of a chroot jail if the process is still running with superuser privileges. It is possible to do a mount(2) system call in order to remount the real root filesystem and then to chroot in it. It would also be possible to create a /dev/kmem device using the mknod(2) system call, and then modify the filesystem root in kernel memory. If the chrooted process runs with superuser privileges, there are many ways of breaking out of the chroot jail. This is why, when chrooting a process, the user ID should always be changed to a non-privileged user.

The only way left for an unprivileged process to get out of a chroot jail would be to exploit a kernel security hole. If there is some buffer overflow on a system call argument, it could be possible to execute some code with kernel privileges and to set a new filesystem root for the process. Great care is taken when validating system call arguments, so this should not happen. This kind of security hole has been encountered in the past, but there has been much less problem with system call argument validation than with buffer overflows in network daemons and setuid binaries. For an example of a real system call buffer overflow, take a look at this sobering paper.

Summary

We have seen one common security flaw and have learned how it may be exploited. We have also discussed one approach to minimizing this damage. chroot is not perfect, but, with care, it can lead to more secure systems. Next time we will demonstrate how to run a daemon (ntpd) chrooted on NetBSD.

Editor's Note: an earlier version of this article had reversed the arguments to scanf(). This has been corrected.

Emmanuel Dreyfus is a system and network administrator in Paris, France, and is currently a developer for NetBSD.


Return to the BSD DevCenter.

Copyright © 2009 O'Reilly Media, Inc.