CEG 499/699:
Internet Security


College of Engineering & CS
Wright State University
Dayton, Ohio 45435-0001

Software without
Security Holes

 

Prabhaker Mateti

 
Abstract:  This lecture is about developing good habits and learning techniques that prevent errors in security software.
 
This work is supported in part by NSF DUE-9951380.
  08/03/00

Table of Contents

  1. Educational Objectives
  2. Software without Security Holes
    1. Top Ten Security Holes
    2. Design Principles for Secure Programs
    3. Prevention of Errors
    4. Programs that Must be Robust
    5. Writing Safe setuid Programs
  3. Lab Experiment
  4. Acknowledgements
  5. References

Educational Objectives

  1. Understand the contribution to insecurity made by programming errors.
  2. Be able to deploy robust programming techniques.

Software without Security Holes

"The infrastructure of the Internet is fundamentally flawed in so many ways that it is a constant source of amazement that it does not collapse under its own weight. But for those of us who depend on the Internet for the competitive advantages it brings, there is little choice but to live with it and struggle to recover from the constant curves it throws us."

Fred Cohen, Sandia National Laboratories
widely credited as the first one to scientifically describe computer virus

"All problems fall into one of two categories: Those that can and those that cannot be easily solved. For instance, some of the denial of service attacks ... are a result of the IP protocol's design. Short of implementing a new protocol ..., not much can be done beyond stopgap measures that make particular attacks less effective.

Other difficult problems include network sniffing and spoofing. These result from security-related information being sent in the clear over networks. Then there is the general authentication problem. The difficulty with authentication is that the lowest common denominator is user names and passwords, and that method is generally not sufficient.

Unfortunately, solving these problems requires new hardware, new software, and user training, ... Over the longer term, protocols like IPV6 and IPsec will resolve many of these issues. Of course they may create new ones. ...

The solvable problems are the result of poor planning, programming, and implementation. These can be solved by software vendors ... [by] improv[ing] their coding methodologies. ... "

