8. Access Control

“For if a man watch too long, it is odds he will fall asleepe”

—FRANCIS BACON

In this chapter, we take on the idea of controlling access to system resources. Once users have been successfully authenticated to a system, the system generally needs to determine the resources each user should be able to access. There are many different access control models for addressing this issue. Some of the most complicated are used in distributed computing architectures and mobile code systems, such as the CORBA and Java models. Often, access control systems are based on complex mathematical models that may be hard to use. There are certainly too many varying systems to go into them all in detail. (In Chapter 3 we sketched some of the unique access control mechanisms found in distributed computing platforms such as CORBA and DCOM.)

Here we cover the two access control models you’re most likely to use: the traditional UNIX access control system and the Windows model, which is based on Access Control Lists (ACLs). In the case of UNIX systems, we limit our discussions to highly portable constructs.

The UNIX Access Control Model

If you are familiar with the UNIX access control model, we suggest you skip or skim to the subsection entitled The Programmatic Interface.

On UNIX systems, each user is represented to the security infrastructure as a single integer, called the user ID (UID). Users can also belong to “groups,” which are virtual collections set up to allow people to work on collaborative projects. Each group has its own identifier called a group ID (GID). Users can belong to multiple groups.

The UID 0 is special. It is the UID used by the administrator of the system (usually the “root” account on a UNIX machine). UID 0 effectively gives the administrator (or other user borrowing such privilege) complete access to and control over the entire machine.

All objects in a system are assigned an owning UID and GID, including files, devices, and directories. Along with the owning identifiers, there are access permissions associated with each object indicating who has access to read the object, to write to the object, and to execute the object (if the object happens to be a program).1 There are three such “rwx” sets of access permissions per file. The first set applies to the user who owns the file. The second set applies to any users that belong to the group that owns the file. The third set applies to all other users in the system. The UID 0 is capable of performing any operation on any object, regardless of permissions.

1. The meanings of these permissions are different for directories, and are discussed later.

Usually, when a user executes a program, the executing program and all its child processes are assigned the UID of the user running the program. This is the basis for access control in UNIX. Objects are always accessed from a running process. When an access attempt occurs, the operating system scrutinizes the effective UID (EUID) and effective GID (EGID) assigned to the current process, comparing them with the permissions needed for the access to be allowed.

Usually, the EUID is the same as the real UID of a process. However, some programs require special access to system resources and can change the EUID of a process based on a strict set of rules (discussed later). Such programs are called setuid programs. There is a similar notion for groups called setgid programming.

If a setuid program has a dangerous enough security problem in it, any user able to run the program can be enabled to do anything that the owning user can do. Often, setuid programs need special access that requires ownership by UID 0. In this case, a bug in a single setuid program can lead to the complete compromise of the entire system. In practice, attackers often break into a machine through a generic user account (usually by exploiting a hole in a network server, although often compromising a poorly chosen password in some way). From there, a sophisticated attacker (or well-armed script kiddie) will often exploit a broken setuid program owned by the root user to escalate privileges and gain control over the target machine.

How UNIX Permissions Work

There are three sets of permissions associated with any file or directory in a UNIX system. The first set consists of owner permissions, which specify how the owner of an object may use the file. The second set consists of group permissions, which specify what users belonging to the group to which the file belongs can do to the file. The third set specifies what other users (in other words, those other than the actual owner of the file and members of the appropriate group) can do to a file. There are some subtleties here. In particular, there is a best-match policy. If a file is group writable, but not owner writable, then the owner cannot write to the file without first changing the permissions, even if the owner is in the group associated with the file.

There are three types of permission: read, write, and execute. On regular files, the meaning of each permission type should be clear. On directories, the meanings of the permissions are less obvious. The read permission specifies whether someone can see a listing of the contents of a directory. The execute permission specifies whether a user can traverse into a directory or otherwise use files in a directory in any way. The write permission specifies whether users can add, change, and remove files in a directory. If the write permission is granted to an entity, most operating systems provide a way of restricting this functionality so that the entity may only remove or change files it owns, unless that entity is also the owner of the directory (on such systems, this is done by “setting the sticky bit”; more later).

