16

Can someone please explain the set-user-ID mechanism in Unix ? What was the rationale behind this design decision? How is it different from effective user id mechanism ?

4 Answers 4

15

You might know the normal read, write and execute permissions for files in unix.

However, in many applications, this type of permission structure--e.g. giving a given user either full permission to read a given file, or no permission at all to read the file--is too coarse. For this reason, Unix includes another permission bit, the set-user-ID bit. If this bit is set for an executable file, then whenever a user other than the owner executes the file, that user acquires all the file read/write/execute privileges of the owner in accessing any of the owner's other files!

To set the set-user-ID bit for a file, type

 chmod u+s filename

Make sure that you have set group-other execute permission too; it would be nice to have group-other read permission as well. All of this can be done with the single statement

 chmod 4755 filename

It is also referred to as Saved UID. A file that is launched that has a Set-UID bit on, the saved UID will be the UID of the owner of the file. Otherwise, saved UID will be the Real UID.

What is effective uid ?

This UID is used to evaluate privileges of the process to perform a particular action. EUID can be changed either to Real UID, or Superuser UID if EUID!=0. If EUID=0, it can be changed to anything.

Example

An example of such program is passwd. If you list it in full, you will see that it has Set-UID bit and the owner is "root". When a normal user, say "mtk", runs passwd, it starts with:

Real-UID = mtk  
Effective-UID = mtk  
Saved-UID = root

Reference link 1
Reference link 2

1
  • Shouldnt it be Real-UID = mtk Effective-UID = **root** Saved-UID = root?
    – Nor.Z
    Nov 15, 2022 at 2:45
7

man credentials is a good source of information in this case. See also this questiin on SO. For historical explanation see this archived post.

Rather than calling "set UID" and "effective UID" a mechanism, the whole concept of UIDs should be called that. The rationale for existence of the various UIDs are various troubles with privilege separation. Even regular (unprivileged) users sometimes need to do things (access resources) that only privileged users can. To achieve this easily, programs can change their UIDs. There are 3 types of these:

  • real UID - the UID that owns a process

  • effective UID - the UID a process currently runs as - this determines the actual capabilities of the process at any particular moment. This is also what ps shows you in the USER field.

  • saved set UID - placeholder used for switching back and forth between real and effective UIDs

The need for the last one arises from the fact, that regular users can only switch between these three and nothing else and a setuid program usually needs to know somehow, who was the user who loaded it (plus the real UID should not be changed since that would create even bigger mess).

2

mtk's expalanation is a good one.

The passwd example is one of privilege escalation -- passwd always runs as root since it must alter files that only root is allowed to alter. This makes it important that the passwd executable not be prone to buffer overflows, etc, such that a clever normal user might be able to put it to uses for which is was not intended.

Another rationale is to protect the user in the same manner as you might use su if you are logged in as root -- in order to diminish or restrict your privileges for a specific tasks, not escalate them. For example, if I have permission to start a daemon service that does not require access to my stuff and has its own stuff, which is all it needs (eg, a logger), running it suid will mean it only has access to that stuff and not mine or anyone elses.

Note that it is possible to set uid programmatically even if the suid bit is not set on the executable, however, that will not work for escalation. Ie., if you are a normal user and write a program that sets uid at some point itself, that program cannot switch to root. Apache works this way, I believe. It is usually started by root and has one process that then forks children which switch uid to a non-privelleged user (eg, "httpd"). Those child processes are what do the actual web server work.

1

The answer by @mtk above has a small error pointed out by @Nor.Z in his comment.

The credentials man page explains the concepts of real user ID (RUID), effective user ID (EUID) and saved user ID (SUID). See here.

If I understood well, by default, when a process is started, its EUID and SUID are set equal to the process's RUID. The same holds for groups. However, if the binary with which the process was started has the set-user-ID bit set in its permissions, the EUID and the SUID, will be set to that of the owner of the binary, which may be a privileged user like root. The RUID will be left untouched. Later, if the EUID is changed by the program in its source code, it can be re-set back to that of root, for example, whenever needed, because the root UID was also stored in the SUID and can be taken from there. The example C program below demonstrates this (adapted from this YouTube video):

// filename: uid.c 
#define _GNU_SOURCE

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {

    uid_t ruid, euid, suid;

    // Get the real, effective, and saved user IDs
    if (getresuid(&ruid, &euid, &suid) == -1) {
        perror("getresuid");
        return 1;
    }

    // Display the user IDs
    printf("Real      UID: %d\n", ruid);
    printf("Effective UID: %d\n", euid);
    printf("Saved     UID: %d\n", suid);

    // file /etc/hosts is owned by root, and only root can open it
    // in write mode. Normal/unprovileged users can only open it in read mode.

    /*See if we can open the /etc/hosts file for reading and writing, as the EUID*/
    printf("open: %d\n", open("/etc/hosts", O_RDWR));
    /* access() tests what the RUID can do. We check 'writing' in this case */
    printf("access: %d\n", access("/etc/hosts", W_OK));

    printf("--\n");
    // drop the privileges in the EUID by setting it with the RUID, and re-try
    seteuid(ruid);

    if (getresuid(&ruid, &euid, &suid) == -1) {
        perror("getresuid");
        return 1;
    }

    printf("Real      UID: %d\n", ruid);
    printf("Effective UID: %d\n", euid);
    printf("Saved     UID: %d\n", suid);

    /*See if we can open the /etc/hosts file for reading and writing, as the EUID*/
    printf("open: %d\n", open("/etc/hosts", O_RDWR));
    /* access() tests what the RUID can do. We check 'writing' in this case */
    printf("access: %d\n", access("/etc/hosts", W_OK));

    return 0;
}

Compile the program with gcc uid.c -o uid and run it with ./uid. It will output:

Real      UID: 1000
Effective UID: 1000
Saved     UID: 1000
open: -1
access: -1
--
Real      UID: 1000
Effective UID: 1000
Saved     UID: 1000
open: -1
access: -1

An open or access return code of -1 means the operation has failed. Then change the ownership of the program to root with sudo chown root ./uid and set the set-user-ID bit with sudo chmod u+s ./uid. To check do ls -lA and notice the 's' in the uid binary owner permissions:

total 20
-rwsrwxr-x 1 root  gamba 16272 dic  6 16:44 uid
-rw-rw-r-- 1 gamba gamba  1192 dic  6 16:17 uid.c

Run the program again and compare with above:

Real      UID: 1000
Effective UID: 0
Saved     UID: 0
open: 3
access: -1
--
Real      UID: 1000
Effective UID: 1000
Saved     UID: 0
open: -1
access: -1

As can be seen, both, the EUID and the SUID were set to that of root, the owner of the uid binary! Then the open operation that uses the EUID succeeded (return code 3), but the access operation failed (return code -1) because it still uses the RUID (an unprivileged one) which can only read the /etc/hosts file. After the "--" mark, we see that if we change the EUID to the RUID, both operations fail again.

Last, if we set the ownership of the uid binary back to the normal/unprivileged user "gamba", set one more time the set-user-ID bit, but run the program with sudo, we will get the output:

Real      UID: 0
Effective UID: 1000
Saved     UID: 1000
open: -1
access: 0
--
Real      UID: 0
Effective UID: 0
Saved     UID: 1000
access: 0
access: 0

As can be seen, the set-user-ID bit in this case had the effect of setting the EUID to that of an unprivileged user, which made the open operation fail. The access operation succeeded as it was made as the real user, which in this case is root (because we run the program with sudo). This behavior is useful in situations where we don't want to alter some files by mistake while running a program with a privileged user (like root), as pointed out by @goldilocks in his answer above.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .