index-logo

Understanding Linux User IDs

Understanding Linux User IDs (UIDS)


In Linux, User IDs (UIDs) are unique numerical identifiers assigned to each user account on the system. These identifiers are used by the operating system to determine which permissions and resources a user has access to. UIDs are stored in the system's user database, typically located in the "/etc/passwd" file or in a similar system file.


Each UID corresponds to a specific user account, and the system uses these IDs to enforce security policies and manage file permissions. For example, when a user creates a file, the file's ownership is associated with the UID of the user who created it, allowing the operating system to control who can read, write, or execute the file based on the permissions assigned to that UID.


Additionally, UIDs are used for processes running on the system, with each process being associated with the UID of the user who initiated it. This allows the system to enforce resource usage policies and access controls based on user privileges.


Processes User Identifiers


In Linux, there are three main user identifiers related to process execution and permissions:


Real User ID (RUID): This is the actual user who owns the process. It is inherited from the user who executed the process.


Effective User ID (EUID): This is the user ID that determines the permissions the process has while executing. It can be changed during the execution of a process, typically through the setuid mechanism or by privileged processes.


Saved User ID (SUID): The saved user ID (SUID) distinct from SUID binaries (which abbreviate SetUID binaries), is employed when a privileged process (often operating as root) must temporarily relinquish privileges to perform certain actions but subsequently return to its privileged state.


Set-UID binaries


Set-UID binaries, also known as SUID binaries, are programs that, when executed, temporarily set the Effective User ID (EUID) of the process to match the owner of the program.


For example, let's consider a scenario where the root user owns a program named myprogram. By assigning the Set-UID bit to myprogram, any user executing it will have their EUID temporarily set to that of the program's owner (in this case, root).


Let's test this in a real scenario. Let's create a C program that creates a an example file:


#include <stdio.h>

int main() {
    char filename[] = "test";

    FILE *file = fopen(filename, "w");


    return 0;
}

Let's compile it:


elswix@ubuntu$ gcc program.c -o program

I will give ownership of this file to the root user, but I will not grant Set-UID privileges to the file yet.


elswix@ubuntu$ sudo chown root:root program
elswix@ubuntu$ ls -l
-rwxrwxr-x 1 root   root   16016 mar 20 11:10 program
elswix@ubuntu$

Let's see what happens if I run this program as a non-privileged user:


elswix@ubuntu$ ./program
elswix@ubuntu$ ls -l
-rwxrwxr-x 1 root   root   16016 mar 20 11:10 program
-rw-rw-r-- 1 elswix elswix     0 mar 20 11:13 test
elswix@ubuntu$

As you can see, it created a file named test and it's owned by elswix. Okay, this worked as expected. Now, let's see what happens if we set the 'Set-UID' bit for the program.


elswix@ubuntu$ sudo chmod 4755 program
elswix@ubuntu$ ls -l
total 16
-rwsr-xr-x 1 root   root   16016 mar 20 11:10 program
-rw-rw-r-- 1 elswix elswix     0 mar 20 11:13 test
elswix@ubuntu$

Before running the program again, I'll delete the test file:


elswix@ubuntu$ rm test
elswix@ubuntu$ ls -l
total 16
-rwsr-xr-x 1 root root 16016 mar 20 11:10 program
elswix@ubuntu$

Now that the program has the setuid bit enabled and is owned by root, let's run the program again as a non-privileged user:


elswix@ubuntu$ ls -l
total 16
-rwsr-xr-x 1 root root   16016 mar 20 11:10 program
-rw-rw-r-- 1 root elswix     0 mar 20 11:18 test

As you can see, it created a file owned by root. This indicates that, thanks to the Set-UID bit on the program, the EUID of the process is set to that of the program's owner (in this case, root), allowing the program to create a file with root privileges.


It still displays elswix after root because of the Group Identifier (GID). However, in this context, the GID is irrelevant.


System and Exec Functions in C


As you may think, running commands within a Set-UID binary should give us the ability to execute commands as the binary's owner. Your idea is correct, but it depends.


