Mitigate system damage by keeping service compromises contained.
Sometimes keeping up with the latest patches just isn't enough to prevent a break-in. Often, a new exploit will circulate in private circles long before an official advisory is issued, during which time your servers may be open to unexpected attack. With this in mind, it's wise to take extra preventative measures to contain the aftermath of a compromised service. One way to do this is to run your services in sandbox environments. Ideally, this lets the service be compromised while minimizing the effects on the overall system.
Most Unix and Unix-like systems include some sort of system call or other mechanism for sandboxing that offers various levels of isolation between the host and the sandbox. The least restrictive and easiest to set up is a chroot() environment, which is available on nearly all Unix and Unix-like systems. In addition to chroot(), FreeBSD includes another mechanism called jail( ), which provides a few more restrictions beyond those provided by chroot().
chroot() very simply changes the root directory of a process and all of its children. While this is a powerful feature, there are many caveats to using it. Most importantly, there should be no way for anything running within the sandbox to change its effective UID (EUID) to 0, which is root's UID. Naturally, this implies that you don't want to run anything as root within the jail. If an attacker is able to gain root privileges within the sandbox, then all bets are off. While the attacker will not be able to directly break out of the sandbox environment, it does not prevent him from running functions inside the exploited processes' address space that will let him break out. There are many ways to break out of a chroot( ) sandbox. However, they all rely on being able to get root privileges within the sandboxed environment. The Achilles heel of chroot() is possession of UID 0 inside the sandbox.
There are a few services that support chroot() environments by calling the function within the program itself, but many services do not. To run these services inside a sandboxed environment using chroot(), we need to make use of the chroot command. The chroot command simply calls chroot() with the first command-line argument and attempts to execute the program specified in the second argument. If the program is a statically linked binary, all you have to do is copy the program to somewhere within the sandboxed environment; but if the program is dynamically linked, you will need to copy all of its supporting libraries to the environment as well.
See how this works by setting up bash in a chroot() environment. First we'll try to run chroot without copying any of the libraries bash needs:
# mkdir -p /chroot_test/bin # cp /bin/bash /chroot_test/bin/ # chroot /chroot_test /bin/bash chroot: /bin/bash: No such file or directory
Now we'll find out what libraries bash needs, which you can do with the ldd command, and attempt to run chroot again:
# ldd /bin/bash libtermcap.so.2 => /lib/libtermcap.so.2 (0x4001a000) libdl.so.2 => /lib/libdl.so.2 (0x4001e000) libc.so.6 => /lib/tls/libc.so.6 (0x42000000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) # mkdir -p chroot_test/lib/tls && \ > (cd /lib; \ > cp libtermcap.so.2 libdl.so.2 ld-linux.so.2 /chroot_test/lib; \ > cd tls; cp libc.so.6 /chroot_test/lib/tls) # chroot /chroot_test /bin/bash bash-2.05b# bash-2.05b# echo /* /bin /lib
Setting up a chroot environment mostly involves trial and error in getting permissions right and all of the library dependencies in order. Be sure to consider the implications of having other programs such as mknod or mount available in the chroot environment. If these were available, the attacker could possibly create device nodes to access memory directly or to remount filesystems, thus breaking out of the sandbox and gaining total control of the overall system. This threat can be mitigated by putting the directory on a filesystem mounted with options that prohibit the use of device files (as in [Hack #1] ), but that isn't always convenient. It is advisable to make as many of the files and directories in the chrooted directory as possible owned by root and writable only by root, in order to make it impossible for a process to modify any supporting files (this includes files such as libraries and configuration files). In general it is best to keep permissions as restrictive as possible, and to relax them only when necessary (for example, if the permissions prevent the daemon from working properly).
The best candidates for a chroot() environment are services that do not need root privileges at all. For instance, MySQL listens for remote connections on port 3306 by default. Since this port is above 1024, mysqld can be started without root privileges and therefore doesn't pose the risk of being used to gain root access. Other daemons that need root privileges can include an option to drop these privileges after completing all the operations for which it needs root access (e.g., binding to a port below 1024), but care should be taken to ensure that the program drops its privileges correctly. If a program uses seteuid() rather than setuid() to drop its privileges, it is still possible to gain root access when exploited by an attacker. Be sure to read up on current security advisories for programs that will run only with root privileges.
You might think that simply not putting compilers, a shell, or utilities such as mknod in the sandbox environment may protect them in the event of a root compromise within the restricted environment. In reality, attackers can accomplish the same functionality by changing their code from calling system("/bin/sh") to calling any other C library function or system call that they desire. If you can mount the filesystem that the chrooted program runs from using the read-only flag [Hack #1], you can make it more difficult for attackers to install their own code, but this is still not quite bulletproof. Unless the daemon you need to run within the environment can meet the criteria discussed earlier, you might want to look into using a more powerful sandboxing mechanism.
One such mechanism is available under FreeBSD and is implemented through the jail() system call. jail() provides many more restrictions in isolating the sandbox environment from the host system and provides additional features, such as assigning IP addresses from virtual interfaces on the host system. Using this functionality, you can create a full virtual server or just run a single service inside the sandboxed environment.
Just as with chroot(), the system provides a jail command that uses the jail( ) system call. The basic form of the jail command is:
jail new root hostname ipaddr command
where ipaddr is the IP address of the machine on which the jail is running. Try it out by running a shell inside a jail:
# mkdir -p /jail_test/bin # cp /bin/sh /jail_test/sh # jail /jail_test jail_test 192.168.0.40 /bin/sh # echo /* /bin
This time, no libraries needed to be copied, because FreeBSD's /bin/sh is statically linked.
On the opposite side of the spectrum, we can build a jail that can function as a nearly full-function virtual server with its own IP address. The steps to do this basically involve building FreeBSD from source and specifying the jail directory as the install destination.
You can do this by running the following commands:
# mkdir /jail_test # cd /usr/src # make world DESTDIR=/jail_test # cd /etc && make distribution DESTDIR=/jail_test -DNO_MAKEDEV_RUN # cd /jail_test/dev && sh MAKEDEV jail # cd /jail_test && ln -s dev/null kernel
However, if you're planning to run just one service from within the jail, this is definitely overkill. Note that in the real world you'll most likely need to create /dev/null and /dev/log device nodes in your sandbox environment for most daemons to work correctly.