Name Server Security with BIND and chroot()

Hal Pomeranz, Deer Run Associates

 

Too Many BIND Bugs

In the last several months several substantial security problems have been revealed in recent releases of the BIND software—the de facto standard DNS implementation for the Internet.  In fact, it seems that BIND is entering a period that Sendmail went through in the mid ‘90s, where intense scrutiny from both the “black hats” and the “white hats” leads to a situation which feels very much like owning a subscription to the BIND “Bug of the Month Club”.  Flushing out security and stability problems is a good thing in some respects—“that which does not kill us only makes us stronger”, after all—but in the meantime, administrators need to take extra precautions in order to weather this period and keep their name servers secure.

 

In a previous article, Glenn Graham discussed some of the recent problems discovered in BIND and suggested running BIND as an unprivileged user (rather than the default posture of running the name server daemon as root which many sites still practice) in order to reduce the impact of future security issues.  This is certainly an excellent idea—as is making sure you are running the latest release of BIND (with all important security fixes) from the ISC.  However, BIND v8 also allows the administrator to force the name server daemon to run in a chroot()ed environment (similar to anonymous FTP servers and TFTP daemons on most systems).  This means that the daemon is effectively “locked up” in a captive piece of the file system—even if the attacker were able to exploit a buffer overflow in a future release of BIND, they would be severely limited in the amount of damage that they could do to the remote system.  The buffer overflow might be used to crash your running name server daemon (denial-of-service), but it’s unlikely that the attacker will actually be able to compromise the machine.

Preparing to chroot()

The first thing to do is to follow the guidelines in Glenn’s article to prepare an unprivileged account which the name server daemon, named, will run as.  Many Open Source Unix variants come pre-configured with a named or dns account (and matching group) that can be used for this purpose.  On other systems, choose any unused user ID (never use UID zero!) and group ID for the account and make the appropriate entries in /etc/passwd and /etc/group.  Note that the password entry for this user should have a blocked password field (a string like “*NP*” or “*LOCKED*”) and an invalid login shell such as /dev/null in order to prevent attackers from using this account as a “back door” into your system.  For purposes of our examples in this article, we will assume the administrator has created a “named” user and group.

 

The next item of business is to choose a directory that the named process will chroot() into.  This can really be any directory in the file system, though /var/named is a common choice and will be the directory we will use for examples in this article.  As we will see in the next section, this directory will need to contain a “mini operating system” in order for named to function properly in its chroot()ed environment.

Configuring the chroot() Directory

When configuring our chroot() environment, we want to make sure that most of the directory structure is unwritable by the named process—generally by making as much of the directory structure as possible owned by the root user and using much more restrictive access controls than the standard OS directories.  This means that even if an attacker were able to exploit a security flaw in the running named process, they would be unable to do much damage in the chroot() environment that the process is running in.

 

Creating a chroot() environment for any process usually breaks down into five basic steps:

 

Step 1.  Create the Top-Level Directory

The first step is to actually create the directory that the named process is going to chroot() into:

 

mkdir /var/named

chown root:daemon /var/named

chmod 511 /var/named

 

The above permissions are fairly restrictive, but they not only protect the directory from the running named process, they also prevent other unprivileged users on the system from easily navigating in the /var/named directory (which contains sensitive information like zone files).

 

The named binary will also need a directory that it can use as its default working directory inside of the chroot() area.  This directory—we’ll be using /var/named/var/run in our examples—should be writable by the named user:

 

cd /var/named

mkdir –p var/run

chown root:daemon var

chmod 111 var

chown named:named var/run

chmod 700 var/run

 

You will also need a directory for holding the zone files used by the name server—both the local zones for which you are authoritative, as well as any zones that you are acting as a slave server for.  The directory where the cache files for the slave zones will end up must be writable by the named user:

 

cd /var/named

mkdir –p zones/master

chown –R root:daemon zones

chmod –R 111 zones

mkdir zones/slave

