Subject: Re: Addition to force open to open only regular files
To: NetBSD Kernel Technical Discussion List <tech-kern@netbsd.org>
From: Greg A. Woods <woods@weird.com>
List: tech-kern
Date: 11/18/2000 14:57:24
[ On Saturday, November 18, 2000 at 14:51:24 (+0100), Olaf Seibert wrote: ]
> Subject: Re: Addition to force open to open only regular files
>
> Exactly, because anything you store in userland is subject to buffer
> overflows and such.

I'm just paranoid that it could get out-of-sync because of a bug in the
kernel or something -- not a bug in *my* code!  :-)

> Any argument to an open_as() function is subject to
> attack. This would be an argument for *not* passing a uid to an
> open_as() function

Indeed.

The only drawback is that such an interface doesn't solve quite as many
problems, and it solves problems only for set-ID programs.  For example
a monolithic mailer that starts as root in the first place would still
be required to fork() in order to read a user's ~/.forward.  Of course
this is more of an argument against monolithic large processes that
spend most of their time running as root than anything else....

The more I think about it, the more I believe that the idea of a new
FS-ID set of credentials for all processes is really no different from
having a sete*id() set of calls that allow ID swapping (i.e. it is no
gain over what we already have).  If that's true then open_as() doesn`t
really gain anything either unless we want to get rid of ID-swapping
[i.e. get rid of sete*id()].  ID-swapping allows the set-ID programmer
to choose either to run most code as the effective ID, or most code as
the real-ID and yet still be able to gain the effective-ID privs at any
time.  The fallacy is that the former is *not*, and never has been
whenever ID-swapping is possible, more secure.  If there's ever the
possibility of regaining privileges then running as the real-ID is only
going to protect you in a very few minor cases (such as a new shared
library routine that might trick you into opening a tape device), since
the code introduced in a buffer overflow need only call seteuid(0)
before forking its shell.  A set of FS-ID credentials is no more secure
than ID-swapping for much the same reasons.

If open_as() *replaces* sete*id() then of course it must accept at least
UID and GID parameters to be of any use.  In this case you probably do
NOT want to want to provide this capability to other function calls,
though [eg. especially not chmod() or chown()!].

In either case [i.e. with either sete*id() _or_ open_as()], what we're
left with is the fact that large long-running, set-ID programs are still
very dangerous -- the smaller they are the more likely they'll be
correctly and safely coded.

I think what's really critical either way is that all set-ID processes
must be afforded full protection from ptrace(), signals, /procfs, etc.,
at *all* times.  I.e. the permissions checks on all calls that affect
another process must first check if the process image file had set-ID
flags (ala the in-kernel issetuid() check), and only if that's false
then check to see if the current effective-user-ID of the calling
process is equal to just the real-user-ID of the process in question.
The only exception to this rule can be made for signals that would call
for the *normal* safe termination of a process (i.e. HUP, INT, KILL, or
TERM), or the safe interactive control signals (i.e. INFO & CONT, and
maybe TSTP), or user-defined signals (i.e. USR1 & USR2), in which case
the issetuid() check can be ignored and just the real-user-ID would be
compared with the effective-user-ID of the caller (oh, and of course
CONT is allowed if they've got the same session-ID).  I.e. I don't think
the CANSIGNAL() macro in sys/kern/kern_sig.c is careful enough since it
can allow delivery of non-safe signals (even though these signals won't
result in a core dump because other checks are done before that happens
-- I just don't like the idea of a user being able to trigger signal
handlers unexpectedly).  It's already more lax than kill(2) describes
because it allows the effective-UID to match the real-UID or vice
versa.

The messy trick here is ensure that even system calls to things like
procfs are always *forced* to enforce the same rules.  Some of the bugs
I've seen in the past were caused by the fact that there's only an
implicit relationship between open("/proc/1/mem") and the process
running as PID#1.  Any mistake in the coding of such a virtual
filesystem, such as making the ownership of the /proc/1/mem file
dynamically reflect only the effective-ID, or worse only the real-ID, of
that process will allow the open() to succeed even when the process in
question has not completely and permanently dropped its privileges.  The
potential for a new and even more exciting feature like /procfs being
introduced into a system without understanding the full implications of
how it might allow a process to be compromised is even more scary.

What might be better, if it's possible, would be to put some of these
permissions checks much closer to where they actually affect the
process, rather than relying on the earlier system-call interface to do
them.  I.e. don't check permissions in the wrapper [eg. sys_kill()], but
rather in the function which really does the work [eg. psignal()].  This
means you've got to pass a pointer to the calling process' proc
structure down through opens and into the low-level FS layer so that
things like procfs can pass it on to the things like psignal().  This
would perhaps have made procfs safe without the need to replicate all
kinds of credentials checks throughout it.

Oh, and of course *NEVER* link a set-ID program dynamically!

If I were to redesign set-ID again I think I'd make it work in such a
fashion that the resulting process defaulted to running as the real user
and that sete*id() would be necessary to *temporarily* raise privileges
only for the next system call -- this would make set-ID programmers more
aware of when they are using their privileges and might make it easier
for them to figure out when they can completely drop privileges.  I
would also make fork() always revert to the real-IDs just as if you'd
first called setuid(getuid()) -- i.e. no inheritance of privs!

-- 
							Greg A. Woods

+1 416 218-0098      VE3TCP      <gwoods@acm.org>      <robohack!woods>
Planix, Inc. <woods@planix.com>; Secrets of the Weird <woods@weird.com>