Programming in the Solaris OS With Privileges
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 flavours of UNIX, including Solaris prior to Solaris 10 (the various releases of Trusted Solaris notwithstanding), have very course granularity when it comes to the notion of “least” privilege. Historically, Solaris 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 Solaris 10, Solaris 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 flavours of UNIX also adopt this approach; our focus on Solaris is merely a byproduct of the fact that this is an article about Solaris. We should also add that Solaris 10 is not the only OS to adopt this privilege separation model: VMS, for example, had it since its inception.)
For a limited subset of uses, there is another mechanism we can use 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, this 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, Solaris’ 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
Privilege bracketing is what we do when 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 (reliquishing 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 Solaris 10, privilege bracketing can be implemented
using the seteuid function. Although it is course grained, it is better
than not using privilege bracketing at all.)
Process Privileges
Solaris 10 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 (i.e., PRIV_NET_PRIVADDR); provided the program has
been written correctly, all other privileged operations (e.g., 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 initialised 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 either by manipulating P, L, or E by calling
setppriv, or by 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 either the euid, ruid, or suid are 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 their 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 header file.
Individual privileges are represented by a data type called a priv_t,
which we initialise 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 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. |
| This removes the privileges specified by set to the privilege set that is specified by which. |
| 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).
There are several other related functions (e.g., priv_getbyname,
priv_getbynum, priv_getsetbyname, and
priv_getsetbynum) that we don’t describe here.
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 initialisation,
and the latter two are used to implement privilege bracketing.
Example: The priv_init function
The priv_init function initialises 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 headers 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 occured 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
at www.richteer/sw/lock.php.
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 initialise 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 [Teer 2005], is essential for readers developing Solaris applications. The community blogs at www.opensolaris.org/os/blogs (especially that of Casper Dik) are another great source of information, as are the Solaris Security for Developers Guide [Sun 2005a], Privilege Debugging in the Solaris 10 Operating System [Sun 2006], and Limiting Service Privileges in the Solaris 10 Operating System [Sun 2005b].
[Sun 2005a]: Solaris Security for Developers Guide, ISBN 0-595-28558-9. See docs.sun.com/app/docs/doc/816-4863 for more details.
[Sun 2005b]: Limiting Service Privileges in the Solaris 10 Operating System. See www.sun.com/blueprints/0505/819-2680.pdf.
[Sun 2006]: Privilege Debugging in the Solaris 10 Operating System. See www.sun.com/blueprints/0206/819-5507.pdf.
[Teer 2005]: Solaris Systems Programming, ISBN 0-201-75039-2. See www.richteer.ca/ssp for more details.
Author’s Bio
Rich Teer is an independent Solaris consultant who has been an active member of the Solaris community for more than ten years. He is the author of the best-selling Sun Microsystems Press book, Solaris Systems Programming, and several Solaris-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, with his wife, Jenny, and their canine child, Judge. His web site can be found at richteer.ca.