A user can set the permissions on any file the user owns arbitrarily, and the root user can set the permissions of any file at all arbitrarily. No one else can change permissions on a file. Strangely, a user can set the permissions on a file so that it cannot be read, written, or executed by the user actually setting the permissions. Ultimately, such settings have no security implications because you can always do what you want to a file by changing the permissions back to something more liberal. The permissions are there primarily to keep a user from accidentally performing an operation on a file that should probably not be performed.

The root user (UID 0) isn’t subjected to read or write checks at all, so even if the root user gives no one permission to operate a file whatsoever, the root user can still read or write the file without changing the permissions of the file. However, the root user is still subject to limited checks on the executable permission. If anyone can execute a file, then root can. Otherwise, root cannot. One critical implication of “root privilege” is that as root, a user must be extremely careful about manipulating files. Many a green system administrator has accidentally zapped files irretrievably.

There are other special permissions in UNIX. The text permission is one that isn’t commonly used in its original form. However, it’s been usurped, and redubbed the sticky bit, which was previously mentioned because it applies to directories. On files, the text bit often does nothing these days. However, its original intent was to prevent a program from swapping to disk. On operating systems with this capability, the text bit is usually a permission that only the root user can set.

Two more permissions are setuid and setgid, which only have portable behavior on executable files. These permissions indicate whether the UID, GID, EUID, and EGID identifiers can be modified by the executable. If the setuid permission is activated for a file, then when the file is run, the EUID of the process is set to the UID of the file owner. Similarly, the setgid permission causes the EGID to be set to the GID of the file owner on program start-up. This functionality carries a fair amount of security risk, and later we discuss how to use it effectively.

Modifying File Attributes

From the UNIX shell, there are two basic commands used to modify file attributes: chmod, which sets permissions on files or directories, and chown, which changes ownership of a file or directory (both user and group ownership).

The chmod command takes a specification on permission changes or a specification for new permissions altogether, along with files on which to operate. There may also be various options that you can pass to chmod. For example, –R is a commonly used option, which recurses through any directories, setting permissions as specified.

To specify incremental permission changes, you first specify which of the three access specifiers you wish to change: u indicates the file owner (user); g, the group; o, others; and a, everyone. In the access specifier, you can specify more than one of u, g, and o (ugo is the same as a). Next you must specify whether permissions should be granted or denied. + indicates grant, whereas – indicates deny. Using = instead of + or – indicates that the permissions should be granted, but all other permissions should be turned off. Finally, you specify the permissions you wish to grant or deny. r, w, x, s, and t map to read, write, execute, setuid or setgid, and text (sticky) permissions respectively. You can usually provide multiple specifiers by separating them with commas (however, there can be no spaces anywhere in any of the specifiers or even between specifiers). After the specifier, you then indicate files on which you wish to change permissions. For example,

chmod go-rwx *

would cause all files in a directory to be inaccessible to those who are in the group that has group ownership of the files, and to all other people except the user. If the user had previously run the command

chmod u-rwx *

then no one would be able to access any of the files by default, except for root, who is able to read and write, but not execute. The user (file owner) can always move to circumvent that restriction of course. If we wish to allow ourselves to do whatever we wish, but explicitly restrict all others from doing anything on those files, we could just say

chmod u=rwx *

There is also a syntax using octal notation that requires the user to understand how permissions are implemented. Basically, permissions can be thought of as four octal digits. Each permission is represented by a single bit in a single digit. The most significant octal digit is usually 0. This digit encodes setuid, setgid, and the sticky bit. The most significant bit changes the setuid bit, and the least significant bit changes the sticky bit. Therefore, there are the following possible combinations:

image

The next most significant digit represents owner permissions, then group permissions. The least significant digit represents other permissions. In all of these three digits, the most significant bit represents read permissions, the next bit represents write permissions, and the least significant bit represents execute permissions:

image

For example, we could forbid all access to blah.txt as follows:

chmod 0000 blah.txt

To give read-only access to everyone but ourselves, we generally can do

