BSD tty security, part 3: How to Fix It

Dan Bernstein brnstnd at kramden.acf.nyu.edu
Thu Apr 25 20:22:20 AEST 1991


Here's one way to fix the BSD 4.[234] tty system, i.e., to provide some
strong guarantees that pty and tty sessions are safe and not subject to
corruption or denial of service, with minimal changes to the kernel and
to application programs. This is also meant to apply to systems derived
from BSD, such as SunOS, Ultrix, etc.

I've included quite a bit of sample code here, as well as evaluations of
what effect these changes will have on users and old programs. Thanks in
particular to Marc Teitelbaum for his extensive comments. The second
half of the article includes a bunch of optional recommendations that
may make your life easier but are not necessary for security.

Quick summary of kernel changes required: Make /dev/tty ioctls work on
/dev/tty??, make a /dev/stdtty driver which simply dup()s fd 3, and add
an ioctl, TIOCOPENCT, which returns the number of active references to a
given inode. That's it.

Quick summary of application changes required: Have certain programs do
an extra open() of the slave side to fd 3, move two device drivers, add
about fifteen lines of code (forty with complete error checks) to those
programs, add a new uid and group, make /dev/[pt]ty* world-inaccessible,
change chmod()s in those programs so that /dev/[pt]ty* remain
world-inaccessible, and make various programs setuid or setgid. That's
it.

1. Make all /dev/tty-specific ioctls work upon /dev/tty??. If the only
such ioctl is TIOCNOTTY, this is not necessary unless you want to
preserve the programmer's interface to detachment (which is probably
necessary). This may take some work. This step is safe, in that it will
not break working code.

2. Set up a /dev/stdtty driver that dup()s fd 3. This is tedious but not
difficult in principle. On systems with /dev/fd/3, all you have to do is
ln /dev/fd/3 /dev/stdtty. This step is safe.

3. Add an ioctl---I propose ioctl(fd,TIOCOPENCT,&x)---which *reliably*
sets x to the number of references to the *file* (not open file: I mean
file on disk, i.e., dev/inode pair, i.e., [igv]node) given by fd, or -1
if fd is not a disk file. Here ``reference'' means open file (i.e., the
thing in the file table). Under NFS I believe it is sufficient to report
v->v_count of the vnode. ``Reliably'' means that no matter what is going
on---swapped processes, locks of all sorts on the inode, file descriptor
passing, opening and closing---the returned information will be
absolutely correct starting from when ioctl() finishes and continuing as
long as no process opens or closes the file in question. This step is
safe.

4. Make sure that each of the tty-handling programs---getty, telnetd,
rlogind, script, etc.---opens /dev/ttyxx again in the master process and
leaves it open for use in #9 below. ``Master process'' means the process
in charge of the master side of the pty---telnetd, for instance. This is
easy:

	int fdttyagain; /* global variable */
	...
	/* in the parent right after fork */
	fdttyagain = open(line,O_RDWR);
	if (fdttyagain == -1)
	  syslog(LOG_CRIT,"cannot open %s again: %m",line);
	  /* or whatever your favorite error reporting method is */

This step is safe.

5. Make a new uid, pty. Make each of the non-root tty-handling programs
(that means script, as well as programs like atty, mtty, pty, etc. if
you have them installed) setuid pty, and make sure they reset uids
before executing anything. Do not make pty the same as root, unless your
system handles MAXUPRC by effective userid (ugh)---in that case you
can't safely run anything setuid to any user but root, and you should
complain to your vendor. (The latter is true under, e.g. Ultrix 3.1.)
This step is safe, but will take some work if you have many non-root
tty-handling programs.

6. Change the root tty-handling programs (e.g., telnetd) so that they
reset ttys to owner pty mode 600 rather than owner root mode 666. This
step will break any user programs that allocate ttys dynamically and
that you didn't take care of in #5. It is safe otherwise.

