Hack #10. Restrict Services with Sandboxed Environments

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 might be open to unexpected attack. With this in mind, it’s wise to take extra preventative measures to contain the possible effects of a compromised service. One way to do this is to run your services in a sandbox. Ideally, this minimizes the effects of a service compromise 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. FreeBSD also includes another mechanism called jail() , which provides some additional restrictions beyond those provided by chroot().

Tip

If you want to set up a restricted environment but don’t feel that you need the level of security provided by a system-call-based sandboxed environment, see “Restrict Shell Environments” [Hack #20].

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 user ID (EUID) to 0, which is root’s UID. Naturally, this implies that you don’t want to run anything as root within the jail.

There are many ways to break out of a chroot() sandbox, but they all rely on being able to get root privileges within the sandboxed environment. Possession of UID 0 inside the sandbox is the Achilles heel of chroot(). If an attacker is able to gain root privileges within the sandbox, all bets are off. While the attacker will not be able to directly break out of the sandboxed environment, he may be able to run functions inside the exploited processes’ address space that will let him break out.

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(), you 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; however, 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 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 find out what libraries bash needs by using the ldd command. Then copy the libraries into your chroot() environment 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 the permissions right and getting all of the library dependencies in place. Be sure to consider the implications of having other programs such as mknod or mount available in the chroot() environment. If these are available, the attacker may be able to 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 [Hack #1], but that isn’t always convenient. It is advisable to make as many of the files and directories in the chroot()-ed 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 they need root access (e.g., binding to a port below 1024), but care should be taken to ensure that the programs drop their privileges correctly. If a program uses seteuid() rather than setuid() to drop its privileges, an attacker can still exploit it to gain root access. 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 might 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 the chroot()-ed 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 offers 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. Here’s the basic form of the jail command, where ipaddr is the IP address of the machine on which the jail is running:

jail new root hostname ipaddr command
            

The hostname can be different from the main system’s hostname, and the IP address can be any IP address that the system is configured to respond to. You can actually give the appearance that all of the services in the jail are running on a separate system by using a different hostname and configuring and using an additional IP address.

Now, try running a shell inside a jail:

# mkdir -p /jail_test/bin
# cp /stand/sh /jail_test/bin/sh
# jail /jail_test jail_test 192.168.0.40 /bin/sh
# echo /*
/bin

This time, no libraries need to be copied, because the binaries in /stand are statically linked.

On the opposite side of the spectrum, you can build a jail that can function as a nearly fully functional 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
# mount_devfs devfs /jail_test/dev
# 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 probably need to create /dev/null and /dev/log device nodes in your sandbox environment for most daemons to work correctly.)

To start your jails automatically at boot, you can modify /etc/rc.conf, which provides several variables for controlling a given jail’s configuration:

jail_enable="YES"
jail_list=" test"
ifconfig_lnc0_alias0="inet 192.168.0.41 netmask 255.255.255.255"
jail_test_rootdir="/jail_test"
jail_test_hostname="jail_test"
jail_test_ip="192.168.0.41"
jail_test_exec_start="/bin/sh /etc/rc"
jail_test_exec_stop="/bin/sh /etc/rc.shutdown"
jail_test_devfs_enable="YES"
jail_test_fdescfs_enable="NO"
jail_test_procfs_enable="NO"
jail_test_mount_enable="NO"
jail_test_devfs_ruleset="devfsrules_jail"

Setting jail_enable to YES will cause /etc/rc.d/jail start to execute at startup. This in turn reads the rest of the jail_X variables from rc.conf, by iterating over the values for jail_list (multiple jails can be listed, separated by spaces) and looking for their corresponding sets of variables. These variables are used for configuring each individual jail’s root directory, hostname, IP address, startup and shutdown scripts, and what types of special filesystems will be mounted within the jail.

For the jail to be accessible from the network, you’ll also need to configure a network interface with the jail’s IP address. In the previous example, this is done with the ifconfig_lnc0_alias0 variable. For setting IP aliases on an interface to use with a jail, this takes the form of:

ifconfig_<iface>_alias<alias number>="inet <address> netmask 255.255.255.255"

So, if you wanted to create a jail with the address 192.168.0.42 and use the same interface as above, you’d put something like this in your rc.conf:

ifconfig_lnc0_alias1="inet 192.168.0.42 netmask 255.255.255.255"

One thing that’s not entirely obvious is that you’re not limited to using a different IP address for each jail. You can specify multiple jails with the same IP address, as long as you’re not running services within them that listen on the same port.

By now you’ve seen how powerful jails can be. Whether you want to create virtual servers that can function as entire FreeBSD systems within a jail or just to compartmentalize critical services, they can offer another layer of security in protecting your systems from intruders.