chmod 0644 blah.txt

However, if we want everyone to be able to execute the program foo, we would do one of two things, depending on whether foo is a script or a binary. Scripts often need execute permission if they are set up to invoke an interpreter. They always need read permission. In that case, we would use

chmod 0755 foo

In the case of a binary, we can get away without the read permission:

chmod 0711 foo

File permissions can be viewed by using the –l flag to the ls command. Permissions show up as the first thing on the left in most listings. There are ten characters worth of permissions. The first character specifies whether an entry is a directory. If it is, you will see a d; otherwise, you’ll see a dash. The remaining nine characters are grouped in sets of three. The first three are owner permissions, the second three are group permissions, and the last three are other permissions. Each group has one character for read, write and execute, in that order. If the permission is turned on, you will see one of r, w, or x. Otherwise, you will see a dash. For example, if we were to run the command

chmod ug+rwx,o+r,o–wx blah

then a long directory listing would show the permissions

–rwxrwxr––

The long directory listing also shows the username associated with the owner UID (or the owner UID if there is no such username), the group name or ID, and the last modification time of the file, among other things. Note that you should never rely on the last modification time to be accurate unless you completely control the environment, because this can be controlled programmatically. For example, the touch command can be used to change the last modification time arbitrarily (a common trick used to hide tracks).

Modifying Ownership

The chown command changes user and group ownerships of a file. Usually, only root runs this command.2 The primary problem is that a user cannot change the user ownership of a file without first obtaining root permission. The group ownership can be changed, but only to the active group of the user.

2. On System V, non-root users can give away files. This has caused security holes on programs written for BSD.

Although a user can belong to many groups, there is always a “primary” group. All groups are active for permission checking. New files (generally) inherit the group of the parent directory. The primary group is important only for setgid, and (if parent directory permissions are set appropriately on System V-derived systems, such as Solaris) the group of a newly created file. To switch the primary group, the user generally uses the newgrp command. For example, if your login is “fred,” and “fred” is your default primary group, yet you wish to change group ownership of files to “users,” you must first run the command

newgrp users

When running the chown command, a user specifies the new ownership information. In most UNIX variants the owner is a string that is either a username or a userid. The new ownership information is followed by the files on which the operation should be performed. For example,

chown fred blah.txt

changes the ownership of blah.txt to fred, only if “fred” exists, and only if root runs this command. If fred runs the command, nothing happens. If anyone else runs the command, an error results.

To change group ownership, we can prepend a period to the ownership information. For example, one can say

chown .users blah.txt

You can also specify both user and group ownership at once:

chown fred.users blah.txt

The umask

When programs run, there is one important property of the program to keep in mind: the umask. The umask determines which access permissions files created by the running process get. The umask is a three-digit octal number that specifies which access bits should not be set under any conditions when creating new files. The umask never affects the special access bits (setuid, setgid, and sticky). When a program opens a new file, the permissions set on the new file are merged with umask. For example, most commands try to use permission 0666, which can be restricted through use of umask:

0666 & ~umask

That is, if you specify all zeros for umask, then everyone is able to read and write created files. If you specify all sixes or all sevens, then no one is able to do so. In most cases, the executable bit in the umask is ignored, meaning that the maximum permission at file creation is usually 0666, instead of 0777. However, a programmer can specify a higher maximum (usually in C code).

The umask command can set the umask for a command shell. Often, the default is 022, which results in files of mode 644. Such files are world readable, but are only writable by the user creating the file. If you wish to keep all eyes out other than the creating user, set the umask to 0066 or 0077.

You should carefully consider using appropriate umasks when writing your own programs. We discuss how to set the umask programmatically in the next subsection.

The Programmatic Interface

In the previous few subsections we discussed several UNIX commands for modifying file attributes important to access control. All of these things can be manipulated programmatically. First, let’s look at changing permissions on a file, which is done through the chmod() and fchmod() system calls. We’ll exclusively use fchmod() to avoid any potential race conditions (see Chapter 9).

The fchmod() system call takes two arguments: a file descriptor for an open file and a mode_t that represents the new permissions. The mode_t type is usually a 16-bit integer, for which only the least significant 12 bits are used. The bits that are used are grouped in threes, and represent the special, user, group, and other permissions. In most cases, the programmer OR’s in bits using symbolic constants:

image

Remember, only the owner of a file or root may change permissions on a file. Given an already open file f (we have yet to discuss how to open a file securely; we do so in Chapter 9), here’s an example of setting its permissions:

image

On error, –1 is returned, and errno is set. The call to fchmod only fails (in most cases) if there are permissions problems. However, not all permissions problems stem from a user not having the right UID (EPERM). A user may also try to change the permissions on a file mounted on a read-only CD (EROFS). A generic input/output error is also possible (EIO). The other common failure mode occurs if the file descriptor is invalid (that is, the fd does not point to a valid open file [EBADF]). There can also exist file system-specific errors.

If we want to change ownership of a file, we can use chown() or fchown(). Again, we should restrict ourselves to the latter to help avoid unwanted TOCTOU race conditions. The fchown call takes three arguments: a file descriptor, a UID (type uid_t), and a GID (type gid_t). Passing a –1 to either the second or third argument results in no change to that argument.

With the fchown call, we do not need to do the equivalent of the newgrp command. Any group to which the effective user belongs can be passed to the call. However, on many operating systems, only root’s EUID (0) can change the actual owner. Here’s an example of changing the group to the GID 100 (often the GID for “users,” but not always!) on an already open file:

image

The failure modes are the same as with fchmod. The call always succeeds or fails, and it should not be possible to set the group owner successfully when attempting to set the owner to an invalid value.

Setting and querying the umask is straightforward. The umask() system call takes a mode_t, which is a new mask. The old mask is returned. Therefore, to query the umask of a current process, one must do the following;

image

The umask call always succeeds.

Earlier, we argued that time stamps should not be considered reliable. That’s because the utime() system call can arbitrarily modify time stamps. The utime call works only on file names. However, you should be aware that given the right circumstances (such as, if your program is setuid or setgid), a race condition could allow an attacker to modify the time of a file the attacker would otherwise not be able to access through your software.

The utime() call takes a filename and a struct utimbuf:

image

Usually, time_t is a 32-bit integer, representing the number of seconds since January 1, 1970. Within a decade, the construct will probably be changed to a 64-bit integer on most machines. The utime call only fails when access to change a time stamp is denied (for example, the calling EUID is neither the owner of the file nor the root) or when the filename specified does not exist.

Setuid Programming

Earlier in the chapter we discussed the setuid and setgid bits in an access specifier. These bits come in handy when we want to allow access to files or services that the user running the program is not allowed to access. For example, we may want to keep a global log of users who access a particular program. Furthermore, we may want to ensure that no one other than root is able to modify or view this log, except via our program.

By default, a program runs with the permissions of the user running the program. However, setuid and setgid allow us to run a program with the permissions of the executable’s owner. They also give us some flexibility in changing the UID, EUID, GID, and EGID of a program as it runs. Otherwise, the UID would always be equal to the EUID, and the GID to the EGID.

For simplicity’s sake, in this section we look only at setuid programming. Note that setgid programming works the same way, using an API that is the same once UID is replaced with GID in all the calls.

If a setuid program is not owned by root, then there are only two operations on the UID and EUID that can be performed. First, we can swap the UID and the EUID. Second, we can set one to the other. By setting one to the other, we restrict ourselves from accessing the other UID. For example, let’s say a user with the UID mcgraw3 runs a setuid program owned by someone with the UID viega. The program starts with a UID of mcgraw and an EUID of viega. The program has the permissions of the EUID at runtime, not the UID. Without making changes, the process can modify files owned by viega, but not by mcgraw. To change this, we can swap the EUID and the UID. Now, we’re able to modify files owned by mcgraw, but not those owned by viega.

3. Technically, these are the symbolic names for the UIDs, and not the UIDs themselves, because UIDs are numeric.

