Why I don't trust seteuid() and friends

March 14, 2009

Unix has a number of methods that a suitably privileged process can use to temporarily assume another UID, do something with those limited privileges, and then return back to its old, fully privileged state. The grandfather of these is probably setreuid(), which appeared way back in 4.2 BSD.

Normally, there are any number of ways to examine and interfere with processes that are running under your UID; you can send them signals, you can stop them, you can use ptrace() or the equivalent to inspect them, run them as slow as you want, and even inject your own code, and on some operating systems you can use /proc to gain access to files that they have open. (This is not an exhaustive list.)

Allowing you to do any of these to a process that is merely temporarily running with your UID and that actually had elevated privileges opens up various sorts of security holes. If you can directly inject code it is an instant 'game over', but even lesser access is dangerous; for example, such a process may have open file descriptors to files that contain sensitive information (which it has left open because it was counting on them staying inaccessible to you).

In theory, Unix kernels prevent you from doing any of these things to such processes. In practice, people keep adding ways to do things to your processes (and sometimes they add new ways to do things with less privileges), and they do not always realize the full implications or, even if they do, they do not completely protect their mechanism.

The result, at least in my impression, has been a slow stream of ways to make such temporarily reduced privileges 'leak', to exploit them to gain extra access or information or the like. As a result I do not trust any of them to be completely secure all of the time. Which leads me to my view that if you want complete security on Unix, you must irrevocably give up all privileges.

On some systems, even many systems, your code will be safe today if you properly use the right mechanism to temporarily assume another UID (note all of those qualifiers). But I cannot trust it to stay safe tomorrow and on another system, because I feel that the whole edifice is simply too fragile. Irrevocably giving up privileges is harsh, but it is not fragile, and it is honest; when you write such code, you write it with the full knowledge that the user can completely hijack your unprivileged side, and the rest of your code has to be able to deal with that.


Comments on this page:

By Dan.Astoorian at 2009-03-14 11:44:21:

...for example, such a process may have open file descriptors to files that contain sensitive information (which it has left open because it was counting on them staying inaccessible to you).

Giving up privileges permanently instead of temporarily does not mitigate this threat (in fact, if anything, it may exacerbate it, since it gives the unprivileged user a greater window to exploit).

Note also that there have historically been implementations of features where an OS would permit, say, attaching to or otherwise interfering with a process whose privileges have all been revoked, but would not allow this to happen if the real or saved uid/gid differed from the effective uid/gid; under such systems, revoking privileges permanently would give the user access they wouldn't have had if it had merely done seteuid().

(I believe it's the case that most modern OSes remove a process's capability of being traced by an unprivileged user whenever its uids/gids change, but this was not always the case.)

In fact, it follows from your argument that permanently revoking privileges is not sufficient: to prevent access to data or resources that the process may have acquired while privileged, it must completely obliterate itself--close all file descriptors, release all other resources, and wipe its entire address space--and pass down only the well-vetted information that the lesser process needs to perform its function. This will sometimes increase the complexity and cost of the design to the point where it's not a worthwhile trade-off.

In practice, people keep adding ways to do things to your processes (and sometimes they add new ways to do things with less privileges), and they do not always realize the full implications or, even if they do, they do not completely protect their mechanism.

This is, very obviously, the fault of whoever adds such mechanisms (or, depending on your perspective, whoever allows such additions to be integrated into the system). Trying to mitigate against them is the same as trying to defend against backdoors in the kernel--which is, essentially, what such misimplemented features are.

Which leads me to my view that if you want complete security on Unix,...

I disagree with the premise that such an ideal is attainable. However, I think that your conclusion--that it's never as safe to drop privileges temporarily and reacquire them than it is to drop them permanently and design your system so that this can be done practically--is too conservative to adopt as a general design principle.

--Dan

By cks at 2009-03-15 00:13:21:

I'm a security pragmatist. It doesn't matter whose fault it is that temporary privilege reduction mechanisms keep being buggy; what matters is that historically, I believe that they have. This is not the same as trying to defend against arbitrary kernel backdoors, because these 'backdoors' are not arbitrary.

And yes, this means that you must scrub all privileged information out from the process that will run actions as the user. Usually the easiest way to do this is to sanitize file descriptors and exec() a separate program.

As a side note: I expect that permanently revoking privileges leaves your process fully traceable on pretty much every Unix, because this is the mechanism that login and so on use and as far as I know you've always been able to trace your own login shell and so on.

By Dan.Astoorian at 2009-03-16 12:03:35:

As a side note: I expect that permanently revoking privileges leaves your process fully traceable on pretty much every Unix, because this is the mechanism that login and so on use and as far as I know you've always been able to trace your own login shell and so on.

If this were correct, then there would be a disastrous race condition between revoking privileges and calling exec() during which the user could attach to the process and examine its memory.

However, I think you'll find that if, as root on a modern Linux kernel (and hopefully others), you were to run:

   python -c 'import os, time; print os.getpid(); os.setuid(100); time.sleep(60)'

then another process running as uid 100 will not be able to trace it. (It would be able to send signals to it, though.) I believe it's the case that a process which has dropped its privileges must both fork() and exec() in order to become traceable.

--Dan

Written on 14 March 2009.
« The problem with /var today
A realization: planet aggregators have a natural size limit »

Page tools: View Source, View Normal, Add Comment.
Search:
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Sat Mar 14 00:03:31 2009
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.