[alt.security] A partial user-mode tty security fix for SunOS, Ultrix, et al.

brnstnd@kramden.acf.nyu.edu (Dan Bernstein) (06/13/91)

Administrators of machines running SunOS 4.0.3, SunOS 4.1, SunOS 4.1.1,
Ultrix 2.2, Ultrix 4.1, possibly intermediate Ultrix versions, straight
BSD 4.3-Tahoe, DYNIX 3.0.17, Convex UNIX 9.0, and possibly other systems
may be interested in the following quick, though only partially tested
and only partially reliable, fix to the basic BSD tty security problems.
Thanks to Seth Robertson for his help.

This is quick because it requires absolutely no source or binary
patches, to the kernel or to any application programs. This is only
partially reliable because it runs in user mode and hence is subject to
race conditions if an attacker operates quickly enough. The races do
appear to be very heavily biased against an attacker, but I cannot
guarantee complete security. I also cannot guarantee that the code will
work properly on all machines, as it depends on kernel-reading code
which is not supported by individual vendors.

The effect of these changes is that if someone still has access to a
tty, /bin/login will refuse to run on that tty. It will print an error
message showing one pid that has access to the tty (though there may be
more), sleep for thirty seconds, and then exit. NOTE THAT THIS WILL
TYPICALLY HAPPEN TO EVERYONE AS SOON AS A BACKGROUND PROCESS IS RUNNING
ON THE FIRST ACCESSIBLE TTY, *except* (I believe) under SunOS >=4.1 with
the patched telnetd and rlogind. There are very few ways to solve this
without telnetd/rlogind patches. The user can make more connections
before the sleep(30) runs out; in practice he will rapidly come upon an
unused pty. A better solution is to educate your users to detach
long-running background jobs, e.g. with the following detach.c:

  #include <sys/types.h>
  #include <sys/file.h>
  #ifdef BSD
  #include <limits.h>
  #endif
  #include <sys/ioctl.h>

  main(argc,argv)
  int argc;
  char *argv[];
  {
   int fdtty;
   fdtty = open("/dev/tty",O_RDWR);
   if (fdtty != -1)
    {
     ioctl(fdtty,TIOCNOTTY,0);
     close(fdtty);
    }
   for (fdtty = getdtablesize();fdtty--;)
    {
     if (isatty(fdtty))
      {
       int fdnull;
       close(fdtty);
       fdnull = open("/dev/null",O_RDWR);
       if (fdnull != -1)
	 if (fdnull != fdtty)
	  {
	   dup2(fdnull,fdtty);
	   close(fdnull);
	  }
      }
    }
   execlp(argv[1],argv + 1);
   perror("detach: fatal: cannot exec");
   exit(1);
  }

Another possibility would be to have a background daemon manually
allocate any ptys where the ttys are in use. If you can patch telnetd
and rlogind, you can avoid these problems entirely; I will address this
further in a future posting.

In any case, no programs are changed or moved other than /bin/login, and
you do not need source.

1. Pick up kstuff 0.18, just posted to alt.sources. If you do not
   receive alt.sources, get the articles from an alt.sources archive
   site, such as articles 3296-3301 in usenet/alt.sources/articles on
   wuarchive.wustl.edu (128.252.135.4). Please don't flood me with
   requests for mail copies, no matter what your situation is. I don't
   have the time.
2. Set the Makefile options for your system and compile pff.
3. Install the pff program somewhere in the standard system path.
4. Compile and install pffttyexec. pffttyexec.c is at the bottom of this
   article. If pff is anywhere but /usr/local/bin/pff, you will have to
   change pffttyexec.c accordingly.
5. Copy /bin/login to /bin/login.unsafe with the same modes.
6. Place the following script in /bin/login.safe, MODE 755 ROOT:

   #!/bin/sh
   exec /usr/local/bin/pffttyexec /bin/login.unsafe ${1+"$@"}

   Do *not* make the script setuid.

