IntroductionThis article provides a brief overview of some of the things you need to think about when writing a secure program in C. Aspects of Security
There are two areas where security is a concern: denial of service attacks, and compromises. The latter category has two sub-categories: local compromises, where the attack originates from a user who is logged in to the machine, and remote compromises, when the attack originates from a remote machine (be it on the same network, or half way around the world). A denial of service occurs when the attacker makes the services you offer unavailable, by crashing your web server, for example, or saturating your network connection with bogus requests. A compromise occurs when an unauthorized person gains access to one or more of your machines. If that access is as a privileged user (the root account on UNIX® boxes), then attackers can do whatever they like to the machine, from deleting all your files to copying confidential data, to even using the machine as a platform from which to attack other machines, either in your network, or at other sites. Languages like Java use a sandbox to help ensure their security, but what can C programmers do to make their programs as secure as possible? Following is a discussion about some common programming mistakes and how to correct them, and then some tips on how to write secure programs. Buffer OverflowsA buffer overflow is what happens when programs try to store more data in a variable than it has been allocated space for. For example, suppose you have a variable called name that's defined as an array of 10 characters. There is room for 9 characters, plus the terminating null. By default, C does no bounds checking at run-time, so it is very easy for the user of a badly written program to over flow a buffer. Consider this code fragment:
char name [10];
printf ("Enter your name: ");
fflush (stdout);
gets (name);
If the user of this program enters a name that's less than 10 characters, all is well. But if they enter a longer string, the stack will get stomped on and data corruption can occur, causing a core dump, or worse, giving the user shell prompt. If the program is running as root, this would be disastrous. So what can you do to avoid these buffer overflow problems? One answer is to provide really big buffers that "no one will ever overflow". This is a bad idea because it hasn't fixed the problem; it merely makes it harder to accidentally overflow the buffer. But it won't stop a malicious user from deliberately overflowing the buffer. To do that, you need to use functions that let you specify a maximum number of characters to copy. If you change the line that reads
it doesn't matter how many characters the user types in response to the prompt, as only the first 9 characters will be copied into the variable name. (With this example, you also have to remove the
Unfortunately, there is a lot of code out there that has buffer overflow vulnerabilities. A malicious user could send a carefully constructed byte stream to these programs, which would build on the stack the instructions needed to start a shell; if the program compromised is SUID root, the user would get a root shell. Fortunately, users of the Solaris operating environment, beginning with 2.6, have a line of defense against this method of attack: putting the following two lines in set noexec_user_stack = 1 set noexec_user_stack_log = 1 Note: Although this technically violates the SPARC V8 ABI, which specifies that the user stack must have read, write, and execute permissions, in reality, very few programs are adversely affected. The SPARC V9 ABI states that the user stack only has read and write permissions.
There are several unsafe library functions like Don't forget to include the terminating null in your string size calculations. If you need a string LEN characters long, remember to declare the array with LEN + 1 bytes in it. The Program's EnvironmentA security conscious program should never assume anything about its environment: what directory it was run from (the working directory), the value of its umask, what file descriptors are open, and even the values of the environment variables passed to it from its parent. Problems can be circumvented by changing to a specific directory when the program starts, setting a sensible umask value, and closing any files the program doesn't expect to be open. The corollary of this is to make sure that programs set the close on exec flag on file descriptors they don't intend to pass on to child processes. Another thing that comes under the heading of the program's environment is what UID (User ID) and GID (Group ID) the program is designed to run as, and what UID and GID it gets run as. An example of this is Berkeley Internet Named Domain (BIND), the most commonly used DNS server. Recent versions of BIND are designed to run as an unprivileged user, rather than root. A program that is designed to run as a non-root user might have security implications if it is run by root, and vice versa. For instance, what happens if an unprivileged user runs a program that is designed to only be run as root? Or worse, what happens if root runs a program that isn't intended to be run by root? Some Tips for Writing Secure ProgramsYou know some of the concepts that need to be considered when writing secure programs. Here are some more ideas:
Further Reading
About the AuthorRich Teer has more than 10 years of industry experience with UNIX systems and C programming. He runs his own Solaris consultancy and web hosting company, and is currently writing a book, Solaris Systems Programming, to be published byAddison-Wesley in 2002. Rich lives in Kelowna, BC, with his wife, Jenny, and their dog, Judge. June 2001 | ||||||||
|
| ||||||||||||