7. Have each of the tty-handling programs---getty, telnetd, rlogind,
script, etc.---open file descriptor 3 to the tty. This is trivial:

	{ /* after closing other descriptors, right before exec'ing the slave */
	 int fdtty;
	 fdtty = open(line,O_RDWR); /* line is, e.g., "/dev/ttyp7" */
	 if (fdtty == -1)
	   ; /* XXX: complain to the user, or exit */
	 else if (fdtty != 3)
	  {
	   if (dup2(fdtty,3) == -1)
	     ; /* XXX: complain to the user, or exit */
	   close(fdtty);
	  }
	}

This step will break any old code that assumes the first open() will
return 3. (Such code is disgusting, but this is beside the point.)

8. ln /dev/tty /dev/oldtty; rm /dev/tty; ln /dev/stdtty /dev/tty;
chmod 600 /dev/oldtty. This is the first change that will affect users
directly. However, if you have done steps 1, 2, and 7 correctly, nobody
will notice. Marc comments that any programs which redirect or close fd
3 will be affected if they later use /dev/tty; he couldn't think offhand
of any such programs except ksh, which isn't installed on most BSD
machines. If you do find further examples of such programs or scripts,
please post the fixes here. An alternative is to use fd 11 instead of
fd 3 throughout these changes; this won't help ksh, but I've never seen
a script use fd 11.

9. In each of the tty-handling programs, do the following upon slave
exit: (a) Clean up everything except (if it is convenient) [uw]tmp.
Close 0, 1, 2, and any other random descriptors lying around, except
/dev/ptyxx and /dev/ttyxx. (b) Test /dev/ttyxx with TIOCOPEN*. If
someone else still has it open, continue to step (c); otherwise skip to
step (d). (c) Fork, and exit in the parent. Repeatedly test /dev/ttyxx
(a five-second sleep is fine) until it is closed. (d) Clean up [uw]tmp
and exit. Note that steps (b) and (c) can fit into a simple library
routine. Here's sample code, with paranoid error checking:

	/* after cleaning up mostly everything */
	if (fdttyagain != -1)
	 {
	   /* Assumption: /dev/ttyxx is back to mode 600 owner pty. */
	  int count;
	  close(0); close(1); close(2);
	  ... /* close any other descriptors previously opened */
	      /* _except_ /dev/ptyxx (``fdpty'', perhaps) and fdttyagain */
	  (void) ioctl(fdttyagain,TIOCEXCL,(char *) 0);
	    /* entirely optional, but better safe than racing */
	    /* if TIOCOPENCT is not completely reliable */
	  if (ioctl(fdttyagain,TIOCOPENCT,&count) == -1)
	    syslog(LOG_CRIT,"cannot count open references to %s: %m",line);
	    /* or whatever your favorite error reporting method is */
	  else
	    if (count > 1)
	     {
	      syslog(LOG_INFO,"waiting on %s",line);
	      switch(fork())
	       {
	        case -1:
	 	  syslog(LOG_CRIT,"cannot fork to wait on %s: %m",line);
	 	  break;
	        case 0:
	 	   {
	 	    int i;
	 	    i = 0;
	 	    for (;;)
	 	     {
		      sleep(5);
		      if (ioctl(fdttyagain,TIOCOPENCT,&count) == -1)
	                syslog(LOG_CRIT,"weird: cannot count open references to %s: %m",line);
		      else
		        if (count == 1)
		 	  break;
		      ++i;
		      if (!(i % 1000))
		        syslog(LOG_INFO,"waited %d secs on %s",i * 5,line);
		      /* XXX: If i gets large enough, you may want to take */
		      /* desperate measures at this point. Example: */
		      /* vhangup(); fcntl(fdpty,F_SETFL,FNDELAY); */
		      /* vhangup(); write(fdpty,"x",1); vhangup(); */
		      /* read(fdpty,"y",1); vhangup(); */
		      /* And then break. */
		     }
		   }
		  syslog(LOG_INFO,"done waiting on %s",line);
		  break;
	        default:
	  	  exit(0);
 	       }
	     }
	  /* now finish cleaning up everything, and exit */
	 }

It doesn't really matter where the above code comes inside a cleanup
routine, as long as the tty already has the right modes. I think it's
aesthetically better to leave the utmp entry alone until the tty is
deallocated; but if this isn't convenient for some program, feel free to
ignore aesthetics and put the code right before exit().

