The following is a current check list, in short form, for quick reference by lab engineers to use in writing secure Unix code. This material is excerpted from [or adapted from] Chapter 23 (page 701) of Practical UNIX and Internet Security by Simson Garfinkel and Gene Spafford, O'Reilly & Associates, Sebastopol, CA, 1996, To order, call 1-800-998-9938. You can also get more information about the book from the WWW URL: http://www.ora.com/item/pus2.html Also, included are inputs from the paper "Enhancing Security of Unix Systems" by Danny Smith of the Australian Computer Emergency Response Team. Thanks to both Gene and Danny for editing this list. This material is copyright 1996 by O'Reilly & Associates and by AUSCERT, The University of Queensland, and copies may only be made provided that: 1) All copies include the copyright notice and full credit information 2) No charge may be made for copies 3) The material is not to be included as part of any other document, collection or work without express permission of the copyright holders. A Lab engineers check list for writing secure Unix code ------------------------------------------------------- Rev.3C 5/23/96 Writing Secure SUID and Network Programs The answer is sublime: although the Unix security model is basically sound, programmers are careless. Most security flaws in Unix arise from bugs and design errors in programs that run as root or with other privileges, or through unanticipated interactions of such programs. Tips on Avoiding Security-Related Bugs When you write a program that will run as superuser or in some other critical context, it is important that you try to make the program as bug-free as possible. Here are some general rules to code by: 1. Carefully design the program before you start. 2. Check all of your arguments. When checking arguments in your program, here are specific places to which you should pay attention: * Arguments passed to your program on the command line. * Arguments that you pass to Unix system functions. * Do bounds checking on every variable. * Data should be bounded, and verified for syntactic correctness integrity, and origin if possible. * Arguments you (foolishly) get from the environment * Arguments/input you read from a file * Typecast variables to what you expect and verify input is correct for that type (e.g., passing a large integer to a signed int causing it to become negative when you really wanted it to be used as positive) 3. Don't use routines that fail to check buffer boundaries when manipulating strings of arbitrary length. AVOID USE INSTEAD gets fgets strcpy strncpy strcat strncat sprintf bcopy scanf bzero sscanf memcpy, memset Always do your own variable size calculations correctly. i.e., the destination variable must be large enough for the stated size being moved. NEVER assume that the string is '\0' terminated correctly. Bounds check all data manipulation. NEVER forget that a string has a '\0' character on the end. Therefore, the string "abcde" is *6* bytes long, not 5 (although strlen says 5)! 4. Check all return codes from system calls. The Unix operating system has almost every single system call return a return code. Do not always assume it will succeed. Some failures are under the control of the user (such as running out of process descriptors, file descriptors, free memory, and so on). 5. Don't have your program depend on Unix environment. Some of the things you want to do, therefore, are: * 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. Erase what you don't need first, get what you want, and kill the remainder. * Otherwise, simply wipe the environment clean of all but the most essential variables. * Make sure that the file descriptors that you expect to be open are open and that the file descriptors that you expect to be closed are closed. All unnecessary file descriptors should be closed before calling exec(). * 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. * Pass a sensible environment to exec() (not the default environment). 6. Have internal consistency checking code. For example, use the assert macro if programing in C. NOTE: that this is not good in production code as it can generate a core dump and/or leak potentially sensitive information (such as line numbers or assertion expressions). It is much more designed for debug code that can be selectively enabled or disabled. If SIGABRT is not caught, a core dump can be generated. The concept of assert macro is good, and the programmer may wish to design their own for production purposes that cleans up, reports sanitised log information, and exits gracefully. 7. Include lots of logging. Here is specific information that you might wish to log: * The time that the program was run. * The UID and effective UID (also, GID information) of the process that ran it. * The terminal from which it was run. * The process number (PID). * Command line arguments. * Invalid arguments, or failures in consistency checking. * Beware of syslog overflow problems. Restrict the size of the total message that is passed to syslog. 8. Make the critical portion of your program as small as possible and as simple as possible. 9. Read through your code. Think of how you might attack it yourself. What happens if you provide unexpected output? What happens if you are able to delay the program arbitrarily between two system calls? 10. If your program needs to perform some functions as superuser, but generally does not require SUID permissions, consider putting the SUID part in a different program, and constructing a carefully-controlled and monitored interface between the two. 11. If you need SUID or SGID permissions, use them for their intended purpose as early in the program as makes sense, and then revoke them by returning the effective UID and effective GID to those of the process that invoked the program. 12. If you have a program that absolutely must run as SUID, try to avoid equipping the program with a general-purpose programming interface. 13. If your program needs special permissions (for example, a game program that writes high scores into a particular directory), consider having it run SUID to a normal user account other than root. 14. Resist the temptation to make something SUID to root if you can accomplish the same goals by making it SGID to a new group. 15. Always use full path names for any argument, for both commands and files. 16. Anything supplied by the user that is then passed on, written into a file, or used as a filename should be checked for shell meta characters. * Don't just check for things like ../ as this can be foiled by using tricks such as "."./ if parsed by a shell. * Better to check for "valid" characters and discard the remainder. 17. 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? * If you assume that it will be run by root, what happens if it is not run as root? * Many programs designed to be run as daemon or bin can cause security programs when run as root. * If you assume that it is always run in the /tmp directory, what happens if it is run somewhere else? 18. Make good use of available tools. If you are using C and have an ANSI compiler available, use it, and use prototypes for calls. If you don't have an ANSI C compiler, then be sure to use the lint program to check for common mistakes. 19. Test your program a lot. 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. Remember that it is better to find a bug in testing than to let some anonymous system cracker find it for you! 20. Be aware of race conditions. These can be manifest as a deadlock, or as failure of two calls to execute in close sequence. Deadlock: 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 and "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: Be aware that your program does not execute atomically. That is, the program can be interrupted between any two operations and 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. The pairs, "stat"... "open" and "open"..."chown" are especially notorious. * The program environment should *NEVER* be left in a vulnerable state even for a single instruction (as above). 21. Don't have your program dump core. Instead of dumping core, have your program log the appropriate problem and exit. 22. Avoid shell escapes. 23. NEVER use system() or popen() calls. execlp and execvp are also suspect. 24. 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 not to include the O_CREAT flag so that the routine will fail. 25. When performing a series of operations on a file, such as changing its owner, stating the file, or changing its mode, first open the file and then use the fchown(), fstat(), or fchmod() system calls. This will prevent the file from being replaced while your program is running (a possible race condition). 26. 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 (cf#20., above). To open a file, which should already exist: - lstat() the path, check that lstat succeeded - check that it's acceptable (eg, not a symlink) - open() (without O_CREAT), check that the open succeeded - fstat() the fd returned by open - if the lstat and fstat st_ino and st_dev fields match, accept. To create a new file, which doesn't already exist: - lstat() the path, check that you got ENOENT - open(...,...|O_CREAT|O_EXCL,...), check that it succeeded If you're really paranoid, then: - fstat() the fd returned by open - lstat() the path again, check that (a) it exists and (b) isn't a symlink - check that the fstat and the last lstat returned matching st_dev and st_ino fields NOTE: that the latter depends on the O_CREAT|O_EXCL semantics of not following a trailing symlink. ....................................................................... Item Subject: Sec 2 27. If you need to create a temporary file, consider using the tmpfile() function. This will create a temporary file, open it, delete the file, and return a file handle. 28. Error recovery. A privileged program can never assume that operations will succeed due to its privileged status. Recovery should not be attempted unless the recovery is guaranteed. 29. Be aware that /etc/utmp file (which is writeable on some systems) can be exploited by careful modification, to write over any file on the system, and thus gain privileged access. If you must use information from utmp, verify it is correct before use.(E.g., if you expect to write to a tty, then the file should be something like /dev/ttyxx). 30. Dynamically linked libraries. It is possible to replace a system library with a user created library, if a SUID program calls a non-SUID program while still running privileged. 31. Files that exist outside of the chroot space, that are required by the program, must be accessed before the call to chroot(). Writing SUID/SGID Programs 1. "Don't do it. Most of the time it's not necessary." 2. Never write SUID shell scripts. 3. If you are using SUID to access a special set of files, don't. Instead, create a special group for your files and make the program SGID to THAT GROUP. If you must use SUID, create a special user for the purpose. 4. Erase the execution environment, if at all possible, and start afresh. Many security problems have been caused because there was a significant difference between the environment in which the program was run by an attacker and the environment under which the program was developed. The entire environment in which the process will be run verified and reset by the program. This may involve setting known values in environment variables like HOME, PATH, and IFS, setting a valid umask value, and initializing all variables. (Note: if your version of UNIX will allow multiple values in the environment with the SAME name, then simply using "getenv" and "putenv" for each value is insufficient. You either need to clobber the whole environment and start fresh, or you need to do multiple instances of "getenv" to ensure that there are not multiple copies of sensitive variables in the environment.) 5. Avoid using system() and popen(). If you are using perl, try to avoid using pipes. "If you need to spawn extra processes, use "fork" with only the execve(2),exec(3) or execl(3) calls, and use them with great care. (Avoid the execlp(3) and execvp(3) calls.) 6. Do not provide shell escapes. If you must provide them, be sure to setgid(getgid()) and setuid(getuid()) before executing the user's command. You may need to do a fork first if you expect to stay around for additional processing. 7. In general, use the setuid() and setgid() functions to "privilege bracket" your code. Only turn on the setuid() and/or setgid() privilege when it is needed. 8. If you must use pipes or subshells, be especially careful with the environment variables PATH and IFS. If at all possible, erase these variables and set them to safe values. For example: putenv ("PATH=/bin:/usr/bin:/usr/ucb"); NOTE: Ensure that the PATH specification is not /bin:/usr/bin:/usr/ucb: (a trailing ":" or empty path name such as "::") as this implicitly adds a "." to the PATH. putenv ("IFS= \t\r"); See the Note: under #4 9. Use the full path name for all files that you open. Do not make any assumptions about the current directory. (You can enforce this requirement by doing a chdir("/tmp") as one of the first steps in your program.) 10. If you must run as root but only need access to files in a specific directory, You should chdir() to it first, then chroot() to that directory. Also, you need to beware in case you need other files such as /dev/null, /dev/zero, and /dev/log to name a few. 11. Consider using taintperl for your SUID programs and scripts. 12. The entire environment in which the process will be run should be verified and reset by the program. This may involve setting known values in environment variables like HOME, PATH, and IFS, setting a valid umask value, and initializing all variables. PASSWORDS: 1. Don't echo the password as the user types it. 2. When you store the user's password on the computer, encrypt it. If nothing else, use the crypt(3) library function. Use random numbers to choose the password's salt. When the user provides a password, check to see if it is the original password by encrypting the provided password with the same salt. 3. If you need access to crypt(3) from a shell script, consider using /usr/lib/makekey, which provides much the same functionality. Generating Random Numbers: A great deal is known about random numbers. Here are some general rules of thumb: * If a number is random, then each bit of that number should have an equal probability of being a 0 or a 1. * If a number is random, then after each 0 in that number there should be an equal probability that the following bit is a 0 or a 1. Likewise, after each 1 there should be an equal probability that the following bit is a 0 or a 1. * If the number has a large number of bits, then there roughly half of the number's bits should be 0s, and half of the bits should be 1s. For security-related purposes, a further requirement for random numbers is unpredictably: * It should not be possible to predict the output of the random number generator given previous outputs or other knowledge about the computer generating the random numbers. * It should not be possible to determine the internal state of the random number generator. * It should not be possible to replicate the initial state of the random number generator, or to reseed the generator with the same initial value. Picking a Random Seed 1. Seeding a random number generator from a limited space. 2. Using a hash value of only the current time as a random seed. 3. Divulging the seed value itself. 4. Combining some rapidly and constantly changing fields using a cryptographically strong hash function. E.g.;( netstat -a ; ps auxww ; vmstat ; who ) | md5 Some systems may provide a /dev/random device for this purpose. So how do you pick a good random number? Here are some ideas: * Use a genuine source of randomness, such as a radioactive source, static on the FM dial, thermal noise, or something similar. * Ask the user to type a set of text, and sample the time between keystrokes. * Monitor the user. Each time the user presses the keyboard, take the time between the current keypress and the last keypress, add it to the current random number seed, and hash the result with a cryptographic hash function. You can also use mouse movements to add still more randomness. * Avoid relying on the system clock. * Don't use Ethernet addresses or hardware serial numbers. * Beware of using information such as the time of the arrival of network packets. * Don't use random selection from a large database. * Consider using analog input devices already present on your system. This material is copyright 1996 by O'Reilly & Associates and by AUSCERT, The University of Queensland, and copies may only be made provided that: 1) All copies include the copyright notice and full credit information 2) No charge may be made for copies 3) The material is not to be included as part of any other document, collection or work without express permission of the copyright holders.