Software without Security Holes

Prabhaker Mateti

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

SoftwareNoSecHoles.pptx

Table of Contents

  1. Educational Objectives
  2. Software without Security Holes
  3. Top Ten Security Holes
  4. Robust Programs, Correct Programs and Secure Programs
  5. Coding Practices
  6. Writing Safe setuid Programs
  7. Correct By Design and Mathematical Proof
  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 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.’

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

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.

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. 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 kernel of a few years ago about 190 and in 2012 about 320+. Compare that to the thousands of points a hacker has available to attack: sockets, files, devices, and programs.

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.

Coding Practices

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. Good coding practices can make large programs less buggy.

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

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

Be aware of race conditions, deadlock conditions and sequencing conditions.

Never use system() and popen() system calls

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.

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.

Robust Compilation and Libraries. Make good use of tools such as lint and splint. Have internal consistency-checking code. Use your compiler wisely. With gcc, use -Wall -ansi -pedantic flags. Use safe libraries.

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.

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.

Writing Safe setuid Programs

Avoid creating setuid or setgid shell scripts. The setuid feature allows executables launched by an ordinary 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.

Read the man page on setuid carefully. Like most man pages, the descriptions of this most famous syscall vary from Unix to Unix. Unfortunately, the man pages are quite unclear, and many programmers do not study other carefully written setuid programs.

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.

Economy of Mechanism

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

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!

Make the program's critical portion as short and simple as possible.

Fail-Open or Fail-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.

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.

Do not require clear-text authentication information.

Use session encryption to avoid session hijacking and hide authentication information.

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.

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.

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.

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.

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 "Practical Advice on Writing Pre- Post-Conditions for Real Programs," listed in the References.) 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. E.g., a tool, called SPLINT (see References), can analyze large amounts of C source code at a 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.

Use formal specifications. At a minimum, develop pre- and post-conditions in carefully written English.

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.

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: Introduce you to formal methods based tools. Get you to think about secure programming some more.

  1. Study Matt Bishop's article. Provide answers to Exercises 10 and 19 in the lab report.
  2. List (as in ls -l) all the setuid programs in Knoppix.
  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. Include in the lab report the content of exploit4Revised.c
  4. Download the source code of both the locally stored version of 2003 sudo-1.6.7p5.tar.gz, and the latest from http://www.sudo.ws/sudo/. Build one of the two as usual. Check that it "works."
  5. Run splint, with no flags (except for include-related), collectively on all the source code files of sudo. Insert all its messages into the lab report.
  6. Select three interesting messages regarding source code errors generated by splint, and explain the messages and the causes for their generation.
  7. Bonus Points: The tar ball sizes of the two sudo versions you downloaded are shown below. Summarize what has improved. Explain the increase in size.
    -rw-r--r-- 1 pmateti pmateti  349785 2003 sudo-1.6.7p5.tar.gz
    -rw-r--r-- 1 pmateti pmateti 1608969 2012 sudo-1.8.4p4.tar.gz
    
  8. Bonus Points: Study the different versions of the man pages of sudo. Focus on the "seven sins of the specifier", namely, 1. Noise, 2. Silence: 3. Overspecification, 4. Contradiction, 5. Ambiguity, 6. Forward Reference, 7. Wishful Thinking [from Bertrand Meyer, On Formalism In Specifications, IEEE Software, 1985, vol. 2, no. 1. pp. 6-26]. Write up your findings.

Link to Grading Sheet

Acknowledgements

These lecture materials are gleaned from many sources. All are presented after careful reading. In some cases, I may have unintentionally 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, (i) Robust Programming, [ PDF] October 1998. Required Reading. (ii) Secure setuid programs. Local Copy Required Reading.
  2. David Evans and David Larochelle, Improving Security Using Extensible Lightweight Static Analysis, In IEEE Software, 2002. This paper: Required Reading. Download Splint source code and document from www. splint. org, University of Virginia.
  3. Simson Garfinkel, Gene Spafford Practical Unix and Internet Security, O'Reilly & Associates; Chapter 23: Writing Secure SUID and Network Programs. Required Reading.
  4. Prabhaker Mateti, "Practical Advice on Writing Pre- Post-Conditions for Real Programs," Lecture Notes, May 1998. [local copy] Required Reading.
  5. 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.
  6. Adam Shostack, "Security Code Review Guidelines," July 2000, www. homeport.org/ ~adam/ review.html Highly Recommended Reading.
  7. David A. Wheeler, "Secure Programming ...", (i) Secure-Programs-HOWTO, 2000. (ii) Prevent race conditions, 2004. Required Reading.
  8. Murat Torlakcik, Contracts in OpenBSD, MSc. Report, University College Dublin, 2010. "OpenBSD is widely regarded as the world's most secure operating system. ... Historically, API functions in C are documented via standards (like POSIX), manual pages, and code comments. ... they are frequently imprecise, incorrect, and difficult to maintain. ... it was attempted to use the Frama-C framework and its Jessie plug-in on the annotated functions to check their correctness with respect to those specifications." Highly Recommended Reading.

Copyright © 2012 • pmateti@wright.edu Other Internet Security Lectures