Privileges relinquishing order in C

Dropping privileges is a common operation done by programs having setUID and/or setGID bits set. They do privileged operations such as binding a socket to a low port or opening files and then drop privileges to continue execution. However, user and groups have to be relinquished following a certain order otherwise these privileges could be regained later on, allowing attacker to escalate privileges.

set*id() functions

Linux has several system calls to relinquish privileges, either temporarily, to be able to regain it later in execution, or permanently.

When a process want to permanently drop its privileges it uses setuid() :

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 name‐ space), the real UID and saved set-user-ID are also set.

And setgid():

setgid() sets the effective group ID of the calling process. If the calling process is privileged (more precisely: has the CAP_SETGID capability in its user namespace), the real GID and saved set-group-ID are also set.

The return values of these functions have to be checked, to avoid dangerous security issues.

Note : initgroups() or setgroups() are not considered in this article but should also be called.

Order is power

These two functions have to be called in a specific order for it to behave as expected. Indeed, if setuid() is called before setgid(), the latter call will not set groups ID as one would expect. This is because setgid() follow this rule defined by POSIX:

If the process has appropriate privileges, setgid() shall set the real group ID, effective group ID, and the saved set-group-ID of the calling process to gid.

If the process does not have appropriate privileges, but gid is equal to the real group ID or the saved set-group-ID, setgid() shall set the effective group ID to gid; the real group ID and saved set-group-ID shall remain unchanged.

In our case, one would set the effective user ID with an unprivileged value but would then still have both the real group ID and saved set-group-ID filled with privileged values. A subsequent call to setgid() with its previous privileged value would then set the effective group ID back without failing.

Incorrect privileges relinquishing order

The above issue can be implemented with a simple program that is both SUID and SGID root and does the following:

  • drops privileges with setuid(getuid()) and setgid(getgid())
  • try to regain root group privileges with a call to setgid(0)

At all steps it prints UID/GID values to visually determine what is going on:

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

void print_uids()
{
	printf("UID: %d GID: %d EUID: %d EGID %d\n", getuid(), getgid(), geteuid(), getegid());
}

int main()
{
	print_uids();

    // Wrong order here
	if (setuid(getuid()) == -1)
		printf("setuid failed\n");
	if (setgid(getgid()) == -1)
		printf("setgid failed\n");
	
	print_uids();

	// Regain privs
	if (setgid(0) == -1)
		printf("setgid failed\n");
	
	print_uids();

	return 0;
}

It needs to be compiled and set as a root SUID/SGID binary:

# gcc -o drop_bad drop_bad.c 
# chmod +s drop_bad
# ls -l drop_bad
-rwsr-sr-x 1 root root 16352 Oct  1 10:16 drop_bad

Called with an unprivileged user, it gives the following output:

$ ./drop_bad
UID: 1000 GID: 1000 EUID: 0 EGID 0
UID: 1000 GID: 1000 EUID: 1000 EGID 1000
UID: 1000 GID: 1000 EUID: 1000 EGID 0

The second call to setgid(0) does not fail and is able to regain root privileges, illustrating the above POSIX rule.

Correct privileges relinquishing order

The correct way of relinquishing privileges is to invert the previous setuid() and setgid() calls:

	if (setgid(getgid()) == -1)
		printf("setgid failed\n");
	if (setuid(getuid()) == -1)
		printf("setuid failed\n");

Recompiling:

# gcc -o drop_good drop_good.c 
# chmod +s drop_good
# ls -l drop_good
-rwsr-sr-x 1 root root 16352 Oct  1 10:24 drop_good

The following output is now obtained:

$ ./drop_good 
UID: 1000 GID: 1000 EUID: 0 EGID 0
UID: 1000 GID: 1000 EUID: 1000 EGID 1000
setgid failed
UID: 1000 GID: 1000 EUID: 1000 EGID 1000

The second setgid(0) calls fails because the saved set-group-ID is now no longer pointing to a privileged value and it thus cannot regain root privileges.

Impact and conclusion

In real-life situations, other than detecting a wrong order in set*id() functions in a program, an attacker would need to be able to execute code afterwards to call setgid() a second time with the appropriate privileged value. This can be achieved in numerous way such as via a buffer overflow, printf() bugs…

Combining this order issue with a code execution vulnerability will enable an attacker to escalate privileges to the ones of the initial running process.

One has to be careful when it comes to dropping privileges in Linux C programs as it is trickier than it seems. Man pages are not always enough information to do so, it can quickly be error prone and one can easily introduce vulnerabilities in its code.

References

Related Articles