Warning: work in progress. Incomplete
People who have been in security a long time (or even not that long) know that some inputs should be treated
as tainted. For example, environment variables from a user should not be used in a
set-uid program, inputs from a different user should be validated, etc...
Traditionally we say that the environment of a set-uid program is tainted
and
should not be used (or used with much care).
Therefore we want all set-uid/set-gid programs to treat their environment and user inputs as tainted.
A program's code can tell if it should treat the environment with care because it
could check whether getuid() == geteuid() && getgid() == getegid()
early on in
main()
.
But! the libraries that the program uses cannot do this because by the
time they make this check it might be true even though it was not
earlier. To remedy this, in 1996 OpenBSD added
issetugid(2)
, a
system call that indicates whether the process gained privilege at exec
time by running a set-uid or set-gid program:
The issetugid() function returns 1 if the process was made setuid or
setgid as the result of the last or other previous execve() system
calls. Otherwise it returns 0.
This system call exists so that library routines (inside libtermlib,
libc, or other libraries) can guarantee safe behavior when used
inside setuid or setgid programs. Some library routines may be
passed insufficient information and hence not know whether the
current program was started setuid or setgid because higher level
calling code may have made changes to the uid, euid, gid, or egid.
Hence these low-level library routines are unable to determine if
they are being run with elevated or normal privileges.
In particular, it is wise to use this call to determine if a
pathname returned from a getenv() call may safely be used to open()
the specified file. Quite often this is not wise because the status
of the effective uid is not known. The issetugid() system call's
result is unaffected by calls to setuid(), setgid(), or other such
calls. In case of a fork(), the child process inherits the same
status.
Solaris/Illumos adopted OpenBSD's issetugid(2)
with the same semantics.
The other BSDs, and OS X, copied the OpenBSD
issetugid(2)
, but deliberately
changed its
semantics -- they
probably misunderstood the OpenBSD semantics and thought it buggy:
The issetugid() system call returns 1 if the process environment or
mem- ory address space is considered ``tainted'', and returns 0
otherwise.
A process is tainted if it was created as a result of an execve(2)
system call which had either of the setuid or setgid bits set (and
extra privi- leges were given as a result) or if it has changed
any of its real, effective or saved user or group ID's since it
began execution.
This system call exists so that library routines (eg: libc,
libtermcap) can reliably determine if it is safe to use information
that was obtained from the user, in particular the results from
getenv(3) should be viewed with suspicion if it is used to control
operation.
In FreeBSD, calling setuid()
will mark a previously privileged and not-tainted
process as tainted, whereas on OpenBSD it will not. This difference in semantics
may prove difficult to iron out, and has some negative consequences as shown below.
(Linux has a mechanism similar to OpenBSD's that we'll look at further below:
getauxval
(AT_SECURE)
.)
Now, Some systems have a
secure_getenv(), which returns NULL
whenever issetugid(2)
or
getauxval
(AT_SECURE)
,
or similar, returns non-zero, which happens whenever the process gained privilege
during the exec()
that started it.
This is precisely the sort of thing that the FreeBSD
issetugid(2)
http://www.manpagez.com/man/2/issetugid/) claims to be
for: to help decide if the environment is tainted. But the FreeBSD
semantics have some surprising behavior: it causes programs that only
drop privilege to ignore their environments. Thus, for example,
running these commands as root will fail to work as desired on
FreeBSD/NetBSD/DragonFly:
# KRB5_CONFIG=/tmp/krb5.conf sshd -dddp 2222
or
# KRB5_KTNAME=/tmp/kt httpd"
if one is using the Heimdal
implementation of Kerberos. MIT
Kerberos honors the environment unless
one calls krb5_init_secure_context()
, but GSS-API applications (which
is what httpd
and sshd
are) do not and cannot arrange to call that,
therefore set-uid programs that somehow end up using GSS/Kerberos with
MIT Kerberos... will incorrectly honor environment variables. Thus
we're asking MIT to adopt the Heimdal approach of automatically
determining whether to trust the environment rather than requiring
explicit initialization.
Ideally we'd like to implement secure_getenv()
in Heimdal's libroken
(a portability layer) and in MIT Kerberos as follows:
#ifndef HAVE_SECURE_GETENV
char *
secure_getenv(const char *name)
{
if (issetugid())
return getenv(name);
}
#endif
But that will, on OS X and Net/Free/DragonFly BSDs, have the surprising results described above.
Now let's return to the Linux getauxval(AT_SECURE)
mechanism. getauxval()
allows applications to access the ELF auxiliary vector, which is an array of
type/value pairs placed on the stack before main()
's stack frame, after the
environment from execve()
. The AT_SECURE
type has the same kind of sematics as
the OpenBSD issetugid(2)
: it's true if the process gained privilege at exec
time. There are other auxv
types such as AT_RUID
, AT_EUID
, and so on which
allow one to approximate AT_SECURE
's semantics well enough. And the ELF auxv
is
widely implemented.
So perhaps we could implement secure_getenv()
like this:
#ifndef HAVE_SECURE_GETENV
char *
secure_getenv(const char *name)
{
#if defined(HAVE_ISSETUGID) && defined(ISSETUGID_WORKS)
if (issetugid())
return NULL;
#elif defined(HAVE_GETAUXVAL)
#if defined(AT_SECURE)
if (getauxval(AT_SECURE))
return NULL;
return NULL;
#elif defined(AT_RUID) && defined(AT_EUID) && defined(AT_RGID) && defined(AT_EGID)
uid_t ruid, euid;
gid_t rgid, egid;
errno = 0;
if ((ruid = getauxval(AT_RUID)) == 0 && errno == ENOENT)
return NULL;
errno = 0;
if ((euid = getauxval(AT_EUID)) == 0 && errno == ENOENT)
return NULL;
errno = 0;
if ((rgid = getauxval(AT_RGID)) == 0 && errno == ENOENT)
return NULL;
errno = 0;
if ((egid = getauxval(AT_EGID)) == 0 && errno == ENOENT)
return NULL;
errno = 0;
if (ruid != euid || rgid != egid)
return NULL;
#else
if (getuid() != geteuid() || getgid() != getegid())
return NULL;
#endif
#else
if (getuid() != geteuid() || getgid() != getegid())
return NULL;
#endif
return getenv(name);
}
#endif
What a mess!
Oh, but it gets much worse:
-
Older glibc's
getauxval()
does not seterrno
at all when the type is not found in theauxv
. (!!!!!!!!!!!!) -
FreeBSD only supplies the necessary
auxv
types in Linux ABI compat mode.There is also an
AT_EXECFN
/AT_EXECNAME
/AT_SUN_EXECPATH
that contains the path to the executable that the process is running, and which one couldstat()
to determine if it is set-uid or whatever... But if the processchroot()
ed... that could yield incorrect results. -
Some systems (e.g., Illumos, NetBSD, OS X) do not provide a
getauxval()
, though at least Illumos and NetBSD (-current only) provide/proc/self/auxv
(as do Linux and FreeBSD). -
getauxval()
is not standard. It can't be standard because it casts pointers tolong
. See also the next item. -
Illumos implements the ABI standard for the
auxv
entry format ({int type; union { long v; void *vp; void (*)(void) fp; }; }
), while all others use{ long type; long value }
. (The standard format assumes thatlong
, pointers to data, and pointers to functions, can all be different sizes. The C standard does allow function pointers to differ in size from data pointers, though POSIX assumes in several places that the two have the same size.)
A truly heroic secure_getenv()
implementation for Heimdal's libroken would have to
attempt to read through /proc/self/auxv
to find these entries.
Can we get NetBSD, FreeBSD, DragonFly BSD, and OS X to fix their issetugid()
?
Probably not easily. Some will surely argue that there must be a way for a process
to mark itself as tainted or sensitive.
Maybe we just need yet more system calls to deal with this mess:
int istainted(void); /* true if P_SUGID or if setistainted() was called */
void setistainted(void); /* make istainted() return true until next exec() */
int issensitive(void); /* true if P_SNOCD or if setissentitive was called */
void setissentitive(void); /* make issensitive() return true until next exec() */
(Here P_SUGID
is a process flag that is set if the process exec
'ed a set-id
executable, while P_SNOCD
is set if the process changed its credentials in any
way. When P_SNOCD
is set the process will not dump a core that is readable by the
user it is running as, nor will it allow that user to attach a debugger to it.)
It takes time to get new system calls added, and longer still to have them made effective use of.
It would really be best to convince NetBSD, FreeBSD, DragonFly BSD, and OS X, that
issetugid()
should have the semantics that it has on Illumos/OpenBSD, but that's
probably not in the cards.
As good would be to convince them all to include auxval vectors, perhaps even in
non-ELF (Apple, I'm looking at you; but also maybe even in a.out) processes, and to
include getauxval()
or some variation thereof, and always include AT_UID
,
AT_EUID
, AT_GID
, AT_EGID
, and AT_SECURE
.