To understand this, let's first create a program that executes any command you pass as the first argument using the system() function:


#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    // Check if a command is provided as an argument
    if (argc != 2) {
        printf("Usage: %s <command>\n", argv[0]);
        return 1;
    }

    // Execute the command using system()
    system(argv[1]);

    return 0;
}

Let's compile it using gcc:


elswix@ubuntu$ gcc program.c -o program

Let's change its ownership to root and set it the Set-UID bit:


elswix@ubuntu$ sudo chown root:root program
elswix@ubuntu$ sudo chmod 4755 program
elswix@ubuntu$ ls -l
total 16
-rwsr-xr-x 1 root root 16008 mar 20 11:31 program

Great! Now, if we pass any command as an argument, it should run as root, correct?


elswix@ubuntu$ ./program whoami
elswix

WTF?


Well, let's break down what's happening behind the scenes. As you've seen, we've been using the system() function to run commands. But maybe the issue lies within this function.


That's right, but let's explore why. After digging into the documentation for the system() function, I found out that it spawns a child process. The command that runs in the background is /bin/sh -c {our command}.


According to the documentation:


The system() library function uses fork(2) to create a child process that executes the shell command specified in 'command' using execl(3) as follows:


execl("/bin/sh", "sh", "-c", command, (char *) NULL);

Behind the scenes, system() functions merely as an exec function, executing commands with /bin/sh -c. Okay, regardless of what's happening behind the scenes, why don't we achieve command execution as root when the program is owned by root and the setuid bit is enabled?


Well, since Ubuntu 16.04 (though I'm unsure if this countermeasure was implemented earlier), /bin/sh points to /bin/dash. The dash interpreter includes some protections against Set-UID processes. These protections involve a simple check of the User Identifiers.


Consider /bin/sh being Set-UID; theoretically, when executed, it should run as root. However, that's not the case. As explained earlier, while the Set-UID bit sets the effective user ID of the process, the protection against Set-UID drops our privileges. This protection checks whether the Effective User ID equals the Real User ID.


I couldn't find a clear explanation about this in the dash documentation, but I know it works similarly to bash, which explicitly explains this in its documentation:


If the shell is started with the effective user (group) id not equal to the real user (group) id, and the `-p` option is not 
supplied, no startup files are read, shell functions are not  inherited  from  the  environment, the  SHELLOPTS, BASHOPTS,
CDPATH, and GLOBIGNORE variables, if they appear in the environment, are ignored, and the effective user id is set to the 
real user id. If the `-p` option is supplied at invocation, the startup behavior is the same, but the  effective user id is 
not reset.

As I explained earlier, when the shell starts with the effective user ID not equal to the real user ID, the effective user ID is set to the real user ID, thereby dropping our privileges.


It also mentions something about the -p parameter, which also works in dash. In this case, if the -p parameter is supplied, the effective user ID is not reset.


That's why when running our program, even with the setuid bit enabled and owned by root, our commands are executed as the user who started the process. This is because system() uses /bin/sh -c behind the scenes and doesn't supply the -p parameter.


Is there any way to bypass this protection?


The short answer is no, but it depends. You can use the setuid() function to set the UID at runtime. However, there are limitations with setuid() because it behaves differently depending on whether the process is privileged or not.


As per the documentation:


setuid()  sets  the effective user ID of the calling process. If the calling process is privileged (more precisely: if the 
process has the `CAP_SETUID` capability in its user namespace), the real UID and saved set-user-ID are also set.

In simpler terms, when executing setuid as root, all three user identifiers will be set to the specified number. For instance, if we execute setuid(0), the real user ID (RUID), effective user ID (EUID), and saved user ID (SUID) will all be set to 0 (since setuid(0) must be executed as a privileged user or with the CAP_SETUID capability). However, if we execute setuid(1000) as a non-privileged user, it will only modify the effective user ID (EUID) to 1000.


Let's test by using system() with the setuid() function:


#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    // Check if a command is provided as an argument
    if (argc != 2) {
        printf("Usage: %s <command>\n", argv[0]);
        return 1;
    }

    setuid(0);
    system(argv[1]);

    return 0;
}