Harking back to our log file example, once we’ve added an entry to the log file owned by viega, we’d probably like to set both the EUID and the UID to mcgraw. In this way, if the user running the program finds an exploitable bug after the program completes its logging, it could never obtain the permissions of viega because the program will have given them up.

We can set the EUID and UID using the call setreuid (which stands for set the real and effective UIDs). This call takes only two uid_ts: the first being the desired UID and the second the desired EUID. A –1 leaves a value unchanged. We also need to know how to query the UID and the EUID. There are two simple calls to do this: getuid() and geteuid(). Both always succeed, and both return a uid_t.

Here’s an example for our start-up log, which elides how to open the log file securely:

image

image

image

Note that on program entry we immediately change our EUID to that of the user running the program. If an attacker can break our program before we drop privileges altogether (using, say, a buffer overflow), the attacker could most definitely switch them back. Thus, our security move raises the bar only slightly, and in fact it is mostly useful to keep us from accidentally shooting ourselves in the foot.

More often, setuid programs are owned by root because they need some special privilege only granted to the root user. For example, UNIX-based operating systems only allow root to bind to a port under 1024. If you’re writing an SMTP server, then it’s critical that you bind to port 25, and thus you must have at least one program that either runs with root privileges or is setuid root. Other operations that only root can perform include calls to chown(), chroot() (described later), user administration (changing password file entries), and direct access to device drivers.4

4. Note that many disk devices are group readable by “operator” or some other group to permit backup dumps. This can easily leave sensitive files readable by a nonroot user.

In general, letting any program run with root privileges all the time is a very bad idea, because if any part of your program is exploitable, then the exploit can accidentally provide an attacker with root privileges. When possible, practice the principle of least privilege: Do not run as root at all.

When a program does need privileges, try to confine operations that need special privileges to the beginning of your program, and then drop root privileges as quickly as possible. When opening files, devices, or ports, this should be straightforward. Unfortunately, doing this is sometimes impossible, such as when it is necessary to use ioctls that can only be made by root.

The best solution in such cases is to compartmentalize, and hope to minimize the risk of a vulnerability. For example, when you need to deal with a device driver, you may write a daemon that runs as root and does nothing but moderate access to the device in question. Unprivileged programs could connect to that process using UNIX domain sockets.

Or, if you need to authenticate the calling user in a portable way, you could have a setuid program that runs as root and gets spawned by your running program using, say, a named pipe (see the manpage for the mkfifo call). The root process could authenticate the owner of the named pipe after opening it. Often, even a regular pipe will do: Set up the pipe, and fork/exec the setuid program. This eliminates a lot of checking on the rendezvous.

A problem with many of these solutions is that there is no portable built-in authentication mechanism with sockets, even UNIX-domain sockets, that are completely machine local. However, user-based authentication is possible in UNIX-domain sockets by using standard techniques such as passwords. Even if programs should run without user intervention, passwords can be stored in the file system in such a way that only the owner can read them. This technique can work, but it is cumbersome.

The bulk of your code should run as an unprivileged user. Moreover, that user should be compartmentalized from other users. Don’t use the “nobody” user for doing work, because other programs may use it as well. Breaking one causes all of them to break. Instead, create a new user just for your application.

It is worth noting that child processes inherit the UID and EUID of a parent process. Therefore, after calling fork(), but before calling execve(), be sure to set the UID and the EUID of the child to the appropriate values.

Also, note that making a script setuid can engender risks. Many people only allow binaries to be setuid. At the very least, you should carefully consider what it means to make a script setuid. Historically, this was an incredibly bad idea because of a sinister race condition. The interpreter for the script would be run setuid, which would then load in the script. If the attacker linked to a setuid script, the attacker could relink the script to some attack code while the interpreter was loading. Then, the attack code would run with the privileges of the user owning the original script.

This hole is no longer present on any modern UNIX implementations of which we are aware. One approach taken to fix it is to open the script file before launching the interpreter. Another approach is to disallow setuid scripts altogether (Linux takes this approach). Because scripts are more likely to call out to other programs without fully realizing security considerations, the second solution is certainly safer. In general, you should completely avoid setuid scripts, especially if you care at all about portability. If you need to run a setuid script, write a small C program that is setuid that calls your script. Your C program should have a hard-coded value for the location of the script, and you should be sure that the script is opened securely (discussed later).