"... Is code getting better? You could assume that the security holes in operating systems are the result of poor coding way back when, and that new code and coding methods do not have the same problem. You would be wrong. Consider Windows NT and its sorry security state. Or look in our own back yard at Solaris. Bugs in admintool, NIS+, the volume manager, procfs, PPP, PAM, and the PCI bus drivers ... prove the point." [Peter Galvin, Pete's Wicked World column, Sun World Magazine, 1998]

Top Ten Security Holes

In June 2000, GSA Federal Chief Information Officers Council listed the "The Ten Most Critical Internet Security Threats":

  1. BIND weaknesses: nxt, qinv and in.named allow immediate root compromise.
  2. Vulnerable CGI programs and application extensions (e.g., ColdFusion) installed on web servers.
  3. Remote Procedure Call (RPC) weaknesses in rpc.ttdbserverd (ToolTalk), rpc.cmsd (Calendar Manager), and rpc.statd that allow immediate root compromise
  4. Remote Data Services (RDS) security hole in the Microsoft's web server named IIS.
  5. Sendmail buffer overflow weaknesses, pipe attacks and MIME buffer overflow, that allow immediate root compromise.
  6. Buffer overflows in sadmind (remote administration access to Solaris systems) and mountd (controls and arbitrates access to NFS mounts on UNIX hosts) permit root compromise.
  7. Global file sharing and inappropriate information sharing via NFS and Windows NT ports 135-139 (445 in Windows2000) or UNIX NFS exports on port 2049. Also Appletalk over IP with Macintosh file sharing enabled.
  8. User IDs, especially root/administrator with no passwords or weak passwords.
  9. IMAP and POP buffer overflow vulnerabilities or incorrect configuration.
  10. Default SNMP community strings set to ‘public’ and ‘private.’

The full report (available at http://www.itpolicy.gsa.gov/itleaders/toptenthreats.htm) is worth studying, and system administrators are urged to follow the procedures given there in securing their systems.  For our immediate use here, observe how many of these are due to programming errors.  Items 1-6, and 9 are programming errors.  Items 7, 8, and 10 are configuration errors.

Design Principles for Secure Programs

Laws of Large Programs

Axiom 1 (Murphy) All programs are buggy.

Law 1 (Law of Large Programs) Large programs are even buggier than their size would indicate.
Corollary 1.1
A security-relevant program has security bugs.

Theorem 2 If you do not run a program, it does not matter whether or not it is buggy. Corollary 2.1 If you do not run a program, it does not matter if it has security holes.

Theorem 3 Exposed machines should run as few programs as possible; the ones that are run should be as small as possible. 

Correctness and Robustness

When you are writing a normal piece of software, your purpose is to make certain things possible, if the user does things correctly. When you are writing a security-sensitive piece of software, you also must make certain things impossible, no matter what any untrusted user does.

Cryptologists and real-time programmers are familiar with doing things this way. Most other programmers are not.

Economy of Mechanism

Keep your implementation as simple as possible Interactions are a nightmare Least Common Mechanism

Fail-open or -closed?

Security can fail in two different ways: Allow access when it should not; this is called fail-open.  Refuse access when it should not; this is called fail-closed.  As an example, an electronic door lock that locks the door by holding it closed with a massive electromagnet is fail-open when the power goes out -- when the electromagnet has no power, the door will open easily. An electronic door lock that locks the door with a spring-loaded deadbolt that is pulled out of the way with a solenoid is fail-closed -- when the solenoid has no power, it is impossible to pull back the deadbolt.

Many programs are written with the assumption that enough resources will be available. So look to see what happens if there is not enough memory and some allocations fail, usually returning NULL from malloc or new.  Is it possible for untrusted users to use up all the resources (which becomes a denial-of-service problem)? What happens if the program runs out of fds (and whether it is possible)? What happens if the program cannot fork(), or if its child dies during initialization due to resource starvation?

CGI scripts commonly execute other programs, passing them user data on their command lines. In order to avoid having this data interpreted by the shell (on a Unix system) as instructions to execute other programs, access other files, etc., the CGI script removes unusual characters -- things like '<', '|', '!', '"', etc. You can do this in a fail-open way by having a list of "bad characters" that get removed. Then, if you forgot one, it is a security hole. You can do it in a fail-closed way by having a list of "good characters" that do not get removed. Then, if you forgot one, it is an inconvenience.

Security compartments

Any secure system is divided into security compartments. For example, a Linux system has numerous compartments known as "users", and a compartment known as the "kernel", as well as a compartment known as the "network" -- which is divided into sub compartments known as "network connections". There are well-defined trust relationships between these different compartments, which are based on system setup and authentication. The trust relationships must be enforced at every interface between security compartments.

Trusting untrustworthy channels

If you send passwords in clear text over a LAN, if you create a world-writable file and later try to read back data from that file, if you create a file in /tmp with O_TRUNC but not O_EXCL, etc., you are trusting an untrustworthy intermediary.

Proper defaults

If there are non-obvious, but insecure, defaults, it is likely that people will leave them alone. For example, if you unpack an RPM or a ZIP archive and it creates some configuration files world-writable, you are unlikely to notice.

Error Handling and Reporting

Error handling and reporting is an essential part of any programming paradigm. Delicate handling of and recovery from error conditions is an absolute necessity, especially in a third party library.

Assertions and Exit Points

assert(3) is a macro that accepts a single argument which it treats as a Boolean expression. If the expression evaluates to false, the assert macro prints an error message and terminates the program. Assertions are useful in the developmental stages of programs when verbose error handling is not in place or when a grievous error condition that normally should not happen occurs.  One must use assertions, but exiting abruptly even after reporting an error is not acceptable. If a grievous error condition is detected, the code should return error codes to the caller, and let it decide what to do. Code should be able to handle grievous errors well enough to be able to exit gracefully from the top level (if possible).  Also, never use structured exception handling as a substitute for writing solid code in the first place.

 

Construction Principles for Secure Programs

  1. Check all command line arguments.
  2. Check all system call parameters and system call return code.
  3. Check arguments passed in environment parameters and do not depend on Unix environment variables.
  4. Be sure all buffers are bounded.
  5. Do bounds checking on every variable before the contents are copied to a local buffer.
  6. Check user input to be sure it contains only "good" characters
  7. Avoid routines that fail to check buffer boundaries when manipulating strings, particularly: sprintf(), fscanf(), scanf(), vsprintf(), realpath(), getopt(), getpass(), streadd(), strecpy(), strtrns(), gets(), strcpy(), and strcat()
  8. Always use full pathnames for any file arguments.
  9. If creating a new file, use O_EXCL and O_CREAT flags to assure that the file does not already exist.
  10. Do not create files in world-writable directories.
  11. Use lstat() to make sure a file is not a link, if appropriate.
  12. Explicitly change directories (chdir()) to an appropriate directory at program start.
  13. Set limit values to disable creation of a core file if the program fails.
  14. If using temporary files, consider using tmpfile() or mktemp() system calls to create them (although most mktemp() library calls have problematic race conditions).
  15. Include lots of logging, including date, time, uid and effective uid, gid and effective gid, terminal information, pid, command-line arguments, errors, and originating host.
  16. Be aware of race conditions, including deadlock conditions and sequencing conditions.
  17. Place timeouts and load level limits on incoming network-oriented read request.
  18. Place timeouts on outgoing network-oriented write requests.
  19. do not require clear-text authentication information.
  20. Use session encryption to avoid session hijacking and hide authentication information.
  21. Avoid using system() and popen() system calls.
  22. Avoid creating setuid or setgid shell scripts.
  23. do not make assumptions about port numbers, use getservbyname() instead.
  24. do not assume connections from low-numbered ports are legitimate or trustworthy.
  25. do not assume the source IP address is legitimate.
  26. Make good use of tools such as lint.
  27. Have internal consistency-checking code.
  28. Make the program's critical portion as short and simple as possible.
  29. gcc -pg -a causes the program to produce a bb.out file that can be helpful in determining how effective your tests are at covering all branches of the code.
  30. Have code reviewed by many. 3Com's  CoreBuilder and SuperStack II hubs were revealed to have "secret" backdoor passwords which were revealed to customers in emergencies.
Test the software using the same methods that crackers do:

Change-of-role hole

What was originally a minor annoyance, or sometimes even a convenience, can become a security hole when a program is run in a different context.  For example, suppose you have a PostScript interpreter that was originally intended to let you preview your documents before printing them. This is not a security-sensitive role; the PostScript interpreter does not have any capabilities that you do not. But suppose you start using it to view documents from other people. Suddenly, the presence of PostScript's file access operators becomes a threat! Someone can send you a document which will delete all your files -- or possibly stash copies of your files someplace they can get at them.

Prevention of Errors

There is a section on robust programming techniques that avoid the buffer overflow exploits in Prabhaker Mateti, "Buffer Overflow", Lectures on Internet Security, www.cs.wright.edu /~pmateti/ Courses/ 499/ Top/ lectures.html. In addition, the following are required reading.

  1. Matt Bishop, Robust Programming, October 1998. seclab.cs.ucdavis.edu/ ~bishop/ classes/ ecs153-1998-winter/ robust.html Required Reading.
  2. Prabhaker Mateti, "Practical Advice on Writing Pre- Post-Conditions for Real Programs," Lecture Notes,  May 1998. [local copy]  Required Reading.

Programs that Must be Robust

Programs that Must be Robust:  The OS kernel.  All setuid and setgid programs. All daemons that accept network connections.

If the kernel has security holes, no amount of checking of system programs is going to make the system secure from attack. However, relatively few kernel bugs are being found and exploited these days.

setuid root programs allow users to gain root privileges. Daemons allow users to access the system without first getting authenticated. A network daemon may answer a network request and process it under the daemon's privileges, not a user's. Therefore, this is another way for users to increase access, or even gain initial access, to the target system.

In terms of security, kernels are relatively bug-free because of the limited interfaces available to attack. For instance, Solaris has 210+ system calls (check /usr/include/sys/syscall.h), and Linux about 190. Compare that to the thousands of points a hacker has available to attack: sockets, files, devices, and programs.

Writing Safe setuid Programs

The setuid feature allows executables launched by a "user" to run with "root" privileges. A typical example is the passwd program. Attackers exploit setuid programs in order to gain root level access. Therefore, a system administrator should hunt down all the setuid programs on a system. and remove the setuid bit, or very thorughly evaluate why it must remain set.

Check for "rws----" permissions to see if an executable is setuid root. Run find / -perm +4000 -print to locate all setuid files. Add "-user root" in order to find just those that elevate to root.

 

 SYNOPSIS
      #include <unistd.h>

      int setuid(uid_t uid);

      int setgid(gid_t gid);

 DESCRIPTION
      setuid() sets the real-user-ID (ruid), effective-user-ID (euid),
      and/or saved-user-ID (suid) of the calling process.  The super-user's
      euid is zero.  
       Under  Linux, setuid is implemented like the POSIX version
       with the _POSIX_SAVED_IDS feature.  This allows  a  setuid
       (other  than  root) program to drop all of its user privi­
       leges, do some un-privileged work, and then re-engage  the
       original effective user ID in a secure manner.

       If the user is root or the program is setuid root, special
       care must be taken. The setuid function checks the  effec­
       tive  uid  of  the  caller and if it is the superuser, all
       process related user ID's are set to uid.  After this  has
       occurred,  it is impossible for the program to regain root
       privileges.

       Thus, a setuid-root program wishing  to  temporarily  drop
       root  privileges,  assume the identity of a non-root user,
       and then regain  root  privileges  afterwards  cannot  use
       setuid.  You can accomplish this with the (non-POSIX, BSD)
       call setuid.
       The following conditions govern setuid's behavior:

           o  If the euid is zero, setuid() sets the ruid, euid, and suid to
              uid.

           o  If the euid is not zero, but the argument uid is equal to the
              ruid or the suid, setuid() sets the euid to uid; the ruid and
              suid remain unchanged.  (If a set-user-ID program is not
              running as super-user, it can change its euid to match its
              ruid and reset itself to the previous euid value.)

           o  If euid is not zero, but the argument uid is equal to the
              euid, and the calling process is a member of a group that has
              the PRIV_SETRUGID privilege (see privgrp(4)), setuid() sets
              the ruid to uid; the euid and suid remain unchanged.

      setgid() sets the real-group-ID (rgid), effective-group-ID (egid),
      and/or saved-group-ID (sgid) of the calling process.  The following
      conditions govern setgid()'s behavior:

           o  If euid is zero, setgid() sets the rgid and egid to gid.

           o  If euid is not zero, but the argument gid is equal to the rgid
              or the sgid, setgid() sets the egid to gid; the rgid and sgid
              remain unchanged.

           o  If euid is not zero, but the argument gid is equal to the
              egid, and the calling process is a member of a group that has
              the PRIV_SETRUGID privilege (see privgrp(4)), setgid() sets
              the rgid to gid; the egid and sgid remain unchanged.

 RETURN VALUE
      Upon successful completion, setuid() and setgid() returned 0. 
      On error, -1  is  returned, and errno is set appropriately.

Like most man pages, the descriptions of this most famous syscall vary from Unix to Unix.  Here is a typical entry. As you can see, it is quite unclear, and many programmers do not study other carefully written setuid programs. 

Recommended Reading:

Simson Garfinkel, Gene Spafford Practical Unix and Internet Security, 2nd edition (April 1996), O'Reilly & Associates; ISBN: 1565921488.  Errata: http://www.oreilly.com/catalog/puis/errata/ Chapter 23: Writing Secure SUID and Network Programs. 


Lab Experiment

None.  Work out at least a few of the exercises in Matt Bishop's article.


Acknowledgements

These lecture materials are gleaned from many sources.  All are presented after careful reading.   In some cases, I may have neglected proper attribution. I assure the reader it is not because I claim authorship.  Indeed, in the lectures there is hardly any thing new that I have contributed.  Suggestions for improvement are always welcome. 


References

  1. Matt Bishop, Robust Programming, October 1998. seclab.cs.ucdavis.edu/ ~bishop/ classes/ ecs153-1998-winter/ robust.html Required Reading.
  2. Simson Garfinkel, Gene Spafford Practical Unix and Internet Security, 2nd edition (April 1996), O'Reilly & Associates; ISBN: 1565921488.  Errata: http://www.oreilly.com/catalog/puis/errata/ Chapter 23: Writing Secure SUID and Network Programs.  Recommended Reading.
  3. Prabhaker Mateti, "Practical Advice on Writing Pre- Post-Conditions for Real Programs," Lecture Notes,  May 1998. [local copy]  Required Reading.
  4. Prabhaker Mateti, "Buffer Overflow", Lectures on Internet Security, www.cs.wright.edu /~pmateti/ Courses/ 499/ Top/ lectures.html.
  5. Adam Shostack, "Security Code Review Guidelines," July 2000, www.homeport.org/ ~adam/ review.html  Reference.
  6. David A. Wheeler, "Secure Programming for Linux and Unix HOWTO," April 2000, www.linuxdoc.org/ HOWTO/Secure-Programs-HOWTO.html  Reference.
08/03/00 01:21:35 PM
Open Content Copyright © 2000 pmateti@cs.wright.edu Other Internet Security Lectures by Mateti