We can compile it using gcc (don't worry about warnings):


elswix@ubuntu$ gcc program.c -o program
program.c: In function main:
program.c:11:5: warning: implicit declaration of function setuid [-Wimplicit-function-declaration]
   11 |     setuid(0);
      |     ^~~~~~
elswix@ubuntu$

Let's change the program ownership to root and enable the Set-UID bit:


elswix@ubuntu$ sudo chown root:root program
elswix@ubuntu$ sudo chmod 4755 program

Now, when executing the command whoami, it returns 'root':


elswix@ubuntu$ ./program whoami
root
elswix@ubuntu$ ./program id
uid=0(root) gid=1000(elswix) groups=1000(elswix)

It worked! But what happens if root isn't the owner of this binary? Obviously, setuid(0) won't be permitted, but if the owner is a user with UID 1000, will we execute commands as that user?


Let's modify the program:


#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    // Check if a command is provided as an argument
    if (argc != 2) {
        printf("Usage: %s <command>\n", argv[0]);
        return 1;
    }

    setuid(1000);
    system(argv[1]);

    return 0;
}

This time, instead of setuid(0), we'll use setuid(1000), since the owner won't be root, setting the effective UID to 0 won't be allowed.


Let's compile it:


elswix@ubuntu$ gcc program.c -o program
program.c: In function main:
program.c:11:5: warning: implicit declaration of function setuid [-Wimplicit-function-declaration]
   11 |     setuid(1000);
      |     ^~~~~~
elswix@ubuntu$

This time, the owner of this file will be elswix, whose UID is 1000. Let's enable the Set-UID bit for the program:


elswix@ubuntu$ chmod 4755 program
elswix@ubuntu$ ls -l
total 20
-rwsr-xr-x 1 elswix elswix 16048 mar 20 13:52 program
-rw-rw-r-- 1 elswix elswix   279 mar 20 13:52 program.c

Well, of course, if we run this program as the current user, it will execute commands as elswix:


elswix@ubuntu$ ./program whoami
elswix

But what happens if instead of elswix, we are www-data (or any other user different from elswix or root):


www-data@ubuntu$ ./program whoami
www-data
www-data@ubuntu$

What?


If we're using setuid(), why aren't we executing commands as elswix (the owner of the Set-UID program)?


Well, as I explained earlier, the setuid() function only sets the effective user ID when running as an unprivileged user. In this case, we're www-data, which is indeed an unprivileged user, but the binary is Set-UID. However, the key difference here is that the owner is elswix, who is also an unprivileged user. So, even though we're running the program as elswix, we're only setting the effective user ID to elswix.


In this scenario, as a non-privileged user, the use of setuid() doesn't make any changes because it will only set the effective user ID, and this is already set by executing the Set-UID binary.


Of course, changing the effective user ID would be sufficient to execute commands as another user. However, in this case, we're using system, which behind the scenes utilizes /bin/sh -c.


So, is there any way to execute commands as another non-privileged user using system() and a Set-UID program?


The answer is yes. Let's use the same example as above. Imagine a program where the owner is the user with the UID 1000 and has the Set-UID bit enabled. You can use setreuid() to set both the Real User ID and the Effective User ID. This will be enough to prevent /bin/dash from dropping privileges:


#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    // Check if a command is provided as an argument
    if (argc != 2) {
        printf("Usage: %s <command>\n", argv[0]);
        return 1;
    }

    setreuid(1000, 1000);
    system(argv[1]);

    return 0;
}

We can use gcc to compile it (ignore the warnings):


elswix@ubuntu$ gcc program.c -o program

Let's make it Set-UID:


elswix@ubuntu$ chmod 4755 program
elswix@ubuntu$ ls -l
total 20
-rwsr-xr-x 1 elswix elswix 16056 mar 20 14:07 program
-rw-rw-r-- 1 elswix elswix   287 mar 20 14:07 program.c