Access Control in Windows NT

In this section we provide a cursory glance at the Windows NT access model (as well as its successors). Although we don’t provide Windows-specific API information in this book, our supporting Web site does have links to appropriate Microsoft programmer documentation.

Much like UNIX, Windows has a notion of security IDs (SIDs) that are assigned to both individual users (account SIDs) and groups (group SIDs). However, Windows also has many other constructs that do not directly map to the UNIX model. The first construct is the token. There are several types of tokens in Windows NT. The most important is the access token, which is a bit of data held by a machine that establishes whether a particular entity has been previously authenticated. The access token contains all relevant information about the capabilities of the authenticated entity. When deciding whether to allow a particular access, the security infrastructure consults the access token.

Another important type of token is the impersonation token, which allows an application to use the security profile of another user. This token affords the same type of functionality provided by setuid programs in UNIX (for example, impersonation has to be explicitly enabled for an application), but it is implemented quite differently.

A second notion important to the Windows access control model is the security attribute. Security attributes are typically stored in access tokens and they specify privileges that an entity can be granted if the right criteria are met. These attributes are used for many purposes, including deciding whether particular rights can be transferred to other users.

One of the most noteworthy differences between access control in NT and UNIX is that NT generally offers more granular privileges. For example, in a UNIX system, the right to transfer ownership of a file is implicit in the notion of ownership. By contrast, NT implements this right as a separate attribute.

Similarly, file permissions are much more granular in Windows NT than they are in a UNIX system. As we have described, UNIX provides only read, write, and execute permissions. On NT, permissions are made up of a set of basic capabilities, such as the ability to read or the ability to transfer ownership.

There are four “standard” NT permissions, but an arbitrary number can be created. One standard permission is “No access,” which affords a user no access whatsoever to the resource in question (external attributes such as size may not even be queried). The second standard permission is “Read access,” which enables three capabilities: the ability to query basic file attributes, the ability to read data in the file, and the ability to execute the file. “Change access” expands on Read access, adding the ability to modify and delete files, and to display ownership and permission information. The final default permission is “Full control,” which adds to Change access the ability to change file permissions and to take ownership of a file. For directories, there are seven default permissions as opposed to UNIX’s three.

Another significant advantage to the NT model is that the permissions structure is not as flat as on UNIX systems. On a UNIX system, most interesting services (such as network services) must be owned by the “superuser” account (in other words, the account with UID 0, which can ultimately access the entire machine—often root). This is because most interesting capabilities are held by the kernel, and no facilities exist to accommodate UIDs that are more privileged than the average user but are at the same time capable of performing only a subset of the functionality afforded UID 0. In other words, UNIX is an “all-or-nothing” system for any privilege not related to file access: You can either run code in the kernel, in which case you can access the object, or you cannot. Yet from the guidelines we presented in Chapter 5, we know that it is good security practice to grant only the minimum privilege necessary for performing a particular set of duties.

Windows NT does not have the same limitations inherent in most UNIX systems. In NT, the privilege structure is broken into four privilege types. Much like a UNIX machine, there is a standard user privilege type and an administrator privilege, which affords superuser status on a machine and is just as dangerous as UID 0 on a UNIX machine. There is also a guest privilege type, which is similar to the user type, but is theoretically more restricted.

The privilege type that sets NT apart is the operator type, which defines useful subsets of administrator privilege. An example is the print operator, a type that allows a service to perform printer management tasks. The print operator privilege allows only limited file access; file writes and deletes must be limited to a single spool directory. Consequently, if an attacker can manage to break a program to obtain print operator access, the damage that can be done to the file system is limited to the spool directory.

The operator types are better used in NT Server than they are in NT Workstation. On machines running NT Workstation, there are only two operator types, lumping together many various types of functionality.

In Windows 2000, the granularity of rights assignment is increased even more dramatically. Although the administrator user still exists by default, refinements to the privilege assignment scheme make having an administrator account unnecessary.