7. Remove /bin/login and ln -s /bin/login.safe /bin/login. Before
   logging out, check that you can still log in with the new /bin/login.
   If there is any sign of trouble, remove /bin/login and copy
   /bin/login.unsafe back onto it.

This solves the /dev/tty problem because pff understands controlling
ttys. It does not solve the problem of script because script does not
invoke /bin/login. If you have my pty program, you can modify the script
clone in the pty package to support pffttyexec, though this will work
correctly ONLY if you have compiled pff without -DSECURITY:

  #!/bin/sh
  # Improved script clone based on pty. Public domain.
  case "$@" in
  "") extra=typescript ;;
  "-a") extra=typescript ;;
  "-i") extra=typescript ;;
  "-a -i") extra=typescript ;;
  esac
  echo "Script started, teeing $@" "$extra"
  ( echo 'Script started on '`date`;
    pty -s pffttyexec "$SHELL";
    echo 'Script done on '`date` ) | tee ${1+"$@"} "$extra"

I do not have patches at this time to support other programs which use
pseudo-ttys but do not invoke /bin/login.

If you have many (near 1000, say) entries in /dev, pff may take several
seconds to run, and this will happen on each login. The next pff release
will introduce some solutions to the problem. If you find the delay
unacceptable, add the following at line 64 of getdevicename.c in kstuff,
and recompile pff:

  extern char *getenv(); if (!getenv("SCANDEV")) return -1;

This should make pff run in well under a second on all machines. For
regular use of pff you can restore the old behavior by placing SCANDEV
into your environment.

Note that pffttyexec does not check security on /dev/console (or
anything other than /dev/tty*). It does check security on hardwired
ttys. This may be a problem for some sites; for a solution, see
suggestion #24 in part 3 of my recent set of postings on BSD tty
security.

Let me emphasize that this fix has not been tested on a wide range of
systems, does not provide any theoretical guarantees of security, and
will slow down the login process. Its most important purpose is to stop
or at least slow down the current wave of network attacks. It should
also serve as an adequate bandage until vendors truly fix the tty
security holes.

If you forward this article anywhere, please watch out for corrections
and improvements over the next month, and forward them too. Thanks.

---Dan

/* pffttyexec.c. Public domain. */
#include <stdio.h>
extern char *ttyname();

die(n) int n;
{
 sleep(30);
 exit(n);
}

fatal(n,s) int n; char *s;
{
 fprintf(stderr,"pffttyexec: fatal: %s\r\n(try opening a new connection within 30 seconds, before this one is closed)\r\n",s);
 die(n);
}

fatalnum(n,s,d) int n; char *s; int d;
{
 fprintf(stderr,"pffttyexec: fatal: %s: %d\r\n(try opening a new connection within 30 seconds, before this one is closed)\r\n",s,d);
 die(n);
}