Marc notes that this change will leave a pseudo-tty allocated to a user
as long as the user has a background process on the tty. Religious types
will say ``yes, that's how it should be.'' I say that at sites I'm
familiar with, this isn't a problem, because users don't run very many
background jobs, and there are more than enough pseudo-ttys. If this is
a problem for you, you will have to do step #20 below and educate your
users to detach background jobs, meanwhile killing any runaways. Sorry,
but this is the price you pay for security. You may prefer the
``desperate measures'' mentioned in the sample code to simply cut off
tty access after a few hours; any use of vhangup() is chock-full of race
conditions, but it would be exceedingly difficult for a process to make
it past all the races.

10. chown pty /dev/[pt]ty*; chmod 600 /dev/[pt]ty*. This is the big step.
Nonprivileged programs will no longer be able to open any ttys or ptys,
so nobody can deny service to other users without executing a privileged
program that will later show up in acct. Furthermore, the TIOCOPENCT
code guarantees that if a tty-handling program exits, absolutely nobody
is using that tty, so it is safe for immediate use by the next
tty-handling program. This throws a huge wrench into all the fundamental
tty security holes I know.

11. If you're using a Sun, make sure to chmod 600 /etc/utmp, or these
changes will go to waste. You may find it convenient to make certain
programs setgid or setuid here so that they can still write utmp, though
I consider this a mistake---you are bound to slip up when a hundred
different tools all manage one supposedly secure file. (But anything is
better than what Sun currently ships.)

12. Support the BSD 4.3 tty group model: make a new group, tty, chgrp
all /dev/tty* to it, and make ``talk'' and ``write'' setgid tty. Of
course, you don't need to do this if you already have the tty group.

It's possible to accomplish similar results with fewer changes. In fact,
my next version of pty will almost guarantee safety on stock BSD 4.2
systems with no kernel support except read access to /dev/kmem. (It is,
unfortunately, not possible to avoid race conditions from user code.)
You can, for example, place the burden of TIOCOPENCT checking upon the
program opening the tty, rather than the program closing it, so that
it's not a problem if one tty handler fails to do its job; but this
increases turnaround time for the users and allows denial-of-service
attacks. The above changes should be straightforward enough that halfway
solutions are not worthwhile.

(POSIX fans will note that using TIOCOPENCT to keep the tty allocated
past session leader exit *is* compliant: it only affects the secure BSD
exclusive lock on the master side, and does not prevent reassignment of
the slave tty to a new session---not that such a reassignment will ever
occur.)

Why must tty-handling programs be setuid rather than setgid? Because the
user must not be allowed to kill them---he would be able to retain tty
access that way.

There are many further changes you can make to eliminate minor security
holes or improve accounting. EVERYTHING BELOW THIS POINT IS COMPLETELY
OPTIONAL. Here are some of the most important:

13. Fix write. Many people don't appreciate how poor write's security
is; I quote from my pty paper's description of a write clone:

: Finally, write is a vastly improved clone. The old write had several big
: security holes: 1. Control characters were passed through. This version
: converts anything unprintable into a caret. 2. Lines were not
: distinctively marked. A user could manually simulate the ``EOT'' or
: ``EOF'' sequence, wait a few minutes, then start sending anything to the
: other tty without identification. This version precedes each line with
: the name of the sending user, and prints something more informative than
: EOT for an ended message. 3. write could be used to flood a terminal.
: (This is an accident waiting to happen.) This version puts a one-second
: pause between each line and restricts line length. 4. Originally, write
: would only check the protection on the tty being written to. But this
: meant that a user could be interrupted by someone hiding behind mesg n
: and have no recourse. (Footnote: Remember that UNIX has no enforce()
: call to enforce new permissions on an object. Setting mesg n does not
: stop a write in progress.) So many versions of write included
: ``revenge'': X was allowed to write to Y only if Y could write back.
: However, these versions tested tty protection only at the beginning of a
: message---which was useless. This version does the correct test: it
: simply checks write permission before sending each new line.

My write clone is public-domain, so I invite you---I beg you---to steal
code from it. Don't even give me any credit, just fix the bugs. Please.

14. Make script grok utmp and wtmp. (You may have to rethink certain
wtmp-based accounting schemes to do this.) Users constantly complain
that they can't ``talk'' within script, and the lack of accounting
is annoying. This doesn't matter under #18.

