Skip to content

Instantly share code, notes, and snippets.

@nicowilliams
Last active July 2, 2021 00:46
Show Gist options
  • Save nicowilliams/4daf74a3a0c86848d3cbd9d0cdb5e26e to your computer and use it in GitHub Desktop.
Save nicowilliams/4daf74a3a0c86848d3cbd9d0cdb5e26e to your computer and use it in GitHub Desktop.
issetugid() sadness

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 set errno at all when the type is not found in the auxv. (!!!!!!!!!!!!)

  • 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 could stat() to determine if it is set-uid or whatever... But if the process chroot()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 to long. 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 that long, 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment