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
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 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.
Get Network Security Hacks now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.