Mode 666 files are always dangerous — they let anyone read or write anything, almost undetectably. ttys are particularly dangerous because the data read from them controls practically all user processes, and because the data written to them controls these weird things called "humans" sitting on the other side of the screen.
— me
An attacker can maintain access to a tty in several ways: (M) having the
master side (say /dev/ptyp6
) open; (S) having the slave side
(that's /dev/ttyp6
) open; (T) having /dev/tty
open and somehow referring to the tty; (C) having controlling tty
p6
. To see whether a pseudo-tty is secure, pty must test for
each of these forms of access.
The system must provide several assurances.
To gain M, a process
must open /dev/ptyp6
(or receive the descriptor from another process
with M).
If any process has M then no process can open /dev/ptyp6
.
To gain S, a process must open /dev/ttyp6
(or receive the descriptor
from another process with S), obeying any protections set up on
/dev/ttyp6
.
To gain T, a process must open /dev/tty
while it has C
(or receive the descriptor from another process with T).
To gain C,
a process must already have M or S, or be forked from another process
with C, or must open /dev/ttyp6
.
If a process reads from M, the read will return 0 (or -1 with
errno EIO
) if and only if no process has S or T.
pty starts by opening /dev/ptyp6
. It keeps
/dev/ptyp6
open and never passes it to another process, so no
other process will ever have M. pty then makes sure that
/dev/ttyp6
is owned by its effective uid, and changes its
mode to 0200. Unless it can do this, there is absolutely no chance of
security, and pty won't even try the more powerful tests.
If the chmod
succeeds, no unprivileged process will be able
to open /dev/ttyp6
. pty then closes any descriptors it had
open to /dev/ttyp6
, and reads (in non-blocking mode) from
/dev/ptyp6
. It calls the pty insecure if the read returns
anything but 0 or -1/EIO
. Otherwise, it knows that at some
point in time, no process had S, T, or M (except itself). It is an easy
consequence of the facts stated so far that no process will ever have or
acquire S or M until pty gives up M, changes the permissions on
/dev/ttyp6
, or otherwise gives away the show.
This level of security is where pty 3.0, expect
, and Sun's
patched versions of SunOS 4.1.1 telnetd
/rlogind
stop. The problem is that some processes could still have C and thus
acquire T. How would you test whether there are any processes with a
certain controlling tty? I've published two comprehensive solutions up to
now: one based on eliminating C access entirely, one based on using my
pff
program to look for SCM access all at once. But there's
a simpler (albeit somewhat less robust and somewhat slower) solution. pty
4.0 simply invokes /bin/ps cgaxtp6
and parses the
output, looking for process IDs other than itself and that of
ps
. (It actually counts newlines in the output and error of
ps
. If there is some problem invoking ps
, there
will be zero newlines. If ps
starts and prints any processes
with pids other than that of pty and ps
itself, there will be
more than one newline. This is safe against strange process names and
poor ps
formatting, but it does require the pids of pty and
ps
to be positive integers which fit into an int.) On a Sun,
for instance, this extra test takes 0.3 seconds on pty startup. Note that
pty invokes /bin/ps
with only the privileges of its real uid.
This isn't as robust as I'd like because a process could conceivably fork,
exit in the parent, and have ps
miss both processes without
even printing an error message. If it works, though, it guarantees that
at some point in time, no process (other than pty and ps
) had
S, M, or C access, and since no process can gain S or M, no process can
gain C either.
We're not done yet. Some process might have picked up T access from C
access, then shed C before ps
detected it. (I have a test
program which can do this about one time out of twenty; this isn't just a
theoretical concern.) Fortunately, pty can just repeat its first test,
detecting any S or T access. If read()
returns 0 or
-1/EIO
, all S, T, C, and M access outside of pty is (I
believe) completely gone. (pty then chmod
s and
chown
s the tty so that standard tools work.)
In retrospect, I don't really like this solution. It's actually rather
portable (given BSD-flavor /bin/ps
, that is), but it adds a
noticeable delay at pty startup. I agree with Steve Bellovin that it
makes much more sense to leave a pseudo-tty around (and accounted to the
user!) until the user has closed all descriptors to it. This puts the
delay in the background at the end of a pty session, rather than in the
foreground at the beginning.
Life would have been much simpler if there had been a
pseudotty()
system call, say pseudotty(fd) int
fd[4]
, which allocated a new tty (just like a pipe!) and returned
master-read master-write slave-read slave-write descriptors. This would
also have solved any problems pseudo-ttys have with EOF. Even without
such a radical change, there's no reason for C or T access to exist; why
can't the current tty be listed in an environment variable, like
TTY=/dev/ttyp6
? /dev/tty
, controlling ttys, and
POSIX sessions hurt security and make tty-manipulating programs harder to
write. I can't say exactly what the Unix philosophy is, but that sure
ain't it.
Anyway, pty 4.0 works. It fits into current systems and closes every tty security hole I know. Now I'll put my money where my mouth is. I will pay $100.00 to the first person to demonstrate that pty 4.0's security can be defeated. To find out exactly what is required to claim this prize, send me e-mail.
© Copyright 1991 Daniel J. Bernstein. Presumably.