diff options
Diffstat (limited to 'src/linux-user-chroot.c')
-rw-r--r-- | src/linux-user-chroot.c | 108 |
1 files changed, 89 insertions, 19 deletions
diff --git a/src/linux-user-chroot.c b/src/linux-user-chroot.c index 66285e5..8b8700d 100644 --- a/src/linux-user-chroot.c +++ b/src/linux-user-chroot.c @@ -5,7 +5,8 @@ * "safely": I believe that this program, when deployed as setuid on a * typical "distribution" such as RHEL or Debian, does not, even when * used in combination with typical software installed on that - * distribution, allow privilege escalation. + * distribution, allow privilege escalation. See the README for more + * details. * * Copyright 2011,2012 Colin Walters <walters@verbum.org> * @@ -24,16 +25,20 @@ * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ +#include "config.h" + #define _GNU_SOURCE #include <unistd.h> #include <stdio.h> #include <fcntl.h> +#include <errno.h> #include <stdarg.h> #include <string.h> #include <assert.h> #include <stdlib.h> #include <sys/types.h> #include <sys/prctl.h> +#include <sys/fsuid.h> #include <sys/mount.h> #include <sys/syscall.h> #include <sys/wait.h> @@ -45,6 +50,20 @@ #define SECBIT_NOROOT_LOCKED (1 << 1) #endif +#ifndef PR_SET_NO_NEW_PRIVS +#define PR_SET_NO_NEW_PRIVS 38 +#endif + +/* Totally arbitrary; we're just trying to mitigate somewhat against + * DoS attacks. In practice uids can typically spawn multiple + * processes, so this isn't effective. What is needed is for the + * kernel to understand we're creating bind mounts on behalf of a + * given uid. Most likely this will happen if the kernel obsoletes + * this tool by allowing processes with PR_SET_NO_NEW_PRIVS to create + * private mounts or chroot. + */ +#define MAX_BIND_MOUNTS 1024 + static void fatal (const char *message, ...) __attribute__ ((noreturn)) __attribute__ ((format (printf, 1, 2))); static void fatal_errno (const char *message) __attribute__ ((noreturn)); @@ -102,6 +121,28 @@ reverse_mount_list (MountSpec *mount) return prev; } +/** + * fsuid_chdir: + * @uid: User id we should use + * @path: Path string + * + * Like chdir() except we use the filesystem privileges of @uid. + */ +static int +fsuid_chdir (uid_t uid, + const char *path) +{ + int errsv; + int ret; + /* Note we don't check errors here because we can't, basically */ + (void) setfsuid (uid); + ret = chdir (path); + errsv = errno; + (void) setfsuid (0); + errno = errsv; + return ret; +} + int main (int argc, char **argv) @@ -114,7 +155,7 @@ main (int argc, gid_t rgid, egid, sgid; int after_mount_arg_index; unsigned int n_mounts = 0; - const unsigned int max_mounts = 50; /* Totally arbitrary... */ + const unsigned int max_mounts = MAX_BIND_MOUNTS; char **program_argv; MountSpec *bind_mounts = NULL; MountSpec *bind_mount_iter; @@ -145,7 +186,17 @@ main (int argc, fatal ("Too many mounts (maximum of %u)", n_mounts); n_mounts++; - if (strcmp (arg, "--mount-bind") == 0) + if (strcmp (arg, "--help") == 0) + { + printf ("%s\n", "See \"man linux-user-chroot\""); + exit (0); + } + else if (strcmp (arg, "--version") == 0) + { + printf ("%s\n", PACKAGE_STRING); + exit (0); + } + else if (strcmp (arg, "--mount-bind") == 0) { if ((argc - after_mount_arg_index) < 3) fatal ("--mount-bind takes two arguments"); @@ -257,21 +308,25 @@ main (int argc, clone_flags |= CLONE_NEWNET; if ((child = syscall (__NR_clone, clone_flags, NULL)) < 0) - perror ("clone"); + fatal_errno ("clone"); if (child == 0) { /* - * SECBIT_NOROOT helps close the main historical reason why only - * uid 0 can chroot(2) - because unprivileged users can create - * hard links to setuid binaries, and possibly confuse them into - * looking at data (or loading libraries) that they don't - * expect, and thus elevating privileges. With this, executing - * a setuid program doesn't gain us any new Linux capabilities - * (but it still changes uid). See below for where we create a - * MS_NOSUID bind mount. + * First, we attempt to use PR_SET_NO_NEW_PRIVS, since it does + * exactly what we want - ensures the child can not gain any + * privileges, even attempting to execute setuid binaries. + * + * http://lwn.net/Articles/504879/ + * + * If that's not available, we fall back to using SECBIT_NOROOT. + * + * Following the belt-and-suspenders model, we also make a + * MS_NOSUID bind mount below. */ - if (prctl (PR_SET_SECUREBITS, + if (prctl (PR_SET_NO_NEW_PRIVS, 1) < 0 && errno != EINVAL) + fatal_errno ("prctl (PR_SET_NO_NEW_PRIVS)"); + else if (prctl (PR_SET_SECUREBITS, SECBIT_NOROOT | SECBIT_NOROOT_LOCKED) < 0) fatal_errno ("prctl (SECBIT_NOROOT)"); @@ -308,7 +363,9 @@ main (int argc, } else if (bind_mount_iter->type == MOUNT_SPEC_BIND) { - if (mount (bind_mount_iter->source, dest, + if (fsuid_chdir (ruid, bind_mount_iter->source) < 0) + fatal ("Couldn't chdir to bind mount source"); + if (mount (".", dest, NULL, MS_BIND | MS_PRIVATE, NULL) < 0) fatal_errno ("mount (MS_BIND)"); } @@ -322,13 +379,23 @@ main (int argc, assert (0); free (dest); } - - /* Actually perform the chroot. */ - if (chroot (chroot_dir) < 0) - fatal_errno ("chroot"); - if (chdir (chdir_target) < 0) + + if (fsuid_chdir (ruid, chroot_dir) < 0) fatal_errno ("chdir"); + if (mount (".", ".", NULL, MS_BIND | MS_PRIVATE, NULL) < 0) + fatal_errno ("mount (MS_BIND)"); + + /* Only move if we're not actually just using / */ + if (strcmp (chroot_dir, "/") != 0) + { + if (mount (chroot_dir, "/", NULL, MS_MOVE, NULL) < 0) + fatal_errno ("mount (MS_MOVE)"); + + if (chroot (".") < 0) + fatal_errno ("chroot"); + } + /* Switch back to the uid of our invoking process. These calls are * irrevocable - see setuid(2) */ if (setgid (rgid) < 0) @@ -336,6 +403,9 @@ main (int argc, if (setuid (ruid) < 0) fatal_errno ("setuid"); + if (chdir (chdir_target) < 0) + fatal_errno ("chdir"); + if (execvp (program, program_argv) < 0) fatal_errno ("execv"); } |