Secure C Programming

Introduction

As more devices get connected to the Internet, the security needs of those devices has gained a higher profile. There are many different aspects to security:

  • Physical security. This is where we ensure that physical access to the device we are trying to secure is restricted to authorised personnel.
  • Procedural security. This is where an organisation has policies and procedures in place to prevent unauthorised access to equipment, perhaps by making their staff aware of social engineering (where a would-be intruder poses as an authorised person, and tries to get information from staff), etc.
  • Using strong encryption. There is little point in having physically secure machines and educating staff about the dangers of social engineering if off site personnel access their computers using an unencrypted connection. All the security policies in the world won’t help if a cracker can just grab usernames and passwords over the wire.
  • Using Firewalls. Firewalls limit what sort of network protocols can communicate to which machines in your network. For example, they can ensure that HTTP (Hyper Text Transfer Protocol) requests can only be sent to and from your web servers, and similarly for SMTP (Simple Mail Transfer Protocol), DNS (Domain Name System), and any other network service you want to expose to the outside world. Limiting the protocol that can connect to a given machine limits any remote attacks to using just that protocol.
  • Using programs that have security as one of their design considerations. These are programs that are both algorithmically secure, and have been written in a secure manner.

There two areas where security is of concern: the prevention of DoS (Denial of Service) attacks, and the prevention of compromises. This latter category has two sub-categories: local compromises, where the attack originates from a user that is logged in to the machine in question, and remote compromises, which is where the attack originates from a remote machine (be it on the same network, or half way around the world).

A DoS is when the attacker makes the services we offer unavailable, by crashing our web server, for example, or even just saturating our network connection with bogus requests. A compromise is when an unauthorised person gains access to one or more of our machines. If that access is as a privileged user (the root account on UNIX boxes), then the attacker can do whatever they like to the machine, from deleting all our files, to copying confidential data, to even using the machine as a platform from which to attack other machines, either in our own network, or at other sites.

An article like this can’t possibly cover all the angles: whole books have been written about security. Even security from a programming perspective is a big topic, so interested readers are encouraged to read the references provided here.

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? In this article, I’m going to discuss some common programming mistakes that people make, how to correct them, and offer some tips on how to write secure programs.

Buffer Overflows

A 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 we have a variable called name that’s defined as an array of 10 characters. This has room for 9 characters, plus the terminating NUL. By default, C does no bounds checking at run-time, so it is trivial for a user of a badly written program to overflow 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: 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 programmers do to avoid these buffer overflow problems? One answer is to provide really big buffers that “no one will ever over flow”. This is a bad idea, because not only does it waste precious memory but 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, we need to use functions that let us specify a maximum number of characters to copy. If we change the line that reads

    gets (name);

to

    fgets (name, 10, stdin);

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, we also have to remove the \n character from the end of the name, as fgets doesn’t remove it for us.)

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 themselves a shell; if the program compromised is SUID root, the user would get a root shell! Fortunately, users of Solaris 2.6 and newer have a line of defence against this method of attack: putting the following two lines in /etc/system will help prevent this attack, and provide a warning when an exploit of this type is attempted:

    set noexec_user_stack = 1
    set noexec_user_stack_log = 1