Now, as www-data, I'll run the same command as before, but this time it should work because this program also sets the Real User ID:


www-data@ubuntu$ ./program whoami
elswix
www-data@ubuntu$

As you can see, it worked!


You could also use setresuid(). This function sets all three user IDs. For example:


setresuid(1000,1000,1000);

This ensures that there are no conflicts with any UID.


The exec() family


So far, we have explored how system() functions. We've understood that system() is essentially an exec() function behind the scenes, but with some differences, as it utilizes a shell interpreter /bin/sh -c to create processes.


The exec() family operates differently. Let's see what the documentation tells us about them:


The exec() family of functions replaces the current process image with a new process image.

All the functions in the exec() family are different ways to utilize the execve() function. Here is its manual:


execve() executes the program referred  to by pathname. This causes the program that is currently being run by the calling 
process to be replaced with a new program, with newly initialized stack, heap, and (initialized and uninitialized) data segments.

As you can see, exec() differs from system(). Firstly, execve() uses a single process, unlike system(), which creates a child process with /bin/sh -c.


In simpler terms, execve() is a function that runs a new program by replacing the current one. It loads the program specified by its file path and starts executing it, replacing the current program entirely. This means that the new program starts with a fresh set of memory areas like stack, heap, and data segments.


What about the UIDs? Of course, the documentation provides information about this:


The process's real UID and real GID, as well its supplementary group IDs, are unchanged by a call to execve().

After a call to execve(), the process's real User ID (UID), real Group ID (GID), and supplementary group IDs remain unchanged. This means that if the Set-UID program we execute runs another program using execve(), the User Identifiers are not changed, so the new program will be executed with the same effective user identifier.


For example, let's create a program that runs /usr/bin/whoami. This time we won't use any setuid() function, but we'll enable the Set-UID bit:


#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    // Check if a command is provided as an argument
    
    execve("/usr/bin/whoami", NULL, NULL);

    return 0;
}

Let's compile it (ignore the warnings):


elswix@ubuntu$ gcc program.c -o program
program.c: In function main:
program.c:7:5: warning: implicit declaration of function execve [-Wimplicit-function-declaration]
    7 |     execve("/usr/bin/whoami", NULL, NULL);
      |     ^~~~~~
elswix@ubuntu$

Let's change the ownership to root and enable the Set-UID bit:


elswix@ubuntu$ sudo chown root:root program
elswix@ubuntu$ sudo chmod 4755 program
elswix@ubuntu$ /bin/ls -l
total 20
-rwsr-xr-x 1 root   root   15968 mar 20 14:46 program
-rw-rw-r-- 1 elswix elswix   188 mar 20 14:45 program.c

Now, let's see what happens when running it as a non-privileged user:


elswix@ubuntu$ ./program
root
elswix@ubuntu$

As you can see, we're running /usr/bin/whoami as root, without using setuid() functions. This is because, unlike system(), execve() uses a single process, and since /bin/sh -c is not being used, the program is executed correctly with the effective user ID of root (0).


This applies not only to root; if the program is owned by elswix and has the Set-UID bit enabled, it would run as elswix.


Conclusion


In conclusion, Linux User Identifiers (UIDs) play a critical role in determining the permissions and privileges of processes and users within the Linux operating system. UIDs are used to identify users and assign access rights to files, directories, and resources. They consist of a numerical value associated with each user, defining their ownership and control over system resources. Understanding how UIDs work is essential for managing security and access control within Linux systems, particularly when dealing with privileged operations and Set-UID programs.


I understand that grasping the concepts of Linux UIDs can be challenging, but with persistence and multiple readings of this article, you'll eventually grasp the main concepts. If you have any doubts or questions, feel free to reach out to me via Instagram. I'll be happy to help clarify any uncertainties you may have. Keep exploring, and don't hesitate to ask for assistance when needed.


I really hope this helped you in some way. Here, I'll provide you with references where I drew inspiration, learned, and obtained information.


References


execve() manual
setuid() manual
exec() manual
dash manual
bash manual


Inspired by Set-UID Rabbithole - 0xdf