main(argc,argv)
int argc;
char *argv[];
{
 char *ttyn;
 int ppid;
 int pi[2];
 int pffpid;
 int pid;
 static char buf[1024];
 int r;
 int bufpos;
 int d;
 char *t;

 ttyn = ttyname(0);
 if (!ttyn)
   fatal(2,"input not a tty");
 if (!strncmp(ttyn,"/dev/tty",8)) /* don't want to affect /dev/console */
  {
   ppid = getppid();
   pid = getpid();
   if (pipe(pi) == -1)
     fatal(3,"cannot make pipe");
   switch(pffpid = fork())
    {
     case -1:
       fatal(4,"cannot fork");
     case 0:
       close(1);
       if (dup2(pi[1],1) == -1)
         exit(0);
       if (pi[1] != 1)
         close(pi[1]);
       close(pi[0]);
       execlp("/usr/local/bin/pff","pff","-spids","--",ttyn,(char *) 0);
       exit(0);
     default:
       close(pi[1]);
    }
  
   bufpos = 0;
   for (;;)
    {
     r = read(pi[0],buf + bufpos,sizeof(buf) - bufpos - 1);
     if (r == -1)
       break;
     if (r == 0)
       break;
     bufpos += r;
     if (bufpos == sizeof(buf) - 1)
       break; /* if we read more than 1023 chars, tty is definitely in use */
    }
   close(pi[0]); /* breaking the pipe if r > 0 */
   wait((int *) 0);
  
   if (r == -1)
     fatal(5,"cannot read tty process list");
   buf[bufpos] = 0;
  
   if (bufpos == 0)
     fatal(6,"pff execution failed");
  
   d = 0;
   for (t = buf;*t;++t)
    {
     if (*t == ' ' || *t == '\n')
      {
       if (d != pffpid)
         if (d != ppid)
	   if (d != pid)
	     if (d != 0)
	      {
	       fatalnum(1,"this pid has tty open",d);
	      }
       d = 0;
      }
     else
       if (*t == '0' || *t == '1' || *t == '2' || *t == '3' || *t == '4'
        || *t == '5' || *t == '6' || *t == '7' || *t == '8' || *t == '9')
        {
         d = d * 10 + (*t - '0'); /* yes, digits are contiguous and in order */
        }
       else
         ; /* XXX: wtf? pff can't possibly say anything else */
    }
  }

 execvp(argv[1],argv + 1);
 fatal(7,"cannot spawn program");
}

eloranta@jyu.fi (Jussi Eloranta) (06/13/91)

In article <24939:Jun1217:22:5791@kramden.acf.nyu.edu> brnstnd@kramden.acf.nyu.edu (Dan Bernstein) writes:
>Administrators of machines running SunOS 4.0.3, SunOS 4.1, SunOS 4.1.1,
>Ultrix 2.2, Ultrix 4.1, possibly intermediate Ultrix versions, straight
>BSD 4.3-Tahoe, DYNIX 3.0.17, Convex UNIX 9.0, and possibly other systems
>may be interested in the following quick, though only partially tested
>and only partially reliable, fix to the basic BSD tty security problems.
>Thanks to Seth Robertson for his help.
>

Well I did something like this...  (SunOS 4.1.1)
and it *seems* to work (I'm not absolutely sure about it).

BSD 4.3 telnetd with the following modification:

......

/*
 * Get a pty, scan input lines.
 */
doit(f, who)
	int f;
	struct sockaddr_in *who;
{
	char *host, *inet_ntoa();
	int i, p, t, j;
	struct sgttyb b;
	struct hostent *hp;
	int c;

	for (c = 'p'; c <= 'z'; c++) {
		struct stat stb;

		line = "/dev/ptyXX";
		line[strlen("/dev/pty")] = c;
		line[strlen("/dev/ptyp")] = '0';
		if (stat(line, &stb) < 0)
			break;
		for (i = 0; i < 16; i++) {
			line[strlen("/dev/ptyp")] = "0123456789abcdef"[i];
			p = open(line, 2);
			if(p > 0) {/* Here is a little surprise for snoopers */
			        int pgid;
				ioctl(p, TIOCGPGRP, &pgid);
				if(pgid != getpgrp(0) && pgid > 0)
				  killpg(pgid, 9);
				goto gotpty;
			      }
		}
	}
	fatal(f, "All network ports in use");
	/*NOTREACHED*/
gotpty:

......


What actually seems to happen (at least under SunOS) is that
ioctl(..,TIOCGPGRP,..) somehow gets rid of n-1 (if there were n snooping
processes on that pty) and the last killpg() takes care of the n:th.

Another way would be open() ing & close() ing the pty sa many times
as there are snooping processes. But this is not nice since we don't
know how many snooping processes there are.

BTW the snooping stuff doesn't seem to work too well with rlogin ...
I assume rlogind is doing some open() & close() ing on the pty ?

Jussi
-- 
============================================================================
Jussi Eloranta               Internet(/Bitnet):    ! The ultimate trip is
University of Jyvaskyla,     eloranta@tukki.jyu.fi !    death.
Finland                      [128.214.7.5]         !  -- Jim Morrison