One final feature of the Windows NT file access mechanism that is prominently different from standard UNIX mechanisms is the ACL. Although the UNIX family of operating systems usually implements access by specifying properties on an “owner, group, other” basis (as described earlier), NT once again provides finer grain control. Along with each entity protected by access control, the operating system stores an ACL, which is a list of users and groups and their associated capabilities.

For example, if we wanted to work on a collaborative project with Alice, Bob, and Chris, we could give Alice Full access to all files in the project, but give Bob only Change access to those files. We could also choose to give Chris Change access, and only to a small subset of the files. For other files, we could give Chris No access. On a typical UNIX system, such sharing is not possible. Instead, we would have to put the files under group ownership; lump Alice, Bob, and Chris in one group; and give them all equal access to our files. On Windows NT, we have arbitrary flexibility, and we don’t even have to use a group.

NT also has ACL inheritance, which UNIX operating systems lack. An ACL on a directory applies to files in that directory, unless a file has an overriding ACL. This feature makes configuration tasks easier.

Compartmentalization

Containing an attacker in the case of a break-in is always a good idea. Let’s turn back to UNIX for our description of compartmentalization.

The chroot() call provides a standard way to compartmentalize code. The chroot() system call changes the root directory for all subsequent file operations. It establishes a “virtual” root directory. In the best case, even if an attacker can break the running program, the damage an attacker can do to the file system is limited to those files under the virtual root. In practice, chroot() usually doesn’t work all that well.

One issue with the chroot() call is that only the root UID can use it. Often, programmers allow the program to continue to run without totally dropping root privileges. This is a bad idea. When you run a process chroot(), the process should set the EUID and UID both to a less-privileged user immediately to eliminate the window of vulnerability.

Another issue is that chroot() doesn’t work exactly as advertised unless you immediately follow the call with a call to chdir("/"). Without that call, an attacker may be able to use relative paths to access the rest of the file system. The problem is that chroot doesn’t change the current working directory, so “.” may point to a directory outside the chroot environment!

Additionally, note that any open file descriptors from before the chroot that point outside the chroot jail are still valid file descriptors. You should make sure that you don’t inadvertently provide access to resources outside the jail in this manner. If you’re afraid of this happening, loop through each possible file descriptor you do not wish to use, and explicitly close them all. In such cases, you will usually close all file descriptors other than stdin, stdout, and stderr (which are 0, 1, and 2). Note that you want to avoid looping through every possible file descriptor because this can take a lot of time. Instead, only close those descriptors that the called program may hard code. (Although this isn’t a good programming technique, it does happen.)

Based on these notes, we should run chroot as follows:

image

There are other problems with this system call. The biggest is that dependent files are not visible unless you put them in the chroot “jail.” For example, if you use popen() or system() to run other programs (see Chapter 12 for why this is a bad idea), you will not get the correct behavior until you install /bin/sh and everything on which it might be dependent! Also, if you dynamically load libraries, those libraries are no longer visible unless you place them in the environment. If you want access to a byte code-interpreted language such as Python, Java, or Perl, you need more than just the binary. In fact, you need to copy every single module and library these languages may load as your program runs its entire course, as well as the language implementation itself. Even worse, some applications require the presence of /dev/zero, /dev/null, /dev/urandom or /dev/random, and they fail unless you also put those devices in the jail.

Obviously, chroot can be a maintenance nightmare. One thing you should avoid putting in a chroot environment is anything at all that is setuid root. When such programs are available in a jail, the jail has a high likelihood of being broken from the inside out.

One more issue is that some system calls that aren’t involved with the file system usually ignore the chroot’d value. This problem makes it easy to break out of a jail as soon as code can be run with root privileges. In particular, ptrace, kill, and setpriority disregard the new root. Additionally, mknod can be used to create a real device in the jail that allows an attacker direct access to devices, which can then be used to escape the jail.

You should avoid leaving anything else in the jail that’s potentially sensitive. For example, a jail should not contain any global password databases. Also note that chroot() doesn’t keep people from opening sockets or performing any other call that isn’t file system related.

