0

For a program I'm developing, there's no reason why it should ever be run with setuid bit set - that always represents a configuration error on the part of the user. As part of defense in depth, I'd like to detect and fix that in the program.

As I understand that, to really do that, I have to call setuid twice, like this:

uid_t real_uid = getuid();
int ret = setuid(real_uid);
/* error checking here */
ret = setuid(real_uid);
/* error checking here */

As I understand it, if I only call setuid once, then the old effective UID would be in the saved set-user-ID, so any injected call to setuid (assume a vulnerability that somehow enables that) could be used to set the effective UID back to its original value. The second call to setuid puts the new value in the saved set-user-ID, fixing this. Is this a reasonable way to achieve this?

Alternatively, at least on Linux (in practice that's where this will always be run), I could use setresuid to do this all on one go:

uid_t real_uid = getuid();
int ret = setresuid(-1, real_uid, real_uid);
/* error checking here */

Is that (more) reasonable?

Does all of this apply to GIDs too? Do I need to worry about the supplementary group list at all (I don't really know what this is)?

Arthur Tacca
  • 8,833
  • 2
  • 31
  • 49

1 Answers1

0

Short answer

setuid() once is sufficient to go from root to another user and prevent setuid() back to root. It has no special effects when the other user isn't root.

Explanation

The concept you are talking about is a drop in privilege.

It is often that a service starts as root so it can open and listen on a socket with a port under 1024 (i.e. Apache2 has to open ports 80 and 443 as root before becoming www-data or some other user). Those sockets can only be opened by the root user. So the synopsis of such a service is:

... do some basic initialization ...
uid_t const me(getuid());
seteuid(0); // or setuid(0); -- it works either way in this direction
... socket()/bind() ...
setuid(me);
... listen() ...

The part between the setuid(0) and the setuid(me) should be as small as possible to make it as safe as possible.

To learn more about this I suggest you closely read the following two manual pages:

man setuid
man seteuid

(on some systems, it may be the same manual page, under Linux, it should be two separate pages).

The seteuid() clearly says it only changes the effective user ID.

The setuid() clearly says that going from root to another user is a one time event:

If the user is root or the program is set-user-ID-root, special care must be taken: setuid() checks the effective user ID of the caller and if it is the superuser, all process-related user ID's are set to uid. After this has occurred, it is impossible for the program to regain root privileges.

So checking the s bit and trying to prevent setuid() between users when not root has no special effects. It's really only from root (superuser) to another user.

Example

Create a file named a.cpp with:

#include <unistd.h>
#include <sys/types.h>

int main()
{
    uid_t const me(getuid());
    uid_t const other(0);   // try with another user and no errors happen

    // you can go back and forth any number of times with seteuid()
    for(int i(0); i < 5; ++i)
    {
        std::cout << "seteuid(): change to other: " << seteuid(other) << "\n";
        std::cout << "seteuid(): change back: " << seteuid(me) << "\n";
    }

    // however, with setuid(), if you drop privileges from root,
    // you can't come back
    for(int i(0); i < 5; ++i)
    {
        std::cout << "setuid(): change to other: " << setuid(other) << "\n";
        std::cout << "setuid(): change back: " << setuid(me) << "\n";
    }

    return 0;
}

Compile:

g++ -o a a.cpp

Change ownership and set the s bit of that executable:

sudo chown root a
sudo chmod u+s a

Finally execute:

./a

Here is the output when the other user is root:

seteuid(): change to other: 0
seteuid(): change back: 0
seteuid(): change to other: 0
seteuid(): change back: 0
seteuid(): change to other: 0
seteuid(): change back: 0
seteuid(): change to other: 0
seteuid(): change back: 0
seteuid(): change to other: 0
seteuid(): change back: 0
setuid(): change to other: 0
setuid(): change back: 0
setuid(): change to other: -1
setuid(): change back: 0
setuid(): change to other: -1
setuid(): change back: 0
setuid(): change to other: -1
setuid(): change back: 0
setuid(): change to other: -1
setuid(): change back: 0

As we can see, a single setuid() was sufficient to kill your ability to come back as root (whether with setuid() as presented here, or seteuid() and I will leave that as a test for you to verify I'm correct).

If the other user is set to a different user (other than root), then the rules change and you can't prevent the back and forth either way. Say you change the user identifier like so:

    uid_t const other(65534);   // the nobody user (verify ID in /etc/passwd)

Recompile and run a. The output has no -1. The switching back and forth works as expected, and you can try with as many setuid() in a raw as you like, it has no special effects. You could replicate this line to make double sure:

        std::cout << "setuid(): change back: " << setuid(me) << "\n";
        std::cout << "setuid(): change back: " << setuid(me) << "\n";
        std::cout << "setuid(): change back: " << setuid(me) << "\n";

Side Note

I do not use SELinux. The rules on that version of Linux may be slightly different. See details about the CAP_SETUID flag.

Alexis Wilke
  • 19,179
  • 10
  • 84
  • 156