chown named:named zones/slave

chmod 700 zones/slave

 

Step 2.  Copy Required Binaries

The named process requires a copy of the named-xfer program in the chroot() directory.  named-xfer is the program called by named to perform a zone transfer when your name server is acting as a slave server for some other domain.  On many Unix systems, named-xfer is found in /usr/libexec, though Solaris puts this program in /usr/sbin.  You will need to locate the named-xfer binary on your system and copy it into the equivalent directory in the chroot() environment:

 

cd /var/named

mkdir –p usr/libexec

cp /usr/libexec/named-xfer usr/libexec

chown –R root:daemon usr

chmod –R 111 usr

 

Step 3.  Copy Shared Libraries

Most Unix systems these days use shared libraries—this means that in addition to the named-xfer binary itself, you need to copy all of the shared libraries that are required by that binary into the chroot() environment.  The ldd command will tell you which files are needed.  For example, here is the output of the ldd command on a Solaris machine:

 

% ldd /usr/sbin/named-xfer

      libresolv.so.2 =>        /usr/lib/libresolv.so.2

      libsocket.so.1 =>        /usr/lib/libsocket.so.1

      libnsl.so.1 =>           /usr/lib/libnsl.so.1

      libc.so.1 =>             /usr/lib/libc.so.1

      libdl.so.1 =>            /usr/lib/libdl.so.1

      libmp.so.2 =>            /usr/lib/libmp.so.2

      /usr/platform/SUNW,Ultra-5_10/lib/libc_psr.so.1

%

 

Obviously, it would be much easier if we didn’t have to copy all of these libraries into the chroot() directory (not to mention having to re-copy the libraries every time we install a new patch which updates the files in /usr/lib).  Also, not having the libraries in the chroot() directory means that they aren’t available for our hypothetical attacker to subvert.

 

