Several people have asked about the costs and benefits of splitting a setuid program into an unprivileged user process and a non-setuid daemon. C=08Co=08os=08st=08ts=08s.=08. It shouldn't take more than five minutes for= a kernel implementor to support getpeereuid(). For example, Linux has "struct ucred peercred" inside struct sock, and a SO_PEERCRED macro in sys/socket.h, used by the following four syscalls: * socket() sets peercred.uid to -1. * listen() sets peercred.uid to the euid of the current process. * connect() copies peercred.uid from the listening socket to the connecting socket, and copies the euid of the current process to peercred.uid in the new connected socket. * getsockopt(...SO_PEERCRED...) copies the peercred to user space. There's similar code to handle gid (and pid). The entire implementation is about twenty lines long. Given widespread kernel support for getpeereuid(), it's easy to split a setuid program. All you have to do is identify the atomic operations that the program performs upon restricted files, and move the code for those operations to a separate daemon. B=08Be=08en=08ne=08ef=08fi=08it=08ts=08s.=08. Two common types of code are = no longer security-critical when a setuid program is split: * Code in the unprivileged user process is safe. For example, /usr/bin/at has quite a bit of user-interface code, such as code to parse dates from argv. If that code loses its privilege then it is no longer a security threat to root. One can of course obtain the same benefit without getpeereuid(): split the setuid program into an unprivileged user program and a smaller setuid program. Why don't most programmers do this? Because they don't see the benefits, whereas they do see the simplicity of using just one process. That isn't an option if setuid disappears. * Code in the daemon that deals with stdin, stdout, stderr, argv, envp, cwd, timers, et al. is safe. What makes setuid programs so difficult to review is the number of ways that the code could interact with an untrusted user. An OS library routine might secretly use the environment, for example; this has led to many security holes. There's much less to worry about when user data is isolated inside controlled files. What's left is a tiny percentage of today's setuid code: the part that reads untrusted user data. Securing this code against local attackers is just like securing network code against remote attackers. For comparison, here's the idea mentioned by Steve Bellovin. Put your restricted file into a protected directory. Change its mode to 777. Now the setuid program can (1) chdir() to the protected directory, (2) setuid(getuid()), and (3) read and write the restricted file. Does this idea reduce the amount of security-critical code? Does it save time for security reviewers? Can the programmer relax? No! Step #3 no longer has a privileged uid but it still has a privileged cwd. The attacker is still trying to convert his control over portions of the process state into control over the restricted file. This idea is like replacing setuid with setgid: it might turn a disaster into a marginally less impressive disaster, but it makes no contribution to the goal of eliminating all disasters. ---Dan P.S. Beware that the setuid() syscall is inherently dangerous on some UNIX variants, because the user is allowed to attach to the process before the process calls execve(). This produces security holes in programs with privileges that aren't dropped until at or just before execve()---for example, the cwd as described above, or secrets hidden in stdio buffers, or close-on-exec file descriptors for /etc/shadow. I'll leave precise damage assessment as an amusing exercise for the reader. My favorite workaround is to make the binary unreadable; I haven't found any vendors silly enough to allow tracing here. Note that this prohibits root-squashed NFS mounting for root-owned binaries.
This archive was generated by hypermail 2b30 : Fri Apr 13 2001 - 14:28:18 PDT