Secure C Programming
Secure C Programming   By Rich Teer, June 2001  

Introduction

This 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

  • Physical security. Ensuring that physical access to the device you want to secure is restricted to authorized personnel.
  • Procedural security. Organizational policies and procedures in place to prevent unauthorized access to equipment.
  • Strong encryption. Prevention of off-site access using an unencrypted connection.
  • Firewalls. Limiting which network protocols can communicate with which machines in your network.
  • Programs that have security as a main design consideration. These are programs that are algorithmically secure and have been written in a secure manner.

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

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, you also have to remove the n character from the end of the name, as fgets() doesn't remove it.)

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 /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

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 fgets() that have a safer alternative. These include strcpy() (use strncpy()), strcat() (use strncat()), and sprintf() (use snprintf()).

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

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 Programs

You know 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 you should always check the return value for errors, even when an error seems unlikely. Only by checking for errors can your 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. Also, 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, you should ensure that your 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(1) 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).
  • Keep your code, especially security critical sections, short and simple. Code that is simple is easier to read, and hence it is easier to find bugs.
  • 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 users to change to a 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 privileges, it should use the least that will get the job done. For example, consider the case of a program that gets invoked by several people and that needs to write to a common file that the users wouldn't ordinarily have access to. It would be tempting to write an 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.
  • 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 is 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, you should also set the real UID/GID to the user you 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 -c /path/to/special/program

  • With windowing systems, shell escapes aren't as important as they used to be. With this in mind, you should try to avoid providing shell escapes in your programs. If you 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.

Further Reading

About the Author

Rich 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

Back to Top


Rate and Review Tell us what you think of the content of this page. Excellent   Good   Fair   Poor   Comments:
If you would like a reply to your comment, please submit your email address:
Note: We may not respond to all submitted comments.
Close    To Top
  • Prev Article-OS:
  • Next Article-OS:
  • Now: Tutorial for Web and Software Design > OS > Solaris > OS Content
    Photoshop Tutorial
     

    Special Effect

      3D Effect
      Photoshop Articles
    Programming Tutorial
     

    C/C++ Tutorial

      Visual Basic
      C# Tutorial
    Database Tutorial
     

    MySQL Tutorial

      MS SQL Tutorial
      Oracle Tutorial
    Geek Tutorial
     

    Blogging Tutorial

      RSS Tutorial
      Podcasting Tutorial
    Graphic Design Tutorial
      Coreldraw Tutorial
      Illustrator Tutorial
      3D Tutorials
    Webmaster Articles
     

    Domain Service

      Web Hosting
      Site Promotion
    Java Tutorial/ Articles
     

    Java Servlets

      JavaEE Tutorial
     

    JavaBeans Tutorial

    XML Tutorial/ Articles
     

    XML Style

      AJAX Tutorial
      XML Mobile
    Flash Tutorial/ Articles
     

    Flash Video

      Action Script
      Flash Articles
    OS Tutorial/ Articles
      Linux Tutorial
      Symbian Tutorial
      MacOS Tutorial
    Personal Tech
      Hardware Tutorial
      Software Tutorial
      Online Auction