Since the BIND source code is available from the ISC (see http://www.isc.org/products/BIND/), you may be able to build your own statically-linked copy of the named-xfer binary: in other words, a copy of named-xfer that does not require any shared libraries to operate.  Some operating systems (notably Solaris) make it difficult to compile statically-linked binaries, though there are usually workarounds if you have reasonable programming skills (for a step-by-step process for creating statically-linked binaries under Solaris, look here).

 

Assuming you are unable to build a statically-linked executable for whatever reason, you will have to copy all of the shared libraries indicated by ldd into your chroot() directory:

 

cd /var/named

mkdir –p usr/platform/SUNW,Ultra-5_10/lib

chown –R root:daemon usr/platform

chmod –R 111 usr/platform

cd usr/platform/SUNW,Ultra-5_10/lib

cp /usr/platform/SUNW,Ultra-5_10/lib/libc_psr.so.1 .

chown root:daemon libc_psr.so.1

chmod 555 libc_psr.so.1

 

cd /var/named

mkdir –p usr/lib

chown –R root:daemon usr/lib

chmod –R 111 usr/lib

cd usr/lib

for lib in libresolv.so.2 libsocket.so.1 libnsl.so.1 \

libc.so.1 libdl.so.1 libmp.so.2 ld.so.1

do

       cp /usr/lib/$lib .

       chown root:daemon $lib

       chmod 555 $lib

done

 

Note that in addition to the libraries reported by ldd, Solaris binaries also require a copy of the /usr/lib/ld.so.1 file.  A similar file may be required on other Unix variants.

 

Step 4.  Make Devices

The next step is to create the device special files that named needs during normal operations.  Unfortunately the list of required devices is completely OS-dependent.  For example, Linux and BSD machines only need a copy of /dev/null in the chroot() area, but Solaris requires six different devices (conslog, null, tcp, ticotsord, udp, and zero).  The only way to truly figure out the list of devices appropriate for your OS is to run named using some sort of kernel tracing program (truss for Solaris, trace or ktrace on other systems)—when named aborts because it is unable to find a particular device, the name of the missing device will be shown in the kernel trace so you create a copy of the device in the chroot() area and try running the program again.  This debugging process can take a while.

 

Knowing which devices you need isn’t sufficient.  You also need to figure out not only the correct permissions for the device

, but also special information including the device type and the major and minor device numbers for that device on your system.  Luckily, all of that information is available in the output of the ls –l command:

 

% ls -l /dev/null

crw-rw-rw-  1 root  wheel  2, 2  Feb 7 01:30 /dev/null

%

 

The “c” in the first column indicates that this is a character special device (don’t worry about what that means), and the two numbers after the group owner are the major and minor device numbers.  All of this information gets used as arguments to the mknod program, which is what you use to create devices on a Unix system:

 

cd /var/named

mkdir dev

chown root:daemon dev

chmod 111 dev

 

mknod dev/null c 2 2

chown root:wheel dev/null

chmod 666 dev/null

 

Note that for Linux and BSD systems, you will also need to run syslogd with an additional argument to create an additional logging device in the chroot() directory.  The correct usage would be:

 

syslogd –a /var/named/dev/log

 

Step 5.  Copy Other Configuration Files

You will also need to create a /var/named/etc directory to hold the named.conf file for your name server:

 

cd /var/named

mkdir etc

mv /etc/named.conf etc

chown –R root:daemon etc

chmod 111 etc

chmod 644 etc/named.conf

 

Typically the name server will also need a copy of the local time zone map (so log entries are displayed with the local time information, rather than GMT).  On BSD systems this is /etc/localtime, but on Solaris the time zone file for your machine is stored someplace under /usr/share/lib/zoneinfo (you could just copy the entire zoneinfo hierarchy into your chroot() area if you want—there’s nothing very sensitive here).  Solaris machines also need a copy of the /etc/netconfig file in the chroot() area.

 

Running Your Name Server

Now that the chroot() directory is fully configured, you will need to move your local zone maps into the /var/named/zones/master directory.  These files should be owned by root, but mode 644 so that they can be read by the name server process running as the named user.

 

You will also probably need to modify the path names used in the /var/named/etc/named.conf file.  For example, you should set the directory option in this file to be /var/run:

 

options {

     directory “/var/run”;

};

 

Note that this means the /var/named/var/run directory we created in Step 1 above.  Remember that by the time your name server reads the named.conf file, it will have already performed the chroot() operation, so all pathnames should be considered relative to the /var/named directory.

 

Similarly, you want to make sure that all of your zone declarations properly reference zone files in either the /zones/master or /zones/slave directories:

 

zone “.” {

     type hint;

     file “/zones/master/root.cache”;

};

 

Finally, you will need to edit your boot scripts to start the named process with the proper arguments.  A typical invocation might be:

 

if [ -x /usr/sbin/named –a \

     –f /var/named/etc/named.conf ]

then

     /usr/sbin/named –t /var/named –u named –g named

fi

 

The –t flag specifies the chroot() directory and –u and –g specify the user and group to run the name server as.

 

Is It Really Worth It?

This may seem like an enormous undertaking, but remember that you only have to do this once per name server, and you typically have very few name servers in a given organization (particularly name servers that are in positions where they can be attacked by external sources).  Also, some operating systems (notably OpenBSD) come pre-configured to chroot() their name servers with only minimal intervention from the administrator.

 

The bottom line is that BIND buffer overflows are actively being exploited in older releases, and people are out there looking for new buffer overflows in existing releases.  The only protection you will have against attacks that we don’t know about yet is to have your name server configured in as secure a manner as possible before the attack occurs.

 

References For Further Reading

The SANS Institute has been kind enough to make the DNS Security section of their Securing Linux: Step-by-Step guide available on the Web.  This document gives a step-by-step procedure for running a chroot()ed named on a Linux system.

 

The ISC BIND pages are still the primary source for information about security issues regarding BIND.  The BIND source code and documentation are also available on-line from this site.