From 90110f5553753b06bb7870136c303e5d8580b1be Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Mon, 17 Apr 2017 16:43:26 -0500 Subject: [PATCH] Revamp issuid() --- lib/roken/issuid.c | 287 ++++++++++------------------------------ lib/roken/test-auxval.c | 48 +++++++ 2 files changed, 120 insertions(+), 215 deletions(-) diff --git a/lib/roken/issuid.c b/lib/roken/issuid.c index 9f8227bf9..c0322e2b1 100644 --- a/lib/roken/issuid.c +++ b/lib/roken/issuid.c @@ -40,207 +40,92 @@ #include #include "roken.h" +#include "getauxval.h" -/* NetBSD calls AT_UID AT_RUID. Everyone else calls it AT_UID. */ -#if defined(AT_EUID) && defined(AT_RUID) && !defined(AT_UID) -#define AT_UID AT_RUID -#endif -#if defined(AT_EGID) && defined(AT_RGID) && !defined(AT_GID) -#define AT_GID AT_RGID -#endif - -#ifdef __GLIBC__ -#ifdef __GLIBC_PREREQ -#define HAVE_GLIBC_API_VERSION_SUPPORT(maj, min) __GLIBC_PREREQ(maj, min) -#else -#define HAVE_GLIBC_API_VERSION_SUPPORT(maj, min) \ - ((__GLIBC << 16) + GLIBC_MINOR >= ((maj) << 16) + (min)) -#endif - -/* - * Do change this check in order to manually test rk_getauxval() for - * older glibcs. - */ -#if HAVE_GLIBC_API_VERSION_SUPPORT(2, 19) -#define GETAUXVAL_SETS_ERRNO -#endif -#endif - -/** - * Like the nearly-standard getauxval(), but reads through - * /proc/self/auxv if it exists (this works on Linux, and, by code - * inspection, on FreeBSD, but not Solaris/Illumos, where the auxv type - * is an int and the value is a union of long, data pointer, and - * function pointer), otherwise it sets errno to ENOENT and returns - * zero. If the auxval is not found returns zero and always sets errno - * to ENOENT. Otherwise if auxval is found it leaves errno as it was, - * even if the value is zero. - * - * @return The value of the ELF auxiliary value for the given type. - */ -ROKEN_LIB_FUNCTION unsigned long ROKEN_LIB_CALL -rk_getprocauxval(unsigned long type) -{ - static int has_proc_auxv = 1; - unsigned long a[2]; - ssize_t bytes; - int save_errno = errno; - int fd; - - if (!has_proc_auxv) { - errno = ENOENT; - return 0; - } - - if ((fd = open("/proc/self/auxv", O_RDONLY)) == -1) { - if (errno == ENOENT) - has_proc_auxv = 0; - errno = ENOENT; - return 0; - } - - /* FIXME: Make this work on Illumos */ - do { - if ((bytes = read(fd, a, sizeof(a))) != sizeof(a)) - break; - if (a[0] == type) { - (void) close(fd); - errno = save_errno; - return a[1]; - } - } while (bytes == sizeof(a) && (a[0] != 0 || a[1] != 0)); - - (void) close(fd); - errno = ENOENT; - return 0; -} - -/** - * Like the nearly-standard getauxval(). If the auxval is not found - * returns zero and always sets errno to ENOENT. Otherwise if auxval is - * found it leaves errno as it was, even if the value is zero. - * - * @return The value of the ELF auxiliary value for the given type. - */ -ROKEN_LIB_FUNCTION unsigned long ROKEN_LIB_CALL -rk_getauxval(unsigned long type) -{ -#ifdef HAVE_GETAUXVAL -#ifdef GETAUXVAL_SETS_ERRNO - return getauxval(type); -#else - unsigned long ret; - unsigned long ret2; - static int getauxval_sets_errno = -1; - int save_errno = errno; - - errno = 0; - ret = getauxval(type); - if (ret != 0 || errno == ENOENT || getauxval_sets_errno == 1) { - if (ret != 0) - errno = save_errno; - else if (getauxval_sets_errno && errno == 0) - errno = save_errno; - return ret; - } - - if (!getauxval_sets_errno) { - errno = save_errno; - return rk_getprocauxval(type); - } - - errno = 0; - ret2 = getauxval(~type); /* Hacky, quite hacky */ - if (ret2 == 0 && errno == ENOENT) { - getauxval_sets_errno = 1; - errno = save_errno; - return ret; /* Oh, it does set errno. Good! */ - } - - errno = save_errno; - getauxval_sets_errno = 0; - return rk_getprocauxval(type); -#endif -#else - return rk_getprocauxval(type); -#endif -} +extern int rk_injected_auxv; /** * Returns non-zero if the caller's process started as set-uid or * set-gid (and therefore the environment cannot be trusted). * + * As much as possible this implements the same functionality and + * semantics as OpenBSD's issetugid() (as opposed to FreeBSD's). + * + * Preserves errno. + * * @return Non-zero if the environment is not trusted. */ ROKEN_LIB_FUNCTION int ROKEN_LIB_CALL issuid(void) { +#ifdef WIN32 + return 0; /* No set-id programs or anything like it on Windows */ +#else /* * We want to use issetugid(), but issetugid() is not the same on * all OSes. * - * On Illumos derivatives, OpenBSD, and Solaris issetugid() returns - * true IFF the program exec()ed was set-uid or set-gid. + * On OpenBSD (where issetugid() originated), Illumos derivatives, + * and Solaris, issetugid() returns true IFF the program exec()ed + * was set-uid or set-gid. * - * On NetBSD and FreeBSD issetugid() returns true if the program - * exec()ed was set-uid or set-gid, or if the process has switched - * UIDs/GIDs or otherwise changed privileges or is a descendant of - * such a process and has not exec()ed since. + * FreeBSD departed from OpenBSD's issetugid() semantics, and other + * BSDs (NetBSD, DragonFly) and OS X adopted FreeBSD's. * - * What we want here is to know only if the program exec()ed was - * set-uid or set-gid, so we can decide whether to trust the - * enviroment variables. We don't care if this was a process that - * started as root and later changed UIDs/privs whatever: since it - * started out as privileged, it inherited an environment from a - * privileged pre-exec self, and so on, so the environment is - * trusted. + * FreeBSDs' issetugid() returns true if the program exec()ed was + * set-uid or set-gid, or if the process has switched UIDs/GIDs or + * otherwise changed privileges or is a descendant of such a process + * and has not exec()ed since. * - * Therefore the FreeBSD/NetBSD issetugid() does us no good. + * The FreeBSD/NetBSD issetugid() does us no good because we _want_ + * to trust the environment when the process started life as + * non-set-uid root (or otherwise privileged). There's nothing + * about _dropping_ privileges (without having gained them first) + * that taints the environment. It's not like calling system(), + * say, might change the environment of the caller. + * + * We want OpenBSD's issetugid() semantics. * * Linux, meanwhile, has no issetugid() (at least glibc doesn't - * anyways). + * anyways) but has an equivalent: getauxval(AT_SECURE). * - * Systems that support ELF put an "auxilliary vector" on the stack - * prior to starting the RTLD, and this vector includes (optionally) - * information about the process' EUID, RUID, EGID, RGID, and so on - * at the time of exec(), which we can use to construct proper - * issetugid() functionality. Other useful (and used here) auxv - * types include: AT_SECURE (Linux) and the path to the program - * exec'ed. None of this applies to statically-linked programs - * though. + * To be really specific: we want getauxval(AT_SECURE) semantics + * because there may be ways in which a process might gain privilege + * at exec time other than by exec'ing a set-id program. * - * Where available, we use the ELF auxilliary vector before trying - * issetugid(). + * Where we use getauxval(), we really use our getauxval(), the one + * that isn't broken the way glibc's used to be. Our getauxval() + * also works on more systems than actually provide one. * - * All of this is as of late March 2017, and might become stale in - * the future. + * In order to avoid FreeBSD issetugid() semantics, where available, + * we use the ELF auxilliary vector to implement OpenBSD semantics + * before finally falling back on issetugid(). + * + * All of this is as of April 2017, and might become stale in the + * future. */ - static int we_are_suid = -1; + static int we_are_suid = -1; /* Memoize; -1 == dunno */ int save_errno = errno; -#if (defined(AT_EUID) && defined(AT_UID)) || (defined(AT_EGID) && defined(AT_GID)) +#if defined(AT_EUID) && defined(AT_UID) && defined(AT_EGID) && defined(AT_GID) int seen = 0; #endif - if (we_are_suid >= 0) + if (we_are_suid >= 0 && !rk_injected_auxv) return we_are_suid; #ifdef AT_SECURE - /* - * AT_SECURE is set if the program was set-id or gained any kind of - * privilege in a similar way. - */ errno = 0; if (rk_getauxval(AT_SECURE) != 0) { errno = save_errno; return we_are_suid = 1; - } - else if (errno == 0) { + } else if (errno == 0) { errno = save_errno; return we_are_suid = 0; } + /* errno == ENOENT; AT_SECURE not found; fall through */ #endif -#if defined(AT_EUID) && defined(AT_UID) +#if defined(AT_EUID) && defined(AT_UID) && defined(AT_EGID) && defined(AT_GID) { unsigned long euid; unsigned long uid; @@ -258,8 +143,7 @@ issuid(void) return we_are_suid = 1; } } -#endif -#if defined(AT_EGID) && defined(AT_GID) + /* Check GIDs */ { unsigned long egid; unsigned long gid; @@ -277,58 +161,32 @@ issuid(void) return we_are_suid = 1; } } -#endif errno = save_errno; - - /* - * This pre-processor condition could be all &&s, but that could - * cause a warning that seen is set but never used. - * - * In practice if any one of these four macros is defined then all - * of them will be. - */ -#if (defined(AT_EUID) && defined(AT_UID)) || (defined(AT_EGID) && defined(AT_GID)) - if (seen == 15) { - errno = save_errno; + if (seen == 15) return we_are_suid = 0; - } #endif #if defined(HAVE_ISSETUGID) - /* - * If we have issetugid(), use it. Illumos' and OpenBSD's - * issetugid() works correctly. - * - * On NetBSD and FreeBSD, however, issetugid() returns non-zero even - * if the process started as root, not-set-uid, and then later - * called seteuid(), for example, but in that case we'd want to - * trust the environ! So if issetugid() > 0 we want to do something - * else. See below. - */ + /* If issetugid() == 0 then we're definitely OK then */ if (issetugid() == 0) return we_are_suid = 0; + /* issetugid() == 1 might have been a false positive; fall through */ #endif /* USE_RK_GETAUXVAL */ -#if defined(AT_EXECFN) || defined(AT_EXECPATH) - - /* - * There's an auxval by which to find the path of the program this - * process exec'ed. - * - * Linux calls this AT_EXECFN. FreeBSD calls it AT_EXECPATH. NetBSD - * and Illumos call it AT_SUN_EXECNAME. - * - * We can stat it. If the program did a chroot() and the chroot has - * a program with the same path but not set-uid/set-gid, of course, - * we lose here. But a) that's a bit of a stretch, b) there's not - * much more we can do here. - */ -#if defined(AT_EXECFN) && !defined(AT_EXECPATH) -#define AT_EXECPATH AT_EXECFN -#endif -#if defined(AT_SUN_EXECNAME) && !defined(AT_EXECPATH) -#define AT_EXECPATH AT_EXECFN -#endif +#ifdef AT_EXECFN + /* + * There's an auxval by which to find the path of the program this + * process exec'ed. + * + * We can stat() it. If the program did a chroot() and the chroot + * has a program with the same path but not set-uid/set-gid, of + * course, we lose here. But a) that's a bit of a stretch, b) + * there's not much more we can do here. + * + * Also, this is technically a TOCTOU race, though for set-id + * programs this is exceedingly unlikely to be an actual TOCTOU + * race. + */ { unsigned long p = getauxval(AT_EXECPATH); struct stat st; @@ -343,27 +201,25 @@ issuid(void) return we_are_suid = 0; } } + /* Fall through */ #endif - /* - * Fall through if we have rk_getauxval() but we didn't have (or - * don't know if we don't have) the aux entries that we needed. - * We're done with it. - */ - #if defined(HAVE_ISSETUGID) errno = save_errno; return we_are_suid = 1; #else - /* * Paranoia: for extra safety we ought to default to returning 1. * * But who knows what that might break where users link statically - * (so no auxv), say. Also, on Windows we should always return 0. + * (so no auxv), say. + * + * We'll check the actual real and effective IDs (as opposed to the + * ones at main() start time. * * For now we stick to returning zero by default. We've been rather - * heroic above trying to find out if we're suid. + * heroic above trying to find out if we're suid, and we're running + * on a rather old or uncool OS if we've gotten here. */ #if defined(HAVE_GETRESUID) @@ -416,4 +272,5 @@ issuid(void) errno = save_errno; return we_are_suid = 0; #endif /* !defined(HAVE_ISSETUGID) */ +#endif /* WIN32 */ } diff --git a/lib/roken/test-auxval.c b/lib/roken/test-auxval.c index a14bdbfde..27276595b 100644 --- a/lib/roken/test-auxval.c +++ b/lib/roken/test-auxval.c @@ -43,6 +43,49 @@ #include "roken.h" #include "getauxval.h" +static void +inject_suid(int suid) +{ +#if defined(AT_SECURE) || (defined(AT_EUID) && defined(AT_RUID) && defined(AT_EGID) && defined(AT_RGID)) + auxv_t e; +#ifdef AT_SECURE + unsigned long secure = suid ? 1 : 0; +#endif +#if defined(AT_EUID) && defined(AT_RUID) && defined(AT_EGID) && defined(AT_RGID) + unsigned long eid = suid ? 0 : 1000; + + /* Inject real UID and GID */ + e.a_un.a_val = 1000; + e.a_type = AT_UID; + if ((errno = rk_injectauxv(&e)) != 0) + err(1, "rk_injectauxv(AT_RUID) failed"); + e.a_type = AT_GID; + if ((errno = rk_injectauxv(&e)) != 0) + err(1, "rk_injectauxv(AT_RGID) failed"); + + /* Inject effective UID and GID */ + e.a_un.a_val = eid; + e.a_type = AT_EUID; + if ((errno = rk_injectauxv(&e)) != 0) + err(1, "rk_injectauxv(AT_EUID) failed"); + e.a_type = AT_EGID; + if ((errno = rk_injectauxv(&e)) != 0) + err(1, "rk_injectauxv(AT_RGID) failed"); +#endif + +#ifdef AT_SECURE + e.a_un.a_val = secure; + e.a_type = AT_SECURE; + if ((errno = rk_injectauxv(&e)) != 0) + err(1, "rk_injectauxv(AT_SECURE) failed"); +#endif + + return; +#else + warnx(1, "No ELF auxv types to inject"); +#endif +} + static unsigned long getprocauxval(unsigned long type) @@ -144,5 +187,10 @@ main(int argc, char **argv, char **env) if (errno != ENOENT) errx(1, "rk_getauxv((max_type_seen = %lu) + 1) did not set " "errno = ENOENT!", max_t); + + inject_suid(!am_suid); + if ((am_suid && issuid()) || (!am_suid && !issuid())) + errx(1, "rk_injectprocauxv() failed"); + return 0; }