Software without Security Holes

Prabhaker Mateti

Abstract:  This lecture is about developing good habits and learning techniques that prevent errors in security software.

secSoftware.ppt  | FormalMethodsSecurityPM.ppt | SPLINT_Oct.ppt |
This article is part of Internet Security Lectures
   

Table of Contents

  1. Educational Objectives
  2. Software without Security Holes
    1. Top Ten Security Holes
    2. Robust Programs, Correct Programs and Secure Programs
    3. Is it Possible?
  3. Design Principles for Secure Programs
  4. Construction Principles for Secure Programs
  5. Correct By Design and Mathematical Proof
  6. Programs that Must be Robust
  7. Writing Safe setuid Programs
  8. Lab Experiment
  9. Acknowledgements
  10. 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

"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 ... to improve 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 MIMEbo, 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.  Note that none of the top ten are due to design errors in TCP/IP.

Robust Programs, Correct Programs and Secure Programs

For the purpose of this article, let us define robustness as being crash proof, and hang-proof no matter what the inputs are.  Crash is unexpected termination.  A hang is unexpected non-termination.  Two classes of being hung are: infinite looping, and waiting for an event that will not occur.  Infinite looping consumes heavily the CPU time.  Waiting for a non-occurring event consumes almost no resources.  Note that infinite recursion will lead to a crash via resource exhaustion.

Is it Possible to Develop Secure Programs?

Yes, but the cost of developing such software is so high that neither customers not companies are willing to spend.  It is also the case, that placing high importance on security will lead to these programs being inefficient.  In the next few paragraphs of this section, we ignore both these issues (cost and efficiency) and speculate on the possibility of developing secure software.

Design Principles for Secure Programs

Laws of Large Programs

All large programs are buggy.  It is unfortunate, but this axiom captures the state of the technology.  Security-relevant programs have security bugs. Large programs are buggier than their size would indicate.  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're writing a normal piece of software, your purpose is to make certain things possible, if the user does things correctly. When you're writing a security-sensitive piece of software, you also must make certain things impossible, no matter what any trusted or 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. Note that simple is different from small: just because you can write a CGI program in 300 bytes of line-noise Perl, doesn't mean you should.  All the usual structured-programming tips help here: clean interfaces between modules, avoid global state, etc.

Keep Interactions Minimal. You often need to check how each pair of subsystems interacts, and possibly even each subset of subsystems.  For example, interactions between the password checker and the page-fault mechanism.

Least Common Mechanisms. The assumptions originally made in shared code may no longer be valid.  Eaxmple 1: Netscape's LiveConnect allows Java and Javascript and the browser to talk to each other.  But Java and Javascript have different ways to get at the same information, and also different security policies.  A malicious Java applet could cooperate with a malicious Javascript page to communicate information neither could have communicated alone. Example 2. Windows exports an easy interface to IE's HTML-rendering code. Mail clients like Eudora, Outlook Express, among other programs, use this interface to display HTML-formatted email.  By default, parsing of Java and Javascript (J-Script) are enabled.  However, the HTML-rendering code "thinks'' that Java and J-Script are unsafe when loaded from the Internet, but safe when loaded from local disk.   The email is loaded from local disk!

Fail-open or -closed?

Security can fail in two different ways: Allow access when it shouldn't; this is called fail-open.  Refuse access when it shouldn't; this is called fail-closed.  As an example, consider electronic door lock.  When the power goes out, locking the door by holding it closed with a massive electromagnet will fail-open, whereas locking the door with a spring-loaded deadbolt that is pulled out of the way with a solenoid will fail-closed.

Many programs do not check if enough resources will be available. What happens if there is not enough memory and some allocations fail? What happens if the program runs out of fille descriptors? What happens if the program cannot fork()?

Security compartments

A secure system is divided into security compartments. For example, a Linux system has numerous compartments known as "users", "kernel", and  "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-writeable 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 system administrators will leave them alone. For example, if you unpack an RPM or a ZIP archive and it creates some configuration files world-writeable, 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.

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 doesn't 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.

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. Do not assume that inputs are valid.  E.g., if an argument should be a positive integer in the range of 2 to 7, verify that.  If an argument should be a non-empty string of letters not exceeding 13 characters in length, verify that.  Check interactive input to be sure it contains only "good" characters.  Consider how such input will be parsed when substituted.  Check arguments passed in environment variables.
  2. Check all system call parameters and system call return code.   System calls should verify their arguments, but unfortunately most OS calls do not for fear of becoming inefficient, so you must.  Fortunately, all system calls return a success or failure code.  Unfortunately, only a few programs verify these result codes.
  3. Buffer Overflow.  Be sure all buffers (arrays of items, usually characters) are bounded.  Do bounds checking on every variable before the contents are copied to a local buffer.  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()
  4. Files and Directories. Always use full pathnames for any file arguments.  The current directory assumed by your program may not be where it is at.  Explicitly change directories (chdir()) to an appropriate directory at program start. If creating a new file, use O_EXCL and O_CREAT flags to assure that the file does not already exist. Do not create files in world-writable directories. Use lstat() to make sure a file is not a link, if appropriate. Set limit values to disable creation of a core file if the program fails. If using temporary files, consider using tmpfile() or mktemp() system calls to create them (although most mktemp() library calls have problematic race conditions).
  5. Logging Events.  Do log relevant information, including date, time, uid and effective uid, gid and effective gid, terminal information, pid, command-line arguments, errors, and originating host.  Make sure that the log files themselves remain bounded in size.
  6. Make the program's critical portion as short and simple as possible.
  7. Be aware of race conditions, deadlock conditions and sequencing conditions.
  8. Do not require clear-text authentication information.
  9. Use session encryption to avoid session hijacking and hide authentication information.
  10. Never use system() and popen() system calls
  11. Avoid creating setuid or setgid shell scripts
  12. Do not make assumptions about port numbers, use getservbyname() instead.  Do not assume connections from low-numbered ports are legitimate or trustworthy. Do not assume the source IP address is legitimate. Place timeouts and load level limits on incoming network-oriented read request.  Place timeouts on outgoing network-oriented write requests.
  13. Robust Compilation and Libraries. Make good use of tools such as lint.  Have internal consistency-checking code.  Use your compiler wisely. With gcc, use -Wall -ansi -pedantic flags.  Use safe libraries.
  14. Have code reviewed by other people. E.g., commercial products such as 3Com's  CoreBuilder and SuperStack II hubs were revealed to have "secret" backdoor passwords.
  15. Test thoroughly.  Test the software using the same methods that crackers do: Try to overflow every buffer in the package, Try to abuse command line options, Try to create every race condition conceivable.  Have others besides the designers and implementers test the code.  Be aware of test coverage; gcc -pg -a causes the program to produce a bb.out file that is helpful in determining how effective your tests are at covering all branches of the code. 
  16. Use formal specifications. At a minimum, develop pre- and post-conditions in carefully written English.

Correct By Design and Mathematical Proof

There is a large body of technical literature that advocates designing software by first writing formal specifications capturing the all requirements of the software yet to be constructed.  Subsequent steps are systematic refinements of such specifications yielding several levels of designs ultimately producing the source code in a programming language.   The writings of Turing-award winners Dijkstra and Hoare are heavily influential in this regard. (For an elementary introduction, read Prabhaker Mateti, "Practical Advice on Writing Pre- Post-Conditions for Real Programs," Lecture Notes,  May 1998.) Unfortunately, the open literature has documented only small programs that are so developed.  Most of the academic computer science community as well as the industry believes that such development is astronomically expensive and even then not necessarily qualitatively "better."

In recent years, more practical uses of the above methodology have emerged.  A tool,  called splint (www.splint.org) , can analyze large amounts of C source code at speed comparable to that of a typical compiler and flag a variety of notorious errors that made secure software succumb to such attacks as buffer overflow.

Programs that Must be Robust

The following programs 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

This lab is exempt from the usual rule: All work should be carried out in Operating Systems and Internet Security (OSIS) Lab, 429 Russ.   Use any of the PCs numbered 23 to 30.  No other WSU facilities are allowed. 

Objective: Get you to think about secure programming some more, and make you familiar with the splint tool from www.splint.org .

  1. Study Matt Bishop's article.  Provide answers to Exercises 10 and 19.
  2. Splint is installed so that environment variables should be set as in this script.
      export PATH=$PATH:/home/pmateti/CEG429/splint-3.1.1/bin
    export LARCH_PATH=.:/home/pmateti/CEG429/splint-3.1.1/lib
    export LCLIMPORTDIR=/home/pmateti/CEG429/splint-3.1.1/imports
    A copy of the Splint Users Manual /home/pmateti/CEG429/splint-3.1.1/doc/manual.pdf is available in the Lab.  Read it sufficiently so that you can do the following.
  3. Run splint on exploit4.c of Aleph One.  Revise the code of exploit4.c, and adjust the flags of splint so that all errors and warnings shown by splint are gone.
  4. Download the source code sudo-1.6.7p5.tar.gz of the standard Linux command sudo.  Build it as usual.  Check that it "works."
  5. Run splint, with no (except for include-related) flags, collectively on all the source code files of sudo.  Select three messages regarding source code errors generated by splint, and explain the messages and the causes for their generation.
  6. To the lab report, append by catenating with ">>" the files  BishopExercises1019.txt, exploit4Revised.c,and sudoExperience.txt.  Please insert a line of dashes (hyphens) between these files as a visual separator.

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. [HTML] [PDFnob.cs.ucdavis.edu/~bishop/ Required Reading.
  2. Simson Garfinkel, Gene Spafford Practical Unix and Internet Security, 2nd edition (April 1996), O'Reilly & Associates; ISBN: 1565921488.  Errata: 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/ 429/ Top/ lectures.html.  There is a section on robust programming techniques that avoid the buffer overflow exploits.  Required Reading.
  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.
  7. David Evans, SPLINT, www.splint.org, University of Virginia, 2001. Reference
     
 
Copyright © 2008 pmateti@wright.edu