15. Change the chown() and fchown() system calls so that files can be
chowned between uid and euid. This opens up chown() for lots of secure
services without forcing the servers to run as root. In this case, it
lets script change the tty owner properly. This doesn't matter, though,
if you implement #16.

16. Don't even bother chowning ttys to the users who own them. (At this
point they might as well not be in the filesystem.) Yes, you can make
biff and mesg setuid pty, and no, nothing breaks except nroff's mesg n.

17. Make sure that telnetd, rlogind, etc. leave ttys with messages *off*
by default. Since UNIX has no way to enforce new access permissions on a
file, the usual default leaves all users open to instant attack. This is
a huge problem in the real world (at universities, at least), and while
there may be a sane argument for having messages on by default, it
cannot justify what amounts to unrestricted output to any and all ttys.

Finally, here are some optional changes that will make the above changes
much easier, or that will add basic features to your system. Do them
first and you'll never regret it.

18. Provide a program that spawns another program under a pseudo-tty,
handling I/O and job control transparently, and obeying all the rules
for tty handlers mentioned above. In fact, the program already exists by
the name of ``pty'' (see, e.g., comp.sources.unix volume 23), and its
author is quite willing to negotiate distribution terms. pty also
supports session management. (Isn't it embarrassing to explain UNIX to a
long-time VMS user? ``No, sorry, Bob, you can't get back to that shell
after your modem went on the fritz. The shell is gone.'' ``vi -r? Oh,
yeah, Bob, that means your connection got hung up.'' ``Nope, sorry, Bob,
you can't start recording a `talk' session without hanging up and
talking again.'' ``No, Bob. This is not VMS. Your process is stuck to
that terminal, right there. Yes, I understand, the terminal screen just
exploded, and you can't see your output. No, you cannot move to the next
terminal and continue work. Sorry, Bob, you're out of luck. Bye, Bob.'')

19. Rewrite all other tty handlers (it's easy---trust me) to invoke that
single, modular program. Don't you find it strange that a dozen programs
all have the same pty allocation code? Don't you find it unreasonable,
at least from a ``software engineering'' standpoint, that since people
are too lazy to do (e.g.) random tty searching every time they recopy
the same code, your average tty handler wastes several dozen open()s on
a big machine when it could use just two? In fact, I'll be glad to do
all the work of conversion for you, provided that you agree to make the
final version available for people to use.

20. Provide a program that detaches from its controlling tty and spawns
another program. The program is usually called ``detach'' and has no
options of note. It should also seek out and reopen("/dev/null") any
file descriptors pointing to any tty.

21. Delete /dev/oldtty and remove the /dev/tty driver from your kernel.
Also remove controlling ttys entirely. Also remove POSIX sessions if you
have them: make setsid() a no-op, and return an implementation-defined
error upon the pointless POSIX SIGCONT special case, and you even retain
POSIX compatibility if you had it. (Well, with one exception: POSIX
defines a foreground process in terms of its controlling terminal,
rather than the terminal it's trying to access as in BSD. Beg the POSIX
folks to make this rule optional---what do they lose?) Notice how much
extra space users get for running programs.

22. Make a stdio stream for stdtty, descriptor 3---after all, programs
do want to use it. Change csh and more to read from stdtty rather than
stderr. Someday you may even be able to open stderr write-only [gasp!].

23. Have accounting record the dev/inode pair on descriptor 3. In fact,
while you're making accounting work sensibly, record the pid, as well as
the dev/inode pair of the program run (if you can get that information).

24. Change getty to spawn the pty-handling program, and to disconnect
that session when it receives BREAK. Guess what? You've just set up a
trusted path.

Most of the recommendations here come from my pty paper, various drafts
of which have been available for many months. (TIOCOPEN* is new.) The
basic ideas come from Bellovin's ``Session Tty'' paper, which has been
available for years. If you get through all of the above and still want
to improve the tty system, you might get some further ideas out of those
papers. See pub/hier/pty/paper.9 on stealth.acf.nyu.edu and its
references for details.

---Dan



More information about the Comp.unix.wizards mailing list