Programming in the Solaris OS With Privileges
By Rich Teer, May 2006
Introduction
One of the guiding principles, if not the guiding principle, of writing secure programs is the principle of least privilege. The principle of least privilege states that any operation carried out by a program should be done so using the least amount of privilege required to successfully complete the task.
Traditional flavors of UNIX, including Solaris releases prior to Solaris 10 (the various releases of Trusted Solaris notwithstanding), have very coarse granularity when it comes to the notion of "least" privilege. Historically, the Solaris OS has supported only two levels of privilege: the regular user and the so-called superuser, or root (the latter of which is identified by having a user ID of 0). In other words, prior to the Solaris 10 release, the Solaris OS had an "all or nothing" approach to privileges: a process with an effective user ID of 0 essentially had no restrictions, and all other processes had many restrictions. (We should point out that most other flavors of UNIX also adopt this approach; our focus on the Solaris OS is merely a byproduct of this article's subject. We should also add that the Solaris 10 OS is not the only OS to adopt this privilege separation model: VMS, for example, has had it since its inception.)
For a limited subset of uses, another mechanism can be used to restrict the amount of privilege a program uses: set-user-ID or set-group-ID. This is when a process runs with its user and/or group ID set to those of the program's owner rather than those of the invoking user. Suppose we have a program that stores data in a certain file, and that the program has a need to restrict how the file is updated. We could make the program set-user-ID (SUID) root, but a less drastic approach would be to assign the program its own user (and/or group), and have it (the program) become that user when necessary. The passwd program is one example of this, but another could be a game which keeps track of high scores in a file that is writable only by the user ID assigned to it (the game). We'll not discuss this model further, because the scope of its use is so limited. Instead, the rest of this article describes how we can use Solaris privileges to implement privilege bracketing.
Problems With "All or Nothing"
There are many privileged operations a program might want to perform. (When we talk of privileged operations, we mean those operations that cannot or should not be performed by just any process.) As we alluded to previously, the Solaris OS "all or nothing" model means that a program that needs to perform a privileged operation can theoretically perform all privileged operations. For example, a program that needs to bind to a reserved port can also, by design or otherwise, shut down the system. This unsatisfactory state of affairs can be mitigated somewhat by using privilege bracketing.
Privilege Bracketing
With privilege bracketing, we enable privileges only when they are actually required, turning them off when they are no longer required. The undesirable opposite is when privileges are always enabled, whether or not they are required. The name comes from the fact that the enabling and disabling of privileges "bracket" the privileged operation, typically around system calls. We can represent this in the following pseudo code:
priv_on ()
perform_privileged_operation ()
priv_off ()
The idea of privilege bracketing is to narrow the window of a program's vulnerability so that it is as small as possible, reducing the damage any exploit can do. For example, in order for a process to be able to write to a file, it is only necessary for that file to be opened for write access. In other words, we would assert privileges only for the open system call (relinquishing them immediately after); they should not be asserted for the write system call.
Without exception, all programs that perform privileged operations should use privilege bracketing, even if they use discrete process privileges. (Prior to the Solaris 10 OS, privilege bracketing can be implemented using the seteuid function. Although it is coarse-grained, it is better than not using privilege bracketing at all.)
Process Privileges
The Solaris 10 OS introduced process privileges, where the god-like abilities available to processes whose effective UID is 0 are broken down into numerous discrete privileges. Following our previous example, now a process that needs to bind to a reserved port can do so by asserting the appropriate privilege (that is, PRIV_NET_PRIVADDR); provided the program has been written correctly, all other privileged operations (such as the ability to read or write any file on the system) will not be available to it. The privileges man page describes each of the available privileges. (As an aside, we should mention that privilege checks are generally not performed if we can already perform an operation without them.)
The Solaris implementation of privileges uses four sets: the Permitted set (P), the Inheritable set (I), the Limit set (L), and the Effective set (E).
P defines the set of privileges a process can ever potentially use. Privileges not in P cannot be asserted by a program. P is initialized when a process is created by being inherited from the parent. A process can remove privileges from P, but cannot add them. Privileges removed from P are automatically removed from E.
I defines the set of privileges that may be passed onto child processes after a call to exec. I and P are often the same, but it is not unusual for privileges to remain in P even though they have been removed from I.
L defines the upper bound of the set of privileges that a process may assert or pass onto its children. Unlike the other privilege sets, changes to L do not take effect until the next exec. L is typically equal to the set of all (or all zone) privileges.
E defines the set of privileges that are currently in use. When a process starts, E and P are equal. Subsequently, E is either a subset of, or is equal to, P.
Each process has a flag associated with it that advertises its privilege awareness state; a process is either privilege-aware or it is not-privilege-aware. By default, processes are not-privilege-aware; they become privilege-aware by doing either of the following:
- Manipulating P, L, or E by calling
setppriv
- Using
setpflags
Observed vs. Implementation Privilege Sets
To discuss how privileges are handled with both types of process, let's introduce two types of E and P privilege sets: the observed set, which we'll indicate by prefixing the set name with an "o", and the implementation set. We'll indicate this by prefixing the set name with an "i".
When a process becomes privilege-aware, the following assignments take place:
iE = oE
iP = oP
The oE and oP sets are invariant when a privilege-aware process changes UID. However, if the process is not-privilege-aware, oE and oP are assigned as follows:
oE = (euid == 0) ? L : iE
oP = (euid == 0 || ruid == 0 || suid == 0) ? L : iP
In other words, if the euid is equal to 0, oE is set to L, otherwise it is set to iE (which is likely to be a subset of L). Similarly, if the euid, ruid, or suid is equal to 0, oP is set to L, otherwise it is set to iP. A side effect of all this is that it is possible for a non-privilege-aware SUID root process to not have all privileges at its disposal (it is this property that ensures that even UID 0 processes in a local zone cannot affect processes in other zones).
We can demonstrate some of this by using the ppriv command, which is used to display or modify the privilege sets associated with a process, and run programs with a specific set of privileges. First, let's run ppriv as a regular user:
rich@excalibur4167# id
uid=1001(rich) gid=10(staff)
rich@excalibur4168# ppriv $$
803: /bin/ksh
flags = <none>
E: basic
I: basic
P: basic
L: all
Now we'll become root and run ppriv again:
rich@excalibur4169# su -
Password:
Sun Microsystems Inc. SunOS 5.11 snv_23 October 2007
root@excalibur503# ppriv $$
4795: /usr/bin/ksh
flags = <none>
E: all
I: basic
P: all
L: all
As we would expect, a root shell has all privileges available to it. This means we can become another user without typing in that user's password:
root@excalibur511# su - rich
Sun Microsystems Inc. SunOS 5.11 snv_23 October 2007
rich@excalibur4096# ppriv $$
4808: -ksh
flags = <none>
E: basic
I: basic
P: basic
L: all
Again, as we would expect, the regular user does not have any privileges other than the basic set. After exiting the regular user's shell, so that we are root once again, let's run a shell (as root) with a reduced L, and try to become another user:
root@excalibur512# ppriv $$
4795: /usr/bin/ksh
flags = <none>
E: all
I: basic
P: all
L: all
root@excalibur513# ppriv -e -s L=basic ksh
root@excalibur514# ppriv $$
4821: ksh
flags = <none>
E: basic
I: basic
P: basic
L: basic
root@excalibur515# su - rich
Could not join default project
su: unable to set credentials
This time, despite our user ID being 0, we can't change our user ID because we have insufficient privilege to do so. To investigate this a bit further and determine why we couldn't join the default project or set credentials, we can run ppriv in debug mode by passing it the -D command-line option:
root@excalibur519# ppriv -D -e -s L=basic ksh
root@excalibur520# su - rich
su[917]: missing privilege "proc_audit" (euid = 0, syscall = 186)
needed at auditsys+0x81
su[917]: missing privilege "proc_taskid" (euid = 0, syscall = 70)
needed at tasksys_settaskid+0x49
Could not join default project
su: unable to set credentials
Here we can see that the su command failed because we couldn't assert the proc_audit and proc_taskid privileges (because we have restricted the Limit privilege set to the basic privileges).
Process Privileges API
Now that we have some background information about privileges, and have seen an example of their use, let's take a look at some of the functions and data types we use in our privilege-aware programs. These are declared in the <priv.h> header file.
Individual privileges are represented by a data type called a priv_t, which we initialize with a privilege ID string. For example:
priv_t priv = PRIV_FILE_DAC_READ;
Similarly, a set of privileges is represented by an opaque data type called a priv_set_t. We manipulate these by using priv_str_to_set and its related functions. Note that although numbers are used to represent privileges within the kernel, the mapping of a given privilege's name to its number is valid for the current kernel instance only, and may change at the next boot. It is for this reason that we should never program using privilege numbers, and should always use the ID strings, calling the conversion functions as necessary.
Let's now take a look at some of the process privilege functions. Examples showing how we can use these functions appear later in this article.
The setppriv Function
The setppriv function is used to add, remove, or replace privileges in any of the four privilege sets we described previously. It has the following prototype:
int setppriv (priv_op_t op, priv_ptype_t which, const priv_set_t *set);
The op argument specifies the operation to be performed, and may be one of the following values:
PRIV_ON This adds the privileges specified by set to the privilege set that is specified by which.
PRIV_OFF This removes the privileges specified by set to the privilege set that is specified by which.
PRIV_SET This replaces the privilege set specified by which with the privileges specified by set.
The which argument specifies which privilege set is to be changed, and must be one of the following: PRIV_PERMITTED, PRIV_INHERITABLE, PRIV_LIMIT, or PRIV_EFFECTIVE.
The priv_set Function
The priv_set function is a convenience wrapper for setppriv, and has the following prototype:
int priv_set (priv_op_t op, priv_ptype_t which, ...);
The op and which arguments have the same meaning as for setppriv, with the additional value of PRIV_ALLSETS for which, which means that the operation should be applied to all privilege sets. The third and subsequent arguments are a NULL terminated list of privilege names.
The priv_str_to_set Function
The priv_str_to_set function is used to convert a string containing the name of one or more privileges to a privilege set. It has the following prototype:
priv_set_t *priv_str_to_set (const char *buf, const char *sep,
const char **endptr);
The first argument, buf, points to a privilege specification that consists of one or more privilege names that are separated by characters in the string pointed to by sep. If endptr is not NULL, then if an error occurs when parsing the string, a pointer to the rest of the string is stored in it. Upon successful completion, a pointer to a priv_set_t data structure is returned. The application must free this structure by calling priv_freeset when it no longer requires it.
The privilege specification may contain one or more of the following strings: "none" for the empty set, "all" for all privileges, "zone" for all privileges available within a zone, and "basic," which is the set of privileges traditionally granted to all users on login (except for root).
Several other related functions exist, which we don't describe here (for example, priv_getbyname, priv_getbynum, priv_getsetbyname, and priv_getsetbynum). Interested readers are referred to their man pages.
The priv_addset Function
This function adds a privilege to a privilege set, and has the following prototype:
int priv_addset (priv_set_t *sp, const char *priv);
The privilege named by priv is added to the set pointed to by sp. Conversely, a function called priv_delset is used to remove a privilege from a privilege set.
The priv_inverse Function
The priv_inverse function is used to invert a privilege set.
void priv_inverse (priv_set_t *sp);
The privilege set pointed to by sp is inverted and stored in sp.
The priv_freeset Function
When we're finished using a privilege set, we must free it using priv_freeset.
void priv_freeset (priv_set_t *sp);
The storage for the privilege set pointed to by sp is freed.
Examples
All this theory is great, but how do we use these functions in practice? Let's assume that we're writing an application that needs to be able to read a file or files it wouldn't normally be able to. A quick read of the privileges man page tells us that the privilege we require is called PRIV_FILE_DAC_READ (DAC is an acronym for Discretionary Access Control, which refers to the usual UNIX permissions and ACLs).
We'll write three functions: priv_init, priv_on, and priv_off. The first of these does any necessary initialization, and the latter two are used to implement privilege bracketing.
Example: The priv_init function
The priv_init function initializes the privilege state of our hypothetical application. This function should be called near the beginning of main so that unnecessary privileges are dropped as soon as possible. On entry, all privilege sets are assumed to be set to all available privileges, as a side effect of the calling program being set-user-ID root. On exit, the privilege sets are equal to the basic set + PRIV_FILE_DAC_READ - PRIV_PROC_FORK - PRIV_PROC_EXEC. Because privileges in addition to the basic set are asserted when priv_init returns, applications should call priv_off immediately after calling priv_init to disable them.
Calling priv_init has the side effect of making the process privilege-aware (because priv_init internally calls setppriv; it is calling the latter that actually makes the process privilege-aware).
The following listing shows the code for priv_init. Note that for the sake of brevity we've omitted header files and the like.
1 void priv_init (void)
2 {
3 priv_set_t *priv_set;
4 if ((priv_set = priv_str_to_set ("basic", ",", NULL)) == NULL) {
5 perror (gettext ("lock: priv_str_to_set failed"));
6 exit (1);
7 }
8 if (priv_addset (priv_set, PRIV_FILE_DAC_READ) == -1) {
9 perror (gettext ("lock: Can't add FILE_DAC_READ privilege"));
10 exit (1);
11 }
12 if (priv_delset (priv_set, PRIV_PROC_EXEC) == -1) {
13 perror (gettext ("lock: Can't remove PROC_EXEC privilege"));
14 exit (1);
15 }
16 if (priv_delset (priv_set, PRIV_PROC_FORK) == -1) {
17 perror (gettext ("lock: Can't remove PROC_FORK privilege"));
18 exit (1);
19 }
20 priv_inverse (priv_set);
21 if (setppriv (PRIV_OFF, PRIV_PERMITTED, priv_set) == -1) {
22 perror (gettext ("lock: Can't set Permitted privilege set"));
23 exit (1);
24 }
25 if (setppriv (PRIV_OFF, PRIV_LIMIT, priv_set) == -1) {
26 perror (gettext ("lock: Can't set Limit privilege set"));
27 exit (1);
28 }
29 priv_freeset (priv_set);
30 setreuid (getuid (), getuid ());
31 }
Let's take a look at priv_init in more detail.
1-19 Create a temporary privilege set, consisting of just the basic privileges. Then add FILE_DAC_READ and remove PROC_EXEC and PROC_FORK. There are other privileges in the basic set that our application doesn't actually need, so we could (and perhaps should) theoretically remove them too. But we don't here for the sake of brevity.
20-28 Invert the temporary privilege set to get the set of privileges we'll never need, then remove them from the Limit and Permitted sets (the latter also implicitly removes them from the Effective set).
29 Free the temporary privilege set, as we are finished with it. In some cases, it can be a good idea to not free the privilege set: we might want to manipulate it further later in the program. In this case, we should not call priv_freeset here, but call it later, when we are finished with the set.
30 Privilege-aware processes do not use the SUID semantics, so reset our effective user ID to our real one, permanently. We do this because our program is SUID root so that it starts executing with all necessary privileges (recall that we can only remove privileges from the Permitted set, not add to them).
Example: The priv_on function
We call the priv_on function when we need to turn on the FILE_DAC_READ privilege. The following listing shows the code for this function.
1 void priv_on (void)
2 {
3 if (priv_set (PRIV_ON, PRIV_EFFECTIVE, PRIV_FILE_DAC_READ, NULL) == -1) {
4 perror (gettext ("lock: Can't enable FILE_DAC_READ privilege"));
5 exit (1);
6 }
7 }
Let's see how this seven-line function works.
1-7 Add the FILE_DAC_READ privilege to our Effective privilege set.
Example: The priv_off function
This function is the converse of priv_on; its code is in the following listing.
1 void priv_off (void)
2 {
3 if (priv_set (PRIV_OFF, PRIV_EFFECTIVE, PRIV_FILE_DAC_READ, NULL) == -1) {
4 perror (gettext ("lock: Can't disable FILE_DAC_READ privilege"));
5 exit (1);
6 }
7 }
1-7 Remove the FILE_DAC_READ privilege from our Effective privilege set.
Example: Privilege Bracketing Using priv_on and priv_off
The following are a few lines that show how we use privilege bracketing around a library call (in this case, getspnam, which is used to look up an entry in the shadow file by user name). They are taken from the compare_passwd function of the lock program.
1 priv_on ();
2 if ((shadow_ent = getspnam (user)) == NULL) {
3 saved_errno = errno;
4 priv_off ();
5 msg = gettext ("lock: Can't get shadow database entry for %s: %s");
6 fprintf (stderr, msg, user, strerror (saved_errno));
7 exit (1);
8 }
9 priv_off ();
1 Turn on the FILE_DAC_READ privilege by calling priv_on. We must do this because the shadow file (which we read in line 2) is normally readable only by root.
2-4 If an error occurred while calling getspnam, save errno and call priv_off to turn off all privileges. We save errno in case an error occurs in priv_off.
9 If the call to getspnam was successful, call priv_off to turn off all privileges.
Although these examples help us understand the privilege API, we ideally need to examine a complete application to see how to use them for real. Space limitations prevent us from doing so in this article, but interested readers are referred to the source code for the author's lock program, from which these examples were taken. Inspired by its BSD namesake, lock is a CDDL-licensed open source utility. It can be found on the Solaris lock home page at http://www.rite-group.com/rich/sw/lock.html.
Summary
This article has described Solaris privileges, and their reason for being. It briefly compared the privilege model with the traditional UNIX "all or nothing" model, and discussed privilege bracketing.
We then described the Solaris implementation of privileges, and a few of the functions that make up the Solaris privileges API. Finally, we showed three examples illustrating the use of this API: priv_init, which we use to initialize the process' privileges, and priv_on and priv_off, which we use to bracket privileged operations in our programs.
Acknowledgments
Many thanks to Glenn Brunette for reviewing this article.
Recommended Reading
The author's book, Solaris Systems Programming, is essential for readers developing Solaris applications. The OpenSolaris community blogs (especially that of Casper Dik) are another great source of information. Other useful resources are the Solaris Security for Developers Guide (on docs.sun.com) and two Sun BluePrints documents: Privilege Debugging in the Solaris 10 Operating System and Limiting Service Privileges in the Solaris 10 Operating System.
Here is more information on these resources:
- Solaris Security for Developers Guide, Sun Microsystems, 2005, ISBN 0-595-28558-9
- Limiting Service Privileges in the Solaris 10 Operating System (pdf), Sun Microsystems, 2005
- Privilege Debugging in the Solaris 10 Operating System (pdf), Sun Microsystems, 2006
- Solaris Systems Programming, Rich Teer, 2005, ISBN 0-201-75039-2
Rich Teer is an independent consultant who has been an active member of the Solaris community for more than 10 years. He is the author of the best-selling Sun Microsystems Press book,
Solaris Systems Programming, and several Solaris OS-related articles. He was a member of the OpenSolaris pilot program, and currently serves on the OpenSolaris Community Advisory Board (CAB). Rich lives in Kelowna, British Columbia, Canada, with his wife, Jenny, and their canine child, Judge. His web site can be found at
www.rite-group.com/rich.