PDA

View Full Version : Temporarily gain root privileges to perform open() on file



Absurd
05-01-2015, 05:00 AM
Hi.
I want to perform open() on a file to which I'm not privileged enough to open.
Up until now, I just ran the executable as root:


sudo ./server

This allowed me to open the file with no problems, of course, but now that the program gets bigger and bigger, I'd prefer to run it as a normal user, and elevate the privileges only for the open() command.
In other words, I want something like this:


int main(int argc, char* argv[]) {

// code...

gain_root_privileges();
open(FILE,O_RDONLY);
drop_root_privileges();

// code...
}

I read about the setuid (http://man7.org/linux/man-pages/man2/setuid.2.html), but I couldn't figure out how to use it for my purpose.
Any help would be greatly appreciated.

Nominal Animal
05-01-2015, 07:27 AM
When you do setuid root stuff, you better include some serious checks.

Only allow what you have to, and deny everything else.

(Allowing everything except things you know are insecure or wrong is absolutely the wrong approach. Won't work, and will bite you in the butt.)


#define _GNU_SOURCE
#include <unistd.h>
#include <string.h>
#include <stdio.h>

#ifndef TARGET_UID
#define TARGET_UID 0
#endif

#ifndef TARGET_GID
#define TARGET_GID 0
#endif

#ifndef UID_MIN
#define UID_MIN 500
#endif

#ifndef GID_MIN
#define GID_MIN 500
#endif

int main(int argc, char *argv[])
{
uid_t ruid, euid, suid; /* Real, Effective, Saved user ID */
gid_t rgid, egid, sgid; /* Real, Effective, Saved group ID */
int uerr, gerr, fd;

if (getresuid(&ruid, &euid, &suid) == -1) {
fprintf(stderr, "Cannot obtain user identity: %m.\n");
return EXIT_FAILURE;
}
if (getresgid(&rgid, &egid, &sgid) == -1) {
fprintf(stderr, "Cannot obtain group identity: %m.\n");
return EXIT_FAILURE;
}
if (ruid != (uid_t)TARGET_UID && ruid < (uid_t)MIN_UID) {
fprintf(stderr, "Invalid user.\n");
return EXIT_FAILURE;
}
if (rgid != (gid_t)TARGET_UID && rgid < (gid_t)MIN_GID) {
fprintf(stderr, "Invalid group.\n");
return EXIT_FAILURE;
}

/* Switch to target user. setuid bit handles this, but doing it again does no harm. */
if (seteuid((uid_t)TARGET_UID) == -1) {
fprintf(stderr, "Insufficient user privileges.\n");
return EXIT_FAILURE;
}

/* Switch to target group. setgid bit handles this, but doing it again does no harm.
* If TARGET_UID == 0, we need no setgid bit, as root has the privilege. */
if (setegid((gid_t)TARGET_GID) == -1) {
fprintf(stderr, "Insufficient group privileges.\n");
return EXIT_FAILURE;
}

/* ... privileged operations ... */

/* Open the restricted file.
* If 'filename' is specified by the calling user,
* in command-line parameters or environment variables,
* you may have handed them a way to read e.g. /etc/gpasswd.
* Don't do that.
* If you have to do that, a lot of additional checks are needed,
* some before opening the file, and others after opening the file.
* Even then it is risky (hard link races and such).
*/
do {
fd = open(filename, O_RDONLY | O_NOCTTY);
} while (fd == -1 && errno == EINTR);
if (fd == -1) {
fprintf(stderr, "Cannot open %s: %m.\n", filename);
return EXIT_FAILURE;
}

/* Drop privileges. */
gerr = 0;
if (setresgid(rgid, rgid, rgid) == -1) {
gerr = errno;
if (!gerr)
gerr = EINVAL;
}
uerr = 0;
if (setresuid(ruid, ruid, ruid) == -1) {
uerr = errno;
if (!uerr)
uerr = EINVAL;
}
if (uerr || gerr) {
if (uerr)
fprintf(stderr, "Cannot drop user privileges: %s.\n", strerror(uerr));
if (gerr)
fprintf(stderr, "Cannot drop group privileges: %s.\n", strerror(gerr));
return EXIT_FAILURE;
}

/* ... unprivileged operations ... */

return 0;
}
The UID_MIN and GID_MIN are defined as the lowest valid user and group IDs (in addition to root, 0:0). They protect against misuse via system accounts used by service daemons.

Compile the program normally. Then, install it as setuid root, with a sensible access mode. Assuming your program is named prog, and you want it installed in /usr/local/bin, use


sudo install -o root -g root -m u=rxs,g=rx,o=x -t /usr/local/bin prog


In many cases, it is safer to use a dedicated user and group for the access to that file. This way, you can have the program read that file on behalf of any normal user, but not read that file directly, nor can they exploit this program (even in case of a security break) to read other files (not owned/accessible to the dedicated user and group). Create such dedicated accounts as 'system' ones, i.e. with uids and gids under 500 or so (depends a bit on which Linux distribution you use).

The ruid != (uid_t)TARGET_UID && and rgid != (gid_t)TARGET_GID && parts allow execution by the privileged account. If you use a dedicated user and group for the privileged data, and the application is intended to be used by real users (and their scripts) only, you should replace these with ruid == (uid_t)TARGET_UID || and rgid == (uid_t)TARGET_GID instead, to explicitly deny running as the dedicated user and/or group.

If the dedicated account is user progu and group progg (typically you'd use the same name for both, of course), use


sudo install -o progu -g progg -m u=rxs,g=rxs,o=x -t /usr/local/bin prog

to install it. This time we set both setuid and setgid bits, because we wish the program to become progu user, progg group, for the privileged bit. (If you skip the group part, and you end up creating the file, the group depends on the directory it was created in (if the directory has the setgroup bit set) or the active group of the user who executed the program. Better be thorough.

Some distributions, like Ubuntu, use file capabilities for more fine-grained privilege separation, but if you just need protected privileged access to some file or directory, the setuid (and setgid if using non-root dedicated account for the privileged stuff) approach is better.

Questions?

Absurd
05-01-2015, 08:47 AM
Amazing, thanks for the great help, again. ;)
Yes, I have couple of questions.

1. Why is it necessary to elevate the egid as well? Why isn't euid enough?

2.

#ifndef TARGET_UID

#define TARGET_UID 0
#endif


Why was it important to use the preprocessor directive #ifndef here?
Is there a special reason?


3.

Compile the program normally. Then, install it as setuid root, with a sensible access mode. Assuming your program is named prog, and you want it installed in /usr/local/bin, use

sudo install -o root -g root -m u=rxs,g=rx,o=x -t /usr/local/bin prog

Why is it needed? What does it mean to "install the program as setuid root"?
Isn't compiling enough?

Nominal Animal
05-01-2015, 09:50 AM
Why is it necessary to elevate the egid as well? Why isn't euid enough?
Like I wrote above, egid is used when you create new files, directories, or other filesystem objects (named pipes, sockets visible in the file system, and so on).
The group of the thing created will be based on egid, unless the directory it is created in has the setgid bit (g+s) set.

If you make darn sure you create no new files or directories while privileged, then you could drop it. I wouldn't. I prefer to be careful.

Why was it important to use the preprocessor directive #ifndef here?
It is used to make adaptation to each system easier. That way, you can define the desired user and group id at compile time using gcc -DTARGET_UID=`id -u target_username` -DTARGET_GID=`id -g target_group` ... without changing the source code at all.

Similarly, you can set -DUID_MIN=1000 -DGID_MIN=1000 if the system happens to start assigning human users and groups at 1000 instead of 500.


Why is it needed? What does it mean to "install the program as setuid root"?
Isn't compiling enough?
Would you allow any program on your machine to become root, just because they want to?

We call that privilege escalation, and definitely do not want it to happen willy-nilly.

When you compile a program, it becomes a normal binary. The compiler does not care, and definitely does not know how to assign special privileges to that binary.

We use commands like chown, chgrp, and chmod to modify the owner user, owner group, and access mode of files. The install command is basically just a copy command, that includes all of those commands functionality, too.

What a program may or may not do, depends on the effective identity. In normal situations, this is the same as real identity, which is the identity of the user (account) that is running the program.

For normal programs, the mode is -rwxr-xr-x (0755) or -rwxrwxr-x (0775). The owner (and the group in the latter case) can write (modify) the binaries, but all users can read and execute them. When executing, the owner and group of the program does not matter, the effective identity of the user executing it controls filesystem access.

When a program is marked setuid (-rwsr-xr-x, 04755), executing the program borrows some of the privileges belonging to the owner of the program file! Similarly, setgid (-rwxr-sr-x, 02755) borrows the privileges belonging to the group owning the program file. Using both, -rwsr-sr-x (06755) grants privileges belonging to the user and the group. (Note, this only works for binaries, not for shell scripts or other scripts beginning with a #! line.)
Technically, only the effective identity is borrowed (from the program file owner and/or grou), and the real user stays intact, and only for the executed process, not to any other processes the real user might be running at the same time.

You wanted real users running the program to be able to read a file that is not available to those users. The simplest way to accomplish that is to set the owner and group of the program to a dedicated user (or root, although I recommend against root if this is all you need the privileges for), and set the setuid and setgid bits so that executing the program borrows the privileges from the owner and group.

In Linux, there are capabilities separate from the user identity, and they can be assigned to a file (if using the standard Linux filesystems) in a similar fashion. They're listed here (http://man7.org/linux/man-pages/man7/capabilities.7.html), for example. Some of them, like CAP_NICE, is useful if the program does time-critical stuff on a loaded system, as it allows it to raise its own priority (make it run more often than other processes get to, if necessary), and CAP_NET_BIND_SERVICE, which allows the program to use a IP port under 1024. For filesystem access checks, CAP_FOWNER bypasses all checks, and it's much more risky than just setuid+setgid. So, for filesystem access stuff, and stuff that should be portable across Unix-like/POSIX-y systems, setuid+setgid is better.

brewbuck
05-01-2015, 02:55 PM
It is possible to pass an open file descriptor across a UNIX socket.

Thus, you could split your program into two processes, connected by a UNIX socket. One process, running with root privs, opens the file, then passes the FD across the socket for the unprivileged program to use.

Nominal Animal
05-01-2015, 06:48 PM
Thus, you could split your program into two processes, connected by a UNIX socket. One process, running with root privs, opens the file, then passes the FD across the socket for the unprivileged program to use.

Good point, brewbuck: This is definitely recommended if the unprivileged program is complex. I routinely do this -- pass file descriptors as ancillary data using Unix domain sockets -- for security purposes, and it's not hard to do right. It just makes the security so much more robust, when you isolate the security-sensitive stuff into a separate program.

On the privileged program part, everything I mentioned above does apply. There is also a bit more added code, as your privileged program will want to require the unprivileged program to provide its credentials. It's not hard; there is a very similar method to descriptor passing that allows for passing the identity of the unprivileged part, verified and enforced by the kernel. It can even be used to verify which program (executable binary) the unprivileged part is, via the /proc pseudofilesystem. Although it sounds a lot, it is not that much code, and once you get it right, it's basically the same thing everywhere you implement it. Hmm, which reminds me, I really should write it into a separate file to be reused, and not respin it every time.

There are some attack vectors, for example a hostile unprivileged process contacting the privileged program and sending it hundreds of descriptors in order to make the privileged program crash, but these are pretty easily detected and countered. (It is annoying, though. I do understand why some Linux kernel developers consider the mechanism silly/flawed/badly designed; a push-pull way, where both ends should agree for the transfer before it can occur, would be much more robust. It just doesn't fit well with the sendmsg()/recvmsg() interface.)

phantomotap
05-01-2015, 07:58 PM
O_o

I third splitting the privileged bits into a second process.


@Nominal Animal: I've seen you reference some "DBUS" and "PolKit" software; do you know of any shipping primitives exposing the mechanism from a server?


Soma

Nominal Animal
05-01-2015, 09:52 PM
any shipping primitives exposing the mechanism from a server?
I'm not sure I can parse that sentence. :confused:

If you mean do I know if d-bus exposes the mechanism, then yes: d-bus has NEGOTIATE_UNIX_FD and AGREE_UNIX_FD (http://dbus.freedesktop.org/doc/dbus-specification.html#auth-command-negotiate-unix-fd) commands. PolKit uses d-bus. If I recall correctly, it is actually used, too.

But, d-bus is utter crap, and I hate it with a passion. It is a classic example of framework thinking: "let's create this hugely complicated, fragile system, that can solve all possible problems!"

Yeah, let's not solve anything, but create a solution factory instead!

Then, when we get bored at tinkering with it, and don't want to maintain the mess anymore, I know: we'll push it into the kernel! Yay!

Sigh.

A modular, asynchronous bus with authorization via tokens would be so much cleaner. (I have a novel approach for kdbus, but I'm not sure if I'm mentally ready for the kind of crapstorm one can get at LKML if the approach turns out to have fatal faults. It'd be very useful for machine-local MPI, too; wouldn't need to use a bunch of threads just to implement real async comms.)

If you are working on something that needs to pass one or more file descriptors reliably, I might help with an implementation. I haven't seen it alone exposed by any interfaces, since it's almost always a part of some other protocol, instead of used alone. Typical use cases are HTTP servers and such, and involve either peeked HTTP connections (httpx, uses socket options to sniff at the Host: header), or one of the gateway protocols (CGI, FastCGI, WSGI, SGI).

The funny thing here is that Linux (including Android) actually has per-thread credentials. That is, each thread in a server could use separate credentials and capabilities for filesystem access, and privilege separation between threads in the same process. In a very real sense this would allow just-about-perfect balance between complexity and actual, working, privilege separation -- excluding script interpreters in the same process, of course; they don't play ball. The only thing is, the C library does not expose that, and actually always sets the capabilities and credentials process-wide, using a realtime interrupt as far as I remember. To get at this, one would need their own C library implementation, it's that deeply intertwined in glibc at least.

phantomotap
05-01-2015, 10:57 PM
I'm not sure I can parse that sentence.

O_o

Well, you were pretty near the mark. You basically answered the question with "I haven't seen it alone exposed by any interfaces, since it's almost always a part of some other protocol, instead of used alone.", but I'll elaborate a bit to remove the terseness of the jargon.

I was asking if you knew of a library providing a simple interface such as `pcmopen' which does the work of the local security measures before communicating with a separately installed server to grab a privileged file descriptor.

The task seemed common enough that a generic implementation had likely been packaged for common installation. I just didn't know of any such library and server.

I'm really not sure why I involved "DBUS" and "PolKit" in my question other than I assumed they would likely be involved in a currently shipping implementation.


If you are working on something that needs to pass one or more file descriptors reliably, I might help with an implementation.

I appreciate the offer, but I prefer to use normal permissions or FACL (http://en.wikipedia.org/wiki/Access_control_list) with a single `setuid'/`setgid' during initialization according to the user/group listed in a configuration file before spawning the actual application. I can always ship the source for the controller with such an approach. I was eaten alive for having the gall to ship a commercial root daemon many years ago so I'm wary of shipping anything which needs root these days.

Soma

Nominal Animal
05-02-2015, 12:32 AM
I was asking if you knew of a library providing a simple interface such as `pcmopen' which does the work of the local security measures before communicating with a separately installed server to grab a privileged file descriptor.
Now I understand. No, none that I know of. The descriptor passing also requires nonzero amount of non-ancillary data (although I guess you could use a single dummy byte), but typically, it's passed with data specific to the protocol used.

Unfortunately, most of the implementations also lack the security features. For example, one known attack vector involves an exec race between checking the credentials, and passing the descriptor.


I was eaten alive for having the gall to ship a commercial root daemon many years ago so I'm wary of shipping anything which needs root these days.
I developed a robust method of running CmapServer as a dedicated unprivileged user in Linux (and it's been used here for years). Last time I checked, the official Linux instructions still include "install this closed-source binary Java blob as root, and reboot your server." Yeah, not gonna happen on any server I'm an admin on.

For about a decade, I've been pushing a group-based admin scheme at the local University. Basically avoids 'sudo' stuff completely (except when the individuals concerned go to the extra mile of blocking others by modifying group access rights by hand), and to me, is clear and simple. For others, they say it's confusing and weird (because they're so accustomed to starting each ssh session with sudo su - ). For services, I push, very hard, for dedicated unprivileged system accounts and groups. Even if compromised, you'd still need an unpatched local privilege escalation exploit to become root.

Remember, none of my points above are specific to root; I apply these principles whenever the privileged account is one of those non-root system accounts, too.

I suspect that there are no HTTP servers that exploit these mechanisms to their fullest extent, because the developers want their servers to be able to run on non-POSIXy machines, too.

Sure, portability is a very nice thing, but when it limits the approaches -- especially security and efficiency-related approaches -- critical service daemons can use, it doesn't sound too good to me. It's like thinking about the fact that a lot of the datasets produced in CERN, are stored and managed for end user researchers using large multi-node storage server boxes targeting rates calculated in gigabytes per second, sustained, across hundreds of concurrent clients, attempting to guarantee a minimum bandwidth to each client.. and these servers run Java.

phantomotap
05-02-2015, 01:08 AM
Yeah, not gonna happen on any server I'm an admin on.

>_<

Yeah. I was a kid. I didn't really appreciate just how foolish you'd need to be to run unknown code as root at machine level.


For about a decade, I've been pushing a group-based admin scheme at the local University.

I've been slowly working my way to a completely--as much as is possible because bugs are things that exist--hardened server.

I use ACL for to populate permissions. I don't even use groups because that one cowboy is always around.

I learned that the hard way too...

Soma

Nominal Animal
05-02-2015, 02:20 AM
I use ACL for to populate permissions. I don't even use groups because that one cowboy is always around.
That is one working approach. I don't like it personally, because it pushes for personality-oriented workplace; place where you have to ask "who should I talk to about X" all the time. I prefer clearly labeled roles. Stuff like having areas you're responsible for listed in the internal directory; not having to go ask a secretary or boss or somebody to find me somebody else to talk about. On servers, permission-granting supplementary groups work well for such roles.

I don't like those person-to-person oriented workplaces; in my experience they're festering holes of social games and exploitation. It is almost as if humans need to know rules exist, before they even start thinking about whether the stuff they do is okay or not. In a person-oriented workplace, such decisions are made by thinking whether someone higher up in the food chain would object to the actions. And that produces inferior results in practice, compared to when each person actually thinks about the things (instead of other people) to accomplish their task. You know, "will this work?" instead of "will X like it if I do this this way?"

I've looked into transparent version management for configuration files, and I've made test examples that do that quite reliably but only for specific files, using file leases (even tracing real user behind sudo su - tricks). Yeah, the tracking daemon will keep an open file descriptor on each specific file.. but it does work. I was hoping fanotify interface would provide the requisite interception capabilities to do that sanely instead. That was a few years ago, it might be sufficient nowadays.

With something like that, catching the cowboys by implementing something like blame FILENAME would make me happy. Also gives stuff like version rollback and whatnot. Configuration files don't take that much space, even if you stored each modified version instead of deltas.

phantomotap
05-02-2015, 03:52 AM
That is one working approach. [...] And that produces inferior results in practice, compared to when each person actually thinks about the things (instead of other people) to accomplish their task.

O_o

I clearly overstated my purpose, and I completely agree with your criticism.

I do use the normal user, group, and other permissions for certain traditional roles. I also give some users/groups additional permissions or revoke certain permissions with ACL tools instead of adding and removing groups which must be managed with the specific combination of permissions required.

Example: I have a `wheel' group, as is largely common, which owns the server. I also have a `witch' group which manages daemons--because of course they do--which implies a lot of permissions including configuring packages thus writing the "/etc" directory or at least files within the "/etc" directory. I could give ownership of "/etc" to the `witch' group and place everyone in the `wheel' group also in the `witch' group. (I could even add the relevant users automatically in several ways.) However, I also have the `mechanic' role needing to manage "/etc" which aren't really responsible for managing daemons or the server. (The `mechanic' role may even be unique to my boxes. The `mechanic' role has the permissions necessary to fix most any configuration mistake, but they can't add or subtract anything from the system.) I can't just give groups ownership of specific files because the roles overlap in at least a few cases. (I could theoretically create a fourth group with the all the relevant users, but the approach begins to border on "security through obscurity" by hiding which members of the group are actually supposed to be responsible for a given file, and the fourth group actually grants permissions to inappropriate users.) The "/etc" directory and all "/etc" files is owned by `root:wheel'. The majority of files in "/etc" have FACL permissions for the `mechanic' group. The "/etc" files associated with most publicly accessible daemons have FACL permissions for the `witch' group. A few files, notably "/etc/ssh/sshd_config" and "/etc/sudoers", have FACL permissions set to deny being directly written even by users in the `wheel' group to prevent a poorly written file from making the system inaccessible.


I've looked into transparent version management [...] each modified version instead of deltas.

Have you tried using the "auditd" package and watching the generated log?

You wouldn't get versioning, but I'd think you were doing something wrong if storing every version of the configuration files ever became a problem.

Soma

Nominal Animal
05-02-2015, 04:38 AM
Example:
Ah, that makes sense; my approach is pretty similar for the security-sensitive stuff. (I can see how some feel it is a bit tedious to have to use e.g. getfacl to see the full set, but I don't mind much. I like the extra + appended to the access mode in ls output when ACLs are present, it tends to be enough for me as an indicator.)

For stuff like project file areas (typically project sites on a web server), I prefer a per-project group that owns that part of the tree, with setgid bit set on directories (so all files created will be owned by the per-project group), and umasks set in system-wide profile files to 002 (allow group write by default) if the user is a member of any of those per-project groups.

In practice, you do need to provide certain user-executable scripts that are installed to grant them extra privileges in order to fix the permissions in the specified tree. Extracting archives using default options typically override the permissions that would be normally be given to the files and directories created.

The nice thing is, that although all users in such a group can modify each others' files (although you might need to use one of said scripts to fix things if somebody is being an idiot), the owner user tells who created the file.


Have you tried using the "auditd" package and watching the generated log?
Of course. It tells us who did the changes, unless the user started their ssh session with sudo su - as auditd does not do the process tree tracing needed to find out the actual non-root user responsible.

wtmp and security logs also help a little, but it is not rare for people here to leave their ssh sessions open, while being root, so you tend to have more than one blame candidate for each change. (The workstations are behind locked doors and password locks, so it's not that bad. I don't do it myself, though, and I do wish others would stop doing it, too. But, as they say, I'm the paranoid one.)

The event handlers you can attach to auditd only receive the events; they get no vetting rights. auditd itself can vet actions, so it would be possible to make it delegate specific audit decisions to another process. Or you could rewrite the entire thing, of course.

As it is, auditd is already on the very complex side, and a very useful tool as is, and I definitely didn't want to try and add more complexity to it. Waiting for fanotify to grow the necessary warts, and support to trickle down to the server distros (CentOS and ScientificLinux) was the sensible option. It's not like anybody was paying for me to do this. On a first glance, all the fanotify bits I think I'd need for this have been there since 3.8 or 3.9 kernels, meaning support should now exist in typical servers.

phantomotap
05-02-2015, 05:29 AM
@moderators: The posts (#8 (http://cboard.cprogramming.com/linux-programming/166827-temporarily-gain-root-privileges-perform-open-file.html#post1231148)-#15 (http://cboard.cprogramming.com/linux-programming/166827-temporarily-gain-root-privileges-perform-open-file.html#post1231175)) from Nominal Animal and I wonder far from the topic so I'm requesting a split.


Waiting for fanotify to grow the necessary warts, and support to trickle down to the server distros (CentOS and ScientificLinux) was the sensible option. It's not like anybody was paying for me to do this. On a first glance, all the fanotify bits I think I'd need for this have been there since 3.8 or 3.9 kernels, meaning support should now exist in typical servers.

O_o

The conversation sparked a renewed interest in doing the version right so I threw some more things at a search.

I found "etckeeper (https://github.com/joeyh/etckeeper)" which I'll have to explore tomorrow.

Soma

Absurd
05-02-2015, 01:31 PM
This is definitely recommended if the unprivileged program is complex. I routinely do this -- pass file descriptors as ancillary data using Unix domain sockets -- for security purposes, and it's not hard to do right. It just makes the security so much more robust, when you isolate the security-sensitive stuff into a separate program.

My program is very simple, and the only thing that requires elevation is one command - open(FILE_NAME, O_RDONLY), where FILE_NAME is not given at runtime, so I think I'll stick with what you suggested in your first post in this thread.

Absurd
05-02-2015, 02:32 PM
I just tried to write an example, but I'm a bit confused about the output...


int main(int argc, char* argv[]) {

uid_t ruid, euid, suid;
gid_t rgid, egid, sgid;
int uerr, gerr;

printf("euid: %d, egid: %d\n", geteuid(), getegid());

/* elevate privileges */
if (getresuid(&ruid, &euid, &suid)==-1) {
fprintf(stderr, "Could not get: %m.\n");
return -1;
}

if (getresgid(&rgid, &egid, &sgid)==-1) {
fprintf(stderr, "Could not get: %m.\n");
return -1;
}

if (seteuid((uid_t)0) == -1) {
fprintf(stderr, "Insufficient user privileges.\n");
return -1;
}

if (setegid((uid_t)0) == -1) {
fprintf(stderr, "Insufficient user privileges.\n");
return -1;
}

/* privileged command */
/* ... */

printf("euid: %d, egid: %d\n", geteuid(), getegid());


/* drop privileges */
gerr = 0;
if (setresgid(rgid, rgid, rgid) == -1) {
gerr = errno;
}

uerr = 0;
if (setresuid(ruid, ruid, ruid) == -1) {
uerr = errno;
}

if (uerr || gerr) {

if (uerr) {
fprintf(stderr, "User: %s.\n", strerror(uerr));
}

if (gerr) {
fprintf(stderr, "Group: %s.\n", strerror(gerr));
}

return -1;
}

printf("uid: %d, gid: %d\n", geteuid(), getegid());

return 0;
}
(I skipped a few tests you made, to make the example clearer)

I compiled it:


gcc -Wall -o example example.c

and installed:


sudo install -o root -g root -m u=rxs,g=rx,o=x -t temp example

But when I run it (NOT as root) the output is:


euid: 0, egid: 1000
euid: 0, egid: 0
uid: 1000, gid: 1000

But shouldn't it be:


euid: 1000, egid: 1000
euid: 0, egid: 0
uid: 1000, gid: 1000

?

phantomotap
05-02-2015, 03:32 PM
/* elevate privileges */

O_O

NOPE.

The "elevate privileges" comment says you need to stop what you are trying to do now.

You just do not know enough about what you are doing to safely shift privileges.

In order for `seteuid' and `setegid' to work in the first place, you require the privileges to change into the relevant user and group. When you install with the SUID bit set, you are telling the system to execute the application with the effective user identifier equal to that of the owner of the executable. When you install with the SGID bit set, you are telling the system to execute the application with the effective group identifier equal to that of the owning group of the executable. When you followed the instructions Nominal Animal provided, you set SUID bit and the owner of executable to the `root' user; you are not elevating to running with `root' because the application started with `root' privileges.

When you write SUID or SGID applications, you need to temporary drop privileges as soon as possible so that you only have the privileges of the actual user and group running the application. When you actually need the SUID or SGID privileges, you will temporary restore privileges to match that of the SUID and/or SGID owner and/or group. As soon as is possible, you should consider permanently dropping privileges to that of the actual user and group running the application to prevent mistakes later in execution.

Soma

Absurd
05-03-2015, 12:31 AM
O_O

NOPE.

The "elevate privileges" comment says you need to stop what you are trying to do now.

You just do not know enough about what you are doing to safely shift privileges.

Maybe I should have started with saying that I'm not as well familiar as you with linux permissions.
I guess I'll need to read some more about the subject and try again.
Thanks for the help, you could have spare the condescending comment, though.

Nominal Animal
05-03-2015, 03:10 AM
I just tried to write an example, but I'm a bit confused about the output...

No worries. I understand exactly why Phantomotap is worried, but I also know that you're trying very hard to learn how to do this stuff right, and I happen to ascribe to the "reading background materials is important to get a good understanding, but experimentation gives you intuition and practical experience" camp, which is why I see no harm in doing stuff like this. You are aware of the security implications, and will not use this in a real world application before you're ready, I'm sure.

Consider this example code, let's say identity.c:


#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <grp.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>

/* Obtain user information based on uid, thread-safe.
* Returns 0 if successful, nonzero errno error otherwise.
* If non-NULL, the pointers will be filled in with the info,
* strings will be dynamically allocated, free()able.
*/
int get_user_info(const uid_t uid,
gid_t *const gid,
char **const name,
char **const dir,
char **const gecos,
char **const shell)
{
struct passwd info, *result;
char *data;
size_t size = 8192;
int err;

if (gid) *gid = (gid_t)-1;
if (name) *name = NULL;
if (dir) *dir = NULL;
if (gecos) *gecos = NULL;
if (shell) *shell = NULL;

while (1) {
/* Do not allow ridiculous passwd entry sizes. */
if (size >= 131072)
return errno = ENOMEM;

data = malloc(size);
if (!data)
return errno = ENOMEM;

do {
err = getpwuid_r(uid, &info, data, size, &result);
} while (err == EINTR);

/* Buffer too small? */
if (err == ERANGE) {
free(data);
size += 8192;
continue;
}

/* Other error? */
if (err) {
free(data);
return errno = err;
}

/* Paranoid check. (C library interposed, or something.) */
if (info.pw_uid != uid) {
free(data);
return errno = EIO;
}

/* Copy desired fields. */

if (gid)
*gid = info.pw_gid;

if (name && info.pw_name && info.pw_name[0]) {
*name = strdup(info.pw_name);
if (!*name) {
free(data);
return errno = ENOMEM;
}
}

if (dir && info.pw_dir && info.pw_dir[0]) {
*dir = strdup(info.pw_dir);
if (!*dir) {
free(data);
return errno = ENOMEM;
}
}

if (gecos && info.pw_gecos && info.pw_gecos[0]) {
*gecos = strdup(info.pw_gecos);
if (!*gecos) {
free(data);
return errno = ENOMEM;
}
}

if (shell && info.pw_shell && info.pw_shell[0]) {
*shell = strdup(info.pw_shell);
if (!*shell) {
free(data);
return errno = ENOMEM;
}
}

free(data);
return 0;
}
}

/* Obtain group information based on gid, thread-safe.
* Returns 0 if successful, nonzero errno error otherwise.
* If non-NULL, name will be set to a dynamically allocated
* string (free()able).
*/
int get_group_info(const gid_t gid,
char **const name)
{
struct group info, *result;
char *data;
size_t size = 8192;
int err;

if (name) *name = NULL;

while (1) {
/* Do not allow ridiculous group entry sizes. */
if (size >= 1073741824)
return errno = ENOMEM;

data = malloc(size);
if (!data)
return errno = ENOMEM;

do {
err = getgrgid_r(gid, &info, data, size, &result);
} while (err == EINTR);

/* Buffer too small? */
if (err == ERANGE) {
free(data);
size += 8192;
continue;
}

/* Other error? */
if (err) {
free(data);
return errno = err;
}

/* Paranoid check. (C library interposed, or something.) */
if (info.gr_gid != gid) {
free(data);
return errno = EIO;
}

/* Group name wanted? */
if (name && info.gr_name && info.gr_name[0]) {
*name = strdup(info.gr_name);
if (!*name) {
free(data);
return errno = ENOMEM;
}
}

free(data);
return 0;
}
}

int main(void)
{
uid_t euid, ruid, suid;
gid_t egid, rgid, sgid;

char *effective_user = NULL, *real_user = NULL, *saved_user = NULL;
char *effective_group = NULL, *real_group = NULL, *saved_group = NULL;

if (getresuid(&ruid, &euid, &suid) == -1) {
fprintf(stderr, "Cannot obtain user identity: %m.\n");
return EXIT_FAILURE;
}

if (getresgid(&rgid, &egid, &sgid) == -1) {
fprintf(stderr, "Cannot obtain group identity: %m.\n");
return EXIT_FAILURE;
}

if (get_user_info(euid, NULL, &effective_user, NULL, NULL, NULL)) {
fprintf(stderr, "Cannot obtain effective user %ld information: %m.\n", (long)euid);
return EXIT_FAILURE;
}

if (get_user_info(ruid, NULL, &real_user, NULL, NULL, NULL)) {
fprintf(stderr, "Cannot obtain real user %ld information: %m.\n", (long)ruid);
return EXIT_FAILURE;
}

if (get_user_info(suid, NULL, &saved_user, NULL, NULL, NULL)) {
fprintf(stderr, "Cannot obtain saved user %ld information: %m.\n", (long)suid);
return EXIT_FAILURE;
}

if (get_group_info(egid, &effective_group)) {
fprintf(stderr, "Cannot obtain effective group %ld information: %m.\n", (long)egid);
return EXIT_FAILURE;
}

if (get_group_info(rgid, &real_group)) {
fprintf(stderr, "Cannot obtain real group %ld information: %m.\n", (long)rgid);
return EXIT_FAILURE;
}

if (get_group_info(sgid, &saved_group)) {
fprintf(stderr, "Cannot obtain saved group %ld information: %m.\n", (long)sgid);
return EXIT_FAILURE;
}

printf("Effective identity for access control:\n");
printf("\t uid = %ld\n", (long)euid);
printf("\t gid = %ld\n", (long)egid);
printf("\t user = %s\n", effective_user);
printf("\tgroup = %s\n", effective_group);
printf("\n");

printf("Real identity:\n");
printf("\t uid = %ld\n", (long)ruid);
printf("\t gid = %ld\n", (long)rgid);
printf("\t user = %s\n", real_user);
printf("\tgroup = %s\n", real_group);
printf("\n");

printf("Saved identity, allowed to switch effective or real identity to:\n");
printf("\t uid = %ld\n", (long)suid);
printf("\t gid = %ld\n", (long)sgid);
printf("\t user = %s\n", saved_user);
printf("\tgroup = %s\n", saved_group);
printf("\n");

/* No need to free() these, since we are about to exit,
* but it definitely is safe. */
free(effective_user);
free(effective_group);
free(real_user);
free(real_group);
free(saved_user);
free(saved_group);

return EXIT_SUCCESS;
}

The get_user_info() and get_group_info are more complicated than this program would need, but I wanted to show how to obtain such information in a thread-safe manner. getpwuid() (http://man7.org/linux/man-pages/man3/getpwuid.3.html) and getgrgid() (http://man7.org/linux/man-pages/man3/getgrgid.3.html) certainly suffice in a program like this (as long as you remember to copy the fields, as successive calls will overwrite them), but .. well, I wanted to show those, too.

Anyway, let's compile the above and install some privileged copies of it:


gcc -Wall -Wextra -O2 identity.c -o identity
sudo install -o root -g root -m u=rwxs,g=rx,o=x identity identity-setuid
sudo install -o root -g root -m u=rwx,g=rxs,o=x identity identity-setgid
sudo install -o root -g root -m u=rwxs,g=rxs,o=x identity identity-setuid-setgid


Running plain ./identity shows the identity of the running user, as it should:


$ ./identity
Effective identity for access control:
uid = 1001
gid = 1001
user = nominal-animal
group = nominal-animal

Real identity:
uid = 1001
gid = 1001
user = nominal-animal
group = nominal-animal

Saved identity, allowed to switch effective or real identity to:
uid = 1001
gid = 1001
user = nominal-animal
group = nominal-animal


Running the setuid root version, we can see we borrow the root user identity and associated privileges. Carefully note how the group identity is still always the same as the real group identity:


$ ./identity-setuid
Effective identity for access control:
uid = 0
gid = 1001
user = root
group = nominal-animal

Real identity:
uid = 1001
gid = 1001
user = nominal-animal
group = nominal-animal

Saved identity, allowed to switch effective or real identity to:
uid = 0
gid = 1001
user = root
group = nominal-animal


Running the setgid version should be pretty obvious by now:


Effective identity for access control:
uid = 1001
gid = 0
user = nominal-animal
group = root

Real identity:
uid = 1001
gid = 1001
user = nominal-animal
group = nominal-animal

Saved identity, allowed to switch effective or real identity to:
uid = 1001
gid = 0
user = nominal-animal
group = root


Finally, the setuid-setgid version shows how to fully change the effective identity:


Effective identity for access control:
uid = 0
gid = 0
user = root
group = root

Real identity:
uid = 1001
gid = 1001
user = nominal-animal
group = nominal-animal

Saved identity, allowed to switch effective or real identity to:
uid = 0
gid = 0
user = root
group = root


Remember, the above code has no setuid()/seteuid()/setresuid() etc. calls at all; everything that is granted is granted by the kernel, and the code itself does not try to get more privileges at all.

Phantomotap and I discussed how important it is to try and avoid root if at all possible. This is simply because root can do anything and everything.

In particular, the root user is allowed to change their identity freely. This is why programs that need root access, are installed as setuid root, not setuid and setgid root. The setgid bit is just unnecessary, as the root user is omnipotent already.

This becomes much safer, and much, much more useful if you do not use root for the above, but a dedicated system account instead. You can use

getent passwd
to obtain the user list. Those with uids and gids below 500 or 1000 (it is configurable, and varies from system to system -- I remember when it used to be just 100!) are system accounts, not "human users".

System accounts are typically set up to forbid any sort of logging in, although in some cases allowing ssh connections via keys, for scripted/automatic data transfers, is useful. They do not have any magic powers, only root does, so they're perfect for restricting access to some piece of information.

The system group associated can also be used to control the access to said piece of information to select user accounts. For example, if you had system user project-x and system group project-x -- often shortened to project-x:project-x -- you could create a restricted directory using


sudo install -o project-x -g project-x -m u=rwx,g=rwxs,o= -d /var/lib/project-x/

For directories, the setgid bit (the s in the g= part) means that any files and directories created in that directory will inherit the group from the directory, not the user doing the creating; i.e. all files and subdirectories will automatically be owned by the project-x group.

Now, an application that is run by users, that requires access to that directory, can be installed as setgid project-x (if it only needs read access), or setuid+setgid project-x if it should be able to create new files there as the project-x user too.

Let's say we also have human project administrators that should have access there. I prefer to add them to the group, using

sudo usermod -a -G project-x nominal-animal
but Phantomotap prefers to use access control lists,

sudo setfacl -m u:nominal-animal:rwx -m d:u:nominal-animal:rwx /var/lib/project-x
where the second adds a default, allowing automatic access to any files and directories created there by others.

(Phantomotap, do correct me if I misrepresented your position here.)

Both are very good approaches, and the reason I prefer the groups is more to do with human psychology -- roles versus persons -- than anything else.

Well, maybe I could claim that having a centralized control, and being able to list all the admins using a single command,

getent group | sed -ne 's|^project-x:[^:]*:[^:]*:||p'
might make things easier, but I'm not convinced of that, either. For example, in student project websites, there's often some exceptions they'd like to make, so you end up using ACLs anyway.

Nominal Animal
05-03-2015, 04:04 AM
My program is very simple, and the only thing that requires elevation is one command - open(FILE_NAME, O_RDONLY), where FILE_NAME is not given at runtime
In that case, my suggestion is to create a dedicated system group for that file, and put it into a directory owned by root and that group, with the group only allowed to access (traverse) the directory:


sudo groupadd -r restricted
sudo install -o root -g restricted -m u=rwx,g=x,o= -d /var/lib/restricted
sudo install -o root -g restricted -m u=rwx,g=rx,o= /var/lib/restricted/protected-file


If there is a specific user or group that owns that directory and file -- for example, it contains some kind of configuration information for a service, and is owned by the user and group belonging to that service, I'd use access control lists instead, adding traversal and read access for the restricted group:


sudo groupadd -r restricted
sudo setfacl -m g:restricted:x /var/lib/restricted
sudo setfacl -m g:restricted:r /var/lib/restricted/protected-file


Which of the two approaches above you choose, depends on who the protected-file actually belongs to.

Note that no users should belong to the restricted group, ever. It exists only to convey read access to that file, via your program.

The beginning of your program needs to be utterly paranoid. example.c:


#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>

#ifndef FILE_GID
#error Use -DFILE_GID="`id -g file-owner-group`"
#endif
#ifndef FILE_NAME
#error Use -DFILE_NAME='"/absolute/path/to/file"'
#endif

#ifndef UID_MIN
#define UID_MIN 500
#endif
#ifndef GID_MIN
#define GID_MIN 500
#endif

int main(int argc, char *argv[])
{
uid_t ruid, euid, suid;
gid_t rgid, egid, sgid;
int fd;

/* Obtain current full identity. */
if (getresuid(&ruid, &euid, &suid) == -1) {
fprintf(stderr, "Cannot obtain user identity: %m.\n");
return EXIT_FAILURE;
}
if (getresgid(&rgid, &egid, &sgid) == -1) {
fprintf(stderr, "Cannot obtain group identity: %m.\n");
return EXIT_FAILURE;
}

/* Verify we have sufficient access. */
if (egid != (gid_t)FILE_GID) {
fprintf(stderr, "Insufficient privileges.\n");
return EXIT_FAILURE;
}

/* Verify user identity is acceptable.
* Unless setuid shenanigans are afoot, they all match. */
if (ruid < (uid_t)UID_MIN || euid < (uid_t)UID_MIN || suid < (uid_t)UID_MIN ||
ruid != euid || euid != suid) {
fprintf(stderr, "Access not allowed for this user.\n");
return EXIT_FAILURE;
}

/* Verify group identity is acceptable.
* Saved group must match either real or effective, too. */
if (rgid < (gid_t)GID_MIN || rgid == (gid_t)FILE_GID || (sgid != rgid && sgid != egid)) {
fprintf(stderr, "Access is not allowed for this group.\n");
return EXIT_FAILURE;
}

/* Open FILE. */
do {
fd = open(FILE_NAME, O_RDONLY | O_NOCTTY | O_CLOEXEC);
} while (fd == -1 && errno == EINTR);
if (fd == -1) {
/* Note: Users do not need to know the name of FILE. */
fprintf(stderr, "Configuration file: %m.\n");
return EXIT_FAILURE;
}

/* Change back to real group identity. */
if (setresgid(rgid, rgid, rgid) == -1) {
fprintf(stderr, "Cannot drop group privileges: %m.\n");
return EXIT_FAILURE;
}

/*
* We have fd open. It is read-only and close-on-exec,
* so that you do not accidentally leak the descriptor
* to whatever commands you might (fork and) execute.
*/
printf("Success, descriptor %d is open.\n", fd);
fflush(stdout);

return EXIT_SUCCESS;
}


We compile and install it as /usr/local/bin/example as setgid restricted. I also prefer to strip the binary (from excess symbols), and make it non-readable:


gcc -DFILE_NAME='"/var/lib/restricted/protected-file"' -DFILE_GID="`id -g restricted`" -Wall -Wextra -fomit-frame-pointer -O2 example.c -s -o example
sudo install -o root -g restricted -m u=rwx,g=xs,o=x -t /usr/local/bin example


When executed, /usr/local/bin/example automatically receives the restricted group privileges. The effective group identity is sufficient to grant access to the /var/lib/restricted/protected-file file. It does not allow listing the files in that directory, creating new files or directories there, or modifying the file.

Immediately after the file is opened, the program reverts back to the real group identity. To avoid leaking the open descriptor, I added the O_CLOEXEC open flag, which means that if you (fork and) execute, the kernel closes the descriptor automatically. Safety first, and paranoia rules, I say.

There is nothing magic in the restricted group, other than being the access control mechanism protecting the /var/lib/restricted directory and the /var/lib/restricted/protected-file file. Unlike root, there are no extra privileges associated.

This means that even if your program was compromised, all that the attacker would gain, would be the traverse access to that directory, and read access to that file, and nothing else. (This is a big, important thing to realize; while not directly stated, it underlines the entire security-related discussion I had with Phantomotap above: we limit the reach of compromised security by compartmentalizing stuff. We do not consider root - non-root, privileged - non-privileged; we try to limit each tool/service/daemon/user/group to the access and privileges it needs to get its work done, but no more.)

Although setuid and setgid binaries are looked with a very suspicious eye, this approach above -- assuming the restricted group is not used for some other system stuff too, and is better named to reflect what the heck it is related to -- is an accepted privilege separation pattern, and if explained in the documentation, would be acceptable to upstream Linux distributions too.

The only "trick" needed is to thoroughly document how the setgid bit is relied upon to restrict access to that file, what checks and limitations (GID_MIN, UID_MIN) are done in practice to catch nefarious usage, and so on. Condense the stuff I've written in this thread to a SECURITY readme file in the project root directory, and you should have no trouble with the approach. Obviously, I'd do this even for an utility internal to your organization.

Note: I have not vetted the above code for errors. Normally, I'd have a second person to go through the security-sensitive parts, just in case I missed something, and talk it out. It's worth doing that. Obviously, this is just example code, so I might have mistyped something. I'm pretty sure about the approach and logic of it, though.

Questions? Comments?

Absurd
05-03-2015, 06:39 AM
As always, I couldn't ask for more.
This is a lot, and as before, it will take me a while to read it carefully and thoroughly to understand everything, but for now it seems like all the information I need is here.
If any thoughts or questions will come up I'll post back.
Thanks again for your time and great effort!

Absurd
05-08-2015, 07:31 AM
OK, I see now what was wrong with my comment above...
I have another question, and if it sounds silly, please bare with me.

In what situations can a call to setuid() (http://man7.org/linux/man-pages/man2/setuid.2.html) be effective or useful?
I mean, calling setuid() sets the uid of the calling process. Meaning - the process is already running, isn't it too late for setuid?
To be more precise, if a setuid program enables any user with the right permissions (say, user_A) to run it with the privileges of its owner (say, root), shouldn't root must set the setuid bit beforehand ('beforehand' - before the program starts running) with the install command-line utility, the way you did in the above example?
How else can it be done with the function setuid()?
For example, suppose there is a file a.out with permissions -rwxrwxr-x owned by root.
Further suppose that root wants to let user_A run it with his (the 'his' is referring to 'root') permissions.
So, if root adds a function call in the code, at the start of the program (or wherever), to setuid() in order to allow user_A to run it with his (i.e root) permissions, but only he (i.e root) has the permissions to call setuid(), then this program is useless to anyone else, and to user_A in particular.
This leaves root with the only option of removing the call to setuid() from his code, and set it beforehand like you showed with the install command, doesn't it?

Nominal Animal
05-08-2015, 09:29 AM
In Linux (and on all POSIXy systems that have the POSIX _POSIX_SAVED_IDS feature), there are three identities: Real, Effective, and Saved. I've explained how these are used in post #4 (http://cboard.cprogramming.com/linux-programming/166827-temporarily-gain-root-privileges-perform-open-file.html#post1231100) in this thread.

The key point is that a process is allowed to set their real identity to any of real, effective, or saved identities.

The setuid bit on an executable binary tells the kernel to automatically change the effective user identity to the owner user of the file. The setgid bit on an executable binary tells the kernel to automatically change the effective group identity to the owner group of the file.

(Yeah, we really should call them "seteuid" and "setegid" bits instead, to be exact in our naming..)

setuid() (http://man7.org/linux/man-pages/man2/setuid.2.html) changes the real user identity of a process, and setgid() (http://man7.org/linux/man-pages/man2/setgid.2.html) changes the real group identity of a process.

See? setuid() operates on real user identity, while the setuid bit on an executable file tells the kernel to change the effective user identity when executing that file!


To be more precise, if a setuid program enables any user with the right permissions (say, user_A) to run it with the privileges of its owner (say, root), shouldn't root must set the setuid bit beforehand ('beforehand' - before the program starts running) with the install command-line utility, the way you did in the above example?
Yes, but the setuid bit on the executable causes only the effective user identity to become the owner user of the executable file; setuid() changes the real user identity. They are two separate things, used for slightly different purposes.


For example, suppose there is a file a.out with permissions -rwxrwxr-x owned by root.
Further suppose that root wants to let user_A run it with his (the 'his' is referring to 'root') permissions.
So, if root adds a function call in the code, at the start of the program (or wherever), to setuid() in order to allow user_A to run it with his (i.e root) permissions, but only he (i.e root) has the permissions to call setuid(), then this program is useless to anyone else, and to user_A in particular.
Correct: that program would be useless. Which is why you don't do it that way.

Let's assume we have binary /usr/local/bin/poke, and a service user account poke-user, and service group account poke-group. We install that setuid and setgid, but do not allow anybody else to run it:

sudo install -o poke-user -g poke-group -m u=rxs,g=rxs,o= -t /usr/local/bin poke
Then, you can add an access control list, allowing user user_A to run it:

sudo setfacl -m u:user_A:x /usr/local/bin/poke
Other users can be added the same way.
Kernel will not allow anybody other than user_A (and any other users or groups added in the ACL like above; just use g:group_B:x for groups) to execute it. When they do execute it, their real identity (getuid() and getgid()) stay as their real identity, but effective identity, which controls access, gets changed to poke-user and poke-group.

Depending on the kernel and system configuration, the real user still has some access to that process. In particular, the real user can send signals to the process, because of the matching real identity. (The nastiest of these is the STOP signal, which pauses the process, allowing a nasty user much better chances of exploiting any race conditions, or maybe just block others from using the restricted resource, if the program e.g. locks the resource for the access duration.)

However, if the program does setuid(geteuid()) and setgid(getegid()), the real identity is set to the effective identity -- poke-user and poke-group -- and the user who ran the binary can no longer send any signals to it! Risks averted!

If you don't have ACL support, you'd check the real user and group identity at the very beginning of the program, and exit unless they matched one that was specifically allowed. But that means either recompiling when the list changes, or a configuration file handling.. The ACLs are easier, and robust.

My post #21 (http://cboard.cprogramming.com/linux-programming/166827-temporarily-gain-root-privileges-perform-open-file.html#post1231220) above shows how access to a restricted resource can be controlled using only a service group with no user members. If you do not allow everybody to execute the binary, but add ACLs to allow specific groups or users to execute it, you can easily restrict access to it.

Other restriction patterns exist, and many of them rely on setuid()/setresuid() to protect from real user intervention while performing privileged stuff. In the simple cases, where you install signal handlers after performing the privileged stuff, those signals cause the program to terminate (the kernel normally prohibits core file generation for setuid/setgid binaries), so you don't need to protect against the signals. I personally carefully handle EINTR "errors" (which occur if a signal is delivered interrupting a blocking syscall), just because it gives me warm fuzzies, but others find it distasteful/wasteful/silly/paranoid. Me, I like to know that signal tricks won't trip me, even if they never occur in the basic situation.

Let's enumerate the cases where setuid() is useful:

When run as root, dropping privileges
The root user is superuser, and is allowed to change to any identity.
I personally use setresuid() and setresgid() GNU extensions to set the identity, because I want to be in full control, but for portable code, you'd use setgid(); setegid(); setuid(); seteuid() instead.
When running a setuid binary, to switch real identity to match effective identity, stopping the original user from sending signals to the process
Sending a well-timed signal is a common way to exploit bugs in code, because most programmers think signals are things that happen to other people, and they don't want to think about them much.
Others I cannot think of right now.
I mostly use setresuid(), which allows me to juggle three different user identities. I haven't needed to, but chaining two setuid binaries in a clever fashion should allow one to switch between the calling user and the two binary file owner users.


Questions?

Absurd
05-08-2015, 09:59 AM
You know... I gotta say, whenever I came across some of these terms ('uid', 'setuid', etc...) in the past, they all looked like gibberish to me. No matter how many times I tried to read and understand, I found myself giving up and skipping ahead at the end.
I never thought I'd get a hold of this, but thanks to you I think I'm starting to understand. And not only that, I think I learned more about linux from your posts here in this thread and on the previous one (http://cboard.cprogramming.com/linux-programming/166412-intercepting-usb-traffic-generated-mouse.html), than I did in a year of writing C code on linux.
Your posts are absolutely gold.