(It should be noted that 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 gets that have safer alternative. These include strcpy (use strncpy), strcat (use strncat), and sprintf (use snprintf).

One last thing while we’re talking about buffer overflows: don’t forget to include the terminating NUL 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 Environment

A 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.

These problems can be circumvented by explicitly chdiring 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 BIND (Berkeley Internet Name Domain), the most commonly used DNS server. Recent versions of BIND are designed to be able to be run as an unprivileged user, rather than root. A program that is designed to run as a non-root user might have security implication if it is run by root, and vice versa: 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 Programs

We’ve discussed some of the concepts that need to be considered when writing secure programs. Here are some more ideas:

  • Check function return values. Most library and system calls return an indication of their success or failure, so we should always check the return value for errors, even when an error seems unlikely. Only by checking for errors can our programs take the appropriate action, rather than just crashing.
  • Avoid the use of system and popen. It is better to implement the required functionality using fork and exec. This is because system and popen start a shell to run the desired command. Talking of exec, be wary of execlp and execvp: ideally, a program should pass on a carefully crafted environment, rather than trusting the one it inherits from its parent.
  • If data confidentiality is an issue, we should ensure that our programs can’t produce a core dump. This is done by limiting the size of a core dump to 0 bytes, either by using the ulimit command before running the program, or by calling setrlimit near the beginning of the program (the latter is preferable, because it is harder to forget or circumvent).
  • KISS! Keep your code, especially security critical sections, short and simple. Code that is simple is easier to read, and hence easier to find bugs (security or otherwise) in.
  • Practice defensive programming: make sure your code performs sanity checks on any data read from external sources, and any functions (especially those that rely on external data) bounds check their inputs. For example, if a function is only expecting values in the range of 1 to 100, it should ensure the input it gets is actually in that range—it shouldn’t just assume it will be. Be especially careful of boundary conditions (off by one errors, etc.).
  • Always use fully qualified pathnames for any files that get opened, especially when running new programs using exec. Programs using relative pathname are dangerous, because they are so easy to subvert: it is trivial for a user to change to directory of their own making, or worse, change their PATH environment variable to include untrusted directories.
  • Maintain the principle of least privilege. If a program needs some sort of privileges, it should use the least that will get the job done, for as little time as necessary. For example, consider the case of a program that gets invoked by several people that needs to write to a common file that the users wouldn’t ordinarily have access to. It would be tempting to write a SUID root program, but this is far too much privilege for such a simple task. A far better approach would be to allocate a dedicated group to the program and its files, and have the program run SGID (Set Group ID) to the new group.
  • Following on from the previous item, if a program must use elevated privileges, use privilege bracketing to turn off the privileges as early as possible, and only enable them when necessary. In the shared program example above, it would revert its GID to that of the invoking user at start up, and only set its GID to the one reserved for it when it needed to open the common file. Once the file was opened, the privileges can subsequently be dropped; writes to the file will succeed because it was opened with the right privileges.

    If the program that must run as a particular user is a daemon that will be started at boot time, we should also set the real UID/GID to the user we want it to run as. Otherwise, although the effective UID/GID will be that of your special user, the real UID/GID will be that of root, so the program will still have some root-like powers. Another (perhaps better) way of achieving this is to run it using the su command in the start/stop script:

        /sbin/su special user -c /path/to/special/program

  • With modern windowing systems, shell escapes aren’t as important as they used to be. With this in mind, we should try to avoid providing shell escapes in our programs. If we must provide this functionality (especially in a SUID/SGID program), then make sure the program resets its UID and GID to the real ones before invoking the shell.

Summary

This article has provided a brief overview of some of the things a person writing a secure program in C needs to think about. We discussed some of the various aspects of security in general, and then talked about buffer overflows and how to avoid them. We then looked at hazards to avoid when dealing with a program’s environment. Finally, we presented a list of tips for writing secure software in C.

Further Reading

Here’s a couple of books and papers you might find interesting:

  • Garfinkel, S., and Spafford, G. 1996. Practical UNIX & Internet Security. O’Reilly and Associates, Sebastopol, Ca.
  • Thompson, K. 1984. Reflections on Trusting Trust. Communication of the ACM, Vol. 27, No. 8, August 1984, pp. 761-763.
Doing a web search for either secure C programming or UNIX security will yield many useful hits (far too many to list here!).

Author’s Bio

Rich Teer has more than 10 years of industry experience with UNIX systems and C programming. He lives in Kelowna, BC, where he runs his own Solaris consultancy and web hosting company, Rite Online Inc. In what little spare time he has between consulting assignments, running a business, teaching himself Java, and writing, Rich enjoys spending time with his wife, Jenny, and their dog, Judge. He is currently writing a book called Solaris Systems Programming, which will be published by Addison Wesley in 2002.