There are more modern solutions to this problem that don’t suffer from the hassles involved in creating a virtual file system, although they usually have their own usability problems. The modern solution is a “sandboxing” approach: The operating system enforces a “just-in-time” access control policy on the file system. The program sees the real file system, but the operating system may choose to forbid a particular access based on the actual parameters to the call.

The major problem with this type of solution is that it requires a well-thought-out, detailed policy regarding what can and cannot be accessed. Such a policy often requires an in-depth understanding of the program being sandboxed. By contrast, chroot()’s access control policy is simple and draconian: If it’s not in the environment, it’s not available (in the best case)! Even so, chroot() isn’t very easy to get right. As of this writing, we know several people who have put many hours into trying “to chroot()” the JVM, to protect against any flaws that might happen to be in the JVM itself. So far, we know of no one who has succeeded. An additional problem with policy-based sandboxing technologies is that they are not widely portable. At the time of this writing, no such tool is portable across more than two platforms. On the other hand, chroot() is universally available across UNIX systems.

Of course, when using a sandboxing technology, you still need to make sure that setuid root programs are not executable by the sandboxed process, or it may be possible to exploit a bug and gain unlimited access.

There are several good sandboxing solutions available for free, including Janus [Goldberg, 1996b], which runs on Linux and Solaris, and Subdomain [Cowan, 2000], which runs only on Linux. Commercial sandboxing solutions exist for Windows and FreeBSD, and several similar technologies exist in research labs. Additionally, Java comes with its own sandboxing solution. All of these solutions allow for fine-grain policy enforcement. For example, the UNIX-based solutions allow you to prevent any system call from succeeding, thus allowing you to prohibit a process from opening sockets. Usually, the control is even more fine-grain, because most such tools can check arguments passed to a system call. This book’s companion Web site provides links to the most current related technologies.

Fine-Grained Privileges

Some UNIX operating systems, such as Trusted Solaris, provide facilities to provide more fine-grain privilege control, allowing applications to select only the privileges they need to run, instead of privilege being “all or nothing,” as in the traditional UNIX model. As a result, these operating systems are better compartmentalized than usual, and there is often support for protecting parts of an operating system from everything else. There’s also a technology called capabilities that has a similar goal. Capabilities was proposed as a POSIX standard, but was never ratified. As of mid 2001, capabilities is yet to be fully realized by any major operating system, and we suspect it never will. At the very least, we find capabilities unlikely ever to become a portable standard.

These facilities tend to be packaged with a concept called mandatory access control, which is generally not found in other operating systems. In a typical operating system, if a user is granted ownership of an object (users can transfer file ownership of files they own, and the root user can arbitrarily set file ownership), the user can assign others access to an object without restriction. Therefore, if a user owns a classified document, nothing prevents that user from sharing the file with people who are not supposed to be able to access classified information. Mandatory access control solves this problem by introducing security labels to objects that necessarily propagate across ownership changes. This sort of functionality can be used to prevent an arbitrary component of the operating system from breaking the system security policy when handling an object created in another part of the system. For example, with a good mandatory access control-enabled operating system, it is possible to prevent anything in the operating system from accessing the disk drive, save the device driver. In a more typical operating system, any file protection mechanism you could imagine creating can be circumvented if a user can trick any part of the operating system into running code that directly accesses the disk.

Although there are some UNIX variants that provide this sort of advanced security functionality, it is currently impossible to get a mainstream non-UNIX operating system that implements mandatory access control.

Conclusion

Access control is one of the most fundamental security services an operating system provides. Unfortunately, access control facilities tend to be difficult to use properly. To make matters worse, the Windows model is significantly different from the UNIX model, and other platforms (such as CORBA) often have their own mechanisms as well.

In this chapter, we’ve focused primarily on the generic UNIX access control model. Many operating systems have their own extensions to this model, but our focus here has been on portability. Despite this focus, our general recommendations for working with any access control system should apply to all: thoroughly familiarize yourself with the model, learn the pitfalls, and above all, code defensively.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.226.66