|
Chapter 23 Writing Secure SUID and Network Programs
|
|
Software engineers define
errors
as mistakes made by humans when designing and coding software.
Faults
are manifestations of errors in programs that may result in
failures
. Failures
are deviations from program
specifications
.
In common usage, faults are called
bugs
.
Why do we bother to explain these formal terms? For three
reasons:
-
To remind you that although bugs (faults)
may be present in the code, they aren't necessarily a problem
until they trigger a failure. Testing is designed to trigger such
a failure before the program becomes operational...and results
in damage.
-
Bugs don't suddenly appear in code. They
are there because some person made a mistake - from ignorance,
from haste, from carelessness, or for some other reason. Ultimately,
unintentional flaws that allow someone to compromise your system
are caused by people who made errors.
-
Almost every piece of
UNIX
software
has been developed without comprehensive specifications. As a result,
you cannot easily tell when a program has actually failed. Indeed,
what appears to be a bug to users of the program might be a feature
that was intentionally planned by the program's authors.[5]
When you write a program that will run as superuser or in
some other critical context, you must try to make the program as
bug free as possible because a bug in a program that runs as superuser
can leave your entire computer system wide open.
Of
course, no program can be guaranteed perfect. A library routine
can be faulty, or a stray gamma ray may flip a bit in memory to
cause your program to misbehave. Nevertheless, there are a variety
of techniques that you can employ when writing programs that will
tend to minimize the security implications of any bugs that may
be present. You can also program defensively to try to counter any
problems that you can't anticipate now.
Here
are some general rules to code by:
-
Carefully design the program before you
start.
Be certain that you
understand what you are trying to build. Carefully consider the
environment in which it will run, the input and output behavior, files
used, arguments recognized, signals caught, and other aspects of
behavior. Try to list all of the errors that might occur, and how you
will deal with them. Consider writing a specification document for the
code. If you can't or won't do that, at least consider
writing documentation including a
complete
manual
page before you write any code. That can serve as a valuable exercise
to focus your thoughts on the code and its intended behavior.
-
Check all of your arguments.
An astonishing number of
security-related bugs arise because an attacker sends an unexpected
argument or an argument with unanticipated format to a program or a
function within a program. A simple way to avoid these kinds of
problems is by having your program
always check all of its
arguments
. Argument checking will not noticeably slow down
most programs, but it will make them less susceptible to hostile
users. As an added benefit, argument checking and error reporting will
make the process of catching non-security-related bugs easier.
When you are checking arguments in your program, pay extra
attention to the following:
-
Check arguments passed to your program
on the command line. Check to make sure that each command-line
argument is properly formed and bounded.
-
Check arguments that you pass to
UNIX
system functions. Even though your program is
calling the system function, you should check the arguments to be sure
that they are what you expect them to be. For example, if you think
that your program is opening a file in the current directory, you
might want to use the
index( )
function to see if
the filename contains a slash character (/). If the file does
contain the slash, and it shouldn't, the program should not open
the file.
-
Check arguments passed in environment variables
to your program, including general environment variables and such
variables as the
LESS
argument.
-
Do bounds checking on every variable. If
you only define an option as valid from 1 to 5, be sure that no one
tries to set it to 0, 6, -1, 32767, or 32768. If string arguments are
supposed to be 16 bytes or less, check the length
before
you copy them into a local buffer (and
don't forget the room required for the terminating null
byte). If you are supposed to have three arguments, be sure you got
three.
-
Don't use routines that fail to
check buffer boundaries when manipulating strings of arbitrary
length.
In the C programming language particularly, note the
following:
Use the following library calls with great
care - they can overflow either a destination buffer or an
internal, static buffer on some systems if the input is "cooked"
to do so:
[6]
sprintf( ),
fscanf( ), scanf( ), sscanf( ), vsprintf( ), realpath( ), getopt(
), getpass( ), streadd( ), strecpy( ),
and
strtrns( )
. Check to make sure that you have the version of the
syslog() library which checks the length of its arguments.
There may be other routines in libraries on your system of
which you should be somewhat cautious. Note carefully if a copy
or transformation is performed into a string argument without benefit
of a length parameter to delimit it. Also note if the documentation
for a function says that the routine returns a pointer to a result
in static storage. If an attacker can provide the necessary input
to overflow these buffers, you may have a major problem.
-
Check all return codes from system
calls.
The
UNIX
operating system has almost every single
system call provide a return code. Even system calls that you think
cannot fail, such as
write()
,
chdir()
, or
chown()
, can fail under exceptional
circumstances and return appropriate return codes.
When the calls fail, check the
errno
variable
to determine
why
they failed. Have your program
log the unexpected value and then cleanly terminate if the system
call fails for any unexpected reason. This approach will be a great
help in tracking down problems later on.
If you think
that a system call should not fail and it does, do something appropriate.
If you can't think of anything appropriate to do, then
have your program delete all of its temporary files and exit.
-
Don't design your program
to depend on
UNIX
environment
variables.
The simplest way to write a secure program
is to make absolutely no assumptions about your environment and
to
set everything explicitly
(e.g. signals,
umask, current directory, environment variables). A common way of
attacking programs is to make changes in the runtime environment
that the programmer did not anticipate.
Thus, you want to make certain that your program environment
is in a known state. Here are some of the things you want to do:
-
If you absolutely must pass information
to the program in its environment, then have your program test for
the necessary environment variables and then erase the environment
completely.
-
Otherwise, wipe the environment clean of all but
the most essential variables. On most systems, this is the
TZ variable that specifies the local time zone,
and possibly some variables to indicate locale. Cleaning the environment
avoids any possible interactions between it and the
UNIX
system libraries.
-
You might also consider constructing a new
envp
and passing that to
exec()
, rather than using even a scrubbed
original
envp
. Doing so is safer because you explicitly create the
environment rather than trying to clean it.
-
Make sure that the
file
descriptors that you expect to be open are open, and that the file
descriptors you expect to be closed are closed.
-
Ensure that your signals are set to a sensible state.
-
Set your umask appropriately.
-
Explicitly
chdir
()
to an appropriate directory when the program starts.
-
Set whatever limit values are necessary so that
your program will not leave a
core
file if it fails. Consider setting your other limits on number of
files and stack size to appropriate values if they might not be
appropriate at program start.
-
Have internal consistency-checking
code.
Use the
assert
macro if you are programming in C. If you have a variable that you
know should either be a 1 or a 2, then your program should not be
running if the variable is anything else.
-
Include lots of
logging.
You are almost always better having too much logging
rather than too little. Report your log information into a dedicated
log file. Or, consider using the
syslog
facility, so that logs can
be redirected to users or files, piped to programs, and/or
sent to other machines. And remember to do bounds checking on arguments
passed to
syslog()
to avoid buffer overflows.
Here is specific information that you might wish to log:
-
The time that the program was run.
-
The
UID
and effective
UID
of the process.
-
The
GID
and effective
GID
of the process.
-
The terminal from which it was run.
-
The process number (
PID
).
-
Command-line arguments.
-
Invalid arguments, or failures in consistency
checking.
-
The host from which the request came (in the case
of network servers).
-
Make the critical portion of your program
as small and as simple as possible.
-
Read through your code.
Think of how
you might attack it yourself. What happens if the program gets unexpected
input? What happens if you are able to delay the program between
two system calls?
-
Always use full
pathnames
for any filename argument, for both commands and data
files.
-
Check anything supplied by the user
for shell meta characters if the user-supplied input is passed on
to another program, written into a file, or used as a filename.
In general, checking for good characters is safer than checking
for a set of "bad characters" and is not that
restrictive in most situations.
-
Examine your code and test it carefully for assumptions
about the operating environments. For example:
-
If you assume that the program is
always run by somebody who is not
root
, what happens if the program
is run by
root?
(Many
programs designed to be run as
daemon
or
bin
can cause security
problems when run as
root
, for instance.)
-
If you assume that it will be run by
root
, what
happens if it is not run as root?
-
If you assume that a program always runs in the
/tmp
or
/tmp/root
[7]
directory, what happens if it is run
somewhere else?
-
Make good use of available tools.
If
you are using C and have an
ANSI
C compiler available,
use it, and use prototypes for calls. If you don't have
an
ANSI
C compiler, then be sure to use the
-Wall
option to your C compiler (if supported) or the
lint
program to check for common mistakes.
-
Test your program thoroughly.
If you have a system
based on
SVR4
, consider using (at the least)
tcov
, a statement-coverage tester. Consider using commercial products,
such as CodeCenter and Purify (from personal experience, we can
tell you that these programs are very useful). Look into
GCT
,
a test tool developed by Brian Marick at the University of Illinois.[8]
Remember that finding a bug in testing is better than letting some
anonymous system cracker find it for you!
-
Be aware of
race conditions.
These can be manifest as a deadlock, or as failure of two calls
to execute in close sequence.
-
Deadlock conditions.
Remember: more than one copy of your program may be running at the
same time. Consider using file locking for any files that you modify.
Provide a way to recover the locks in the event that the program
crashes while a lock is held. Avoid deadlocks or "deadly
embraces," which can occur when one program attempts to
lock file A then file B, while another program already holds a lock
for file B and then attempts to lock file A.
-
Sequence
conditions
. Be aware that your program
does not execute atomically. That is, the program can be interrupted
between any two operations to let another program run for a
while - including one that is trying to abuse yours. Thus, check
your code carefully for any pair of operations that might fail if
arbitrary code is executed between them.
In particular,
when you are performing a series of operations on a file, such as
changing its owner,
stat'
ing the file, or
changing its mode, first open the file and then use the
fchown()
,
fstat()
, or
fchmod()
system
calls. Doing so will prevent the file from being replaced while your
program is running (a possible race condition). Also avoid the use of
the
access()
function to determine
your ability to access a file: Using the
access()
function
followed by an
open()
is a race condition, and almost always a
bug.
-
Don't have your program dump core except during
your testing.
Core files can fill up a filesystem. Core
files can contain confidential information. In some cases, an
attacker can actually use the fact that a program dumps core to break
into a system. Instead of dumping core, have your program log the
appropriate problem and exit. Use the
setrlimit
() function
to limit the size of the core file to 0.
-
Do not provide shell escapes (with job control,
they are no longer needed).
-
Never
use
system()
or
popen()
calls.
Both invoke the shell, and can have unexpected
results when they are passed arguments with funny characters, or
in cases in which environment variables have peculiar definitions.
-
If you are expecting to create a new file with the
open call, then use the O_EXCL |
O_CREAT flags
to cause the routine to fail if the file exists.
If you expect the file to be there, be sure to omit the
O_CREAT flag so that the routine will fail if the file is not
there.[9]
-
If you think that a file should be a file, use
lstat()
to make sure that it is
not a link.
However, remember that what you check may change before you can
get around to opening it if it is in a public directory. (See item
)
-
If you need to create a temporary file, consider
using the
tmpfile( )
or
mktemp( )
function.
This step will
create a temporary file, open the file, delete the file, and return
a file handle. The open file can be passed to a subprocess created
with
fork( )
and
exec( )
,
but the contents of the file cannot be read by any other program
on the system. The space associated with the file will automatically
be returned to the operating system when your program exits. If
possible, create the temporary file in a closed directory, such
as
/tmp/root/
.
NOTE:
The
mktemp()
library call is not safe to use in a program that is running with
extra privilege. The code as provided on most versions of
UNIX
has a race condition between a file test and a
file open. This condition is a well-known problem, and relatively easy
to exploit. Avoid the standard
mktemp()
call.
-
Do not create files in world-writable directories.
-
Have your code reviewed by another competent programmer
(or two, or more).
After they have reviewed it, "walk
through" the code with them and explain what each part
does. We have found that such reviews are a surefire way to discover
logic errors. Trying to explain why something is done a certain
way often results in an exclamation of "Wait a moment ...why
did I do
that?"
-
If you need to use a shell as part of your program,
don't use the
C shell.
Many versions have known flaws that can be exploited, and
nearly every version performs an implicit
eval $TERM
on
start-up, enabling all sorts of attacks. Furthermore, the C shell
makes it difficult to do things that you may want to do, such as
capture error output to another file or pipe.
We recommend the use of
ksh93
(used for most of the shell scripts
in this book). It is well designed, fast, powerful, and well
documented (see
Appendix D
).
Remember, many security bugs are actually programming bugs,
which is good news for programmers. When you make your program more
secure, you'll simultaneously be making it more
reliable.
|