Chapter 29. Program Security

 

CLOWN: What is he that builds stronger than eitherthe mason, the shipwright, or the carpenter?OTHER CLOWN: The gallows-maker; for that frame outlivesa thousand tenants.

 
 --Hamlet, V, i, 42–45.

The software on systems implements many mechanisms that support security. Some of these mechanisms reside in the operating system, whereas others reside in application and system programs. This chapter discusses the design and implementation of a system program. It also presents common programming errors that create security problems, and offers suggestions for avoiding those problems. Finally, testing and distribution are discussed.

This chapter shows the development of the program from requirements to implementation, testing, and distribution.

Introduction

This section considers a specific problem on the Drib's development network infrastructure systems. Numerous system administrators must assume certain roles, such as bin (the installers of software), mail (the manager of electronic mail), and root (the system administrator). Each of these roles is implemented as a separate account, called a role account. Unfortunately, this raises the problem of password management. To avoid this problem, as well as to control when access is allowed, the Drib will implement a program that verifies a user's identity, determines if the requested change of account is allowed, and, if so, places the user in the desired role.

Requirements and Policy

The problem of sharing a password arises when a system implements administrative roles as separate user accounts.

An alternative to using passwords is to constrain access on the basis of identity and other attributes. With this scheme, a user would execute a special program that would check the user's identity and any ancillary conditions. If all these conditions were satisfied, the user would be given access to the role account.

Requirements

The first requirement comes directly from the description of the alternative scheme above. The system administrators choose to constrain access through known paths (locations) and at times of day when the user is expected to access the role account.

  • Requirement 29.2.1. Access to a role account is based on user, location, and time of request.

Users often tailor their environments to fit their needs. This is also true of role accounts. For example, a role account may use special programs kept in a subdirectory of the role account's home directory. This new directory must be on the role account's search path. A question is whether the user's environment should be discarded and replaced by the role account's environment, or whether the two environments should be merged. The requirement chosen for this program is as follows.

  • Requirement 29.2.2. The settings of the role account's environment shall replace the corresponding settings of the user's environment, but the remainder of the user's environment shall be preserved.

The set of role accounts chosen for access using this scheme is critical. If unrestricted access is given (essentially, a full command interpreter), then any user in the role that maintains the access control information can change that information and acquire unrestricted access to the system. Presumably, if the access control information is kept accessible only to root, then the users who can alter the information—all of whom have access to root—are trusted. Thus:

  • Requirement 29.2.3. Only root can alter the access control information for access to a role account.

In most cases, a user assuming a particular role will perform specific actions while in that role. For example, someone who enters the role of oper may perform backups but may not use other commands. This restricts the danger of commands interacting with the system to produce undesirable effects (such as security violations) and follows from the principle of least privilege.[2] This form of access is called “restricted access.”

  • Requirement 29.2.4. The mechanism shall allow both restricted access and unrestricted access to a role account. For unrestricted access, the user shall have access to a standard command interpreter. For restricted access, the user shall be able to execute only a specified set of commands.

Requirement 29.2.4 implicitly requires that access to the role account be granted to authorized users meeting the conditions in Requirement 29.2.1. Finally, the role account itself must be protected from unauthorized changes.

  • Requirement 29.2.5. Access to the files, directories, and objects owned by any account administered by use of this mechanism shall be restricted to those authorized to use the role account, to users trusted to install system programs, and to root.

We next check that these requirements are complete.

Threats

The threats against this mechanism fall into distinct classes. We enumerate the classes and discuss the requirements that handle each threat.

Group 1: Unauthorized Users Accessing Role Accounts

There are four threats that involve attackers trying to acquire access to role accounts using this mechanism.

  • Threat 29.2.1. An unauthorized user may obtain access to a role account as though she were an authorized user.

  • Threat 29.2.2. An authorized user may use a nonsecure channel to obtain access to a role account, thereby revealing her authentication information to unauthorized individuals.

  • Threat 29.2.3. An unauthorized user may alter the access control information to grant access to the role account.

  • Threat 29.2.4. An authorized user may execute a Trojan horse (or other form of malicious logic),[3] giving an unauthorized user access to the role account.

Requirements 29.2.1 and 29.2.5 handle Threat 29.2.1 by restricting the set of users who can access a role account and protecting the access control data. Requirement 29.2.1 also handles Threat 29.2.2 by restricting the locations from which the user can request access. For example, if the set of locations contains only those on trusted or confidential networks, a passive wiretapper cannot discover the authorized user's password or hijack a session begun by an authorized user. Similarly, if an authorized user connects from an untrusted system, Requirement 29.2.1 allows the system administrator to configure the mechanism so that the user's request is rejected.

The access control information that Requirement 29.2.1 specifies can be changed. Requirement 29.2.3 acknowledges this but restricts changes to trusted users (defined as those with access to the root account). This answers Threat 29.2.3.

Threat 29.2.4 is more complex. This threat arises from an untrusted user, without authorization, planting a Trojan horse at some location at which an authorized user might execute it. If the attacker can write into a directory in the role account's search path, this attack is feasible. Requirement 29.2.2 states that the role account's search path may be selected from two other search paths: the default search path for the role account, and the user's search path altered to include those components of the role account's search path that are not present. This leads to Requirement 29.2.5 which states that, regardless of how the search path is derived, the final search path may contain only directories (and may access only programs) that trusted users or the role account itself can manipulate. In this case, the attacker cannot place a Trojan horse where someone using the role account may execute it.

Finally, if a user is authorized to use the role account but is a novice and may change the search path, Requirement 29.2.4 allows the administrators to restrict the set of commands that the user may execute in that role.

Group 2: Authorized Users Accessing Role Accounts

Because access is allowed here, the threats relate to an authorized user changing access permissions or executing unauthorized commands.

  • Threat 29.2.5. An authorized user may obtain access to a role account and perform unauthorized commands.

  • Threat 29.2.6. An authorized user may execute a command that performs functions that the user is not authorized to perform.

  • Threat 29.2.7. An authorized user may change the restrictions on the user's ability to obtain access to the account.

The difference between Threats 29.2.5 and 29.2.6 is subtle but important. In the former, the user deliberately executes commands that violate the site security policy. In the latter, the user executes authorized commands that perform covert, unauthorized actions as well as overt, authorized actions—the classic Trojan horse. Threat 29.2.6 differs from Threat 29.2.4 because the action may not give access to authorized users; it may simply damage or destroy the system.

Requirement 29.2.4 handles Threat 29.2.5. If the user accessing the role account should execute only a specific set of commands, then the access controls must be configured to restrict the user's access to executing only those commands.

Requirements 29.2.2 and 29.2.5 handle Threat 29.2.6 by preventing the introduction of a Trojan horse, as discussed in the preceding section.

Requirement 29.2.3 answers Threat 29.2.7. Because all users who have access to root are trusted by definition, then the only way for an authorized user to change the restrictions on obtaining access to the role account is to implant a back door (which is equivalent to a Trojan horse) or to modify the access control information. But the requirement holds that only trusted users can do that, so the authorized user cannot change the information unless he is trusted—in which case, by definition, the threat is handled.

Summary

Because the requirements handle the threats, and because all requirements are used, the set of requirements is both necessary and sufficient. We now proceed with the design.

Design

To create this program, we build modules that fit together to supply security services that satisfy the requirements. First, we create a general framework to guide the development of each interface. Then we examine each requirement separately, and design a component for each requirement.

Framework

The framework begins with the user interface and then breaks down the internals of the program into modules that implement the various requirements.

User Interface

The user can run the program in two ways. The first is to request unrestricted access to the account. The second is to request that a specific program be run from the role account. Any interface must be able to handle both.

The simplest interface is a command line. Other interfaces, such as graphical user interfaces, are possible and may make the program easier to use. However, these GUIs will be built in such a way that they construct and execute a command line version of the program.

The interface chosen is

role role_account [ command ]

where role_account is the name of the role account and command is the (optional) command to execute under that account. If the user wants unrestricted access to the role account, he omits command. Otherwise, the user is given restricted access and command is executed with the privileges of the role account.

The user need not specify the time of day using the interface, because the program can obtain such information from the system. It can also obtain the location from which the user requests access, although the method used presents potential problems (see Section 29.4.3.1). The individual modules handle the remainder of the issues.

High-Level Design

The basic algorithm is as follows.

  1. Obtain the role account, command, user, location, and time of day. If the command is omitted, the user requests unrestricted access to the role account.

  2. Check that the user is allowed to access the role account

    1. at the specified location;

    2. at the specified time; and

    3. for the specified command (or without restriction).

    If the user is not, log the attempt and quit.

  3. Obtain the user and group information for the role account. Change the privileges of the process to those of the role account.

  4. If the user has requested that a specific command be run, overlay the child process with a command interpreter that spawns the named command.

  5. If the user has requested unrestricted access, overlay the child process with a command interpreter.

This algorithm points out an important ambiguity in the requirements. Requirements 29.2.1 and 29.2.4 do not indicate whether the ability of the user to execute a command in the given role account requires that the user work from a particular location or access the account at a particular time. This design uses the interpretation that a user's ability to run a command in a role account is conditioned on location and time.

The alternative interpretation, that access only is controlled by location and time, and that commands are restricted by role and user, is equally valid. But sometimes the ability to run commands may require that users work at particular times. For example, an operator may create the daily backups at 1 A.M. The operator is not to do backups at other times because of the load on the system. The interpretation of the design allows this. The alternative interpretation requires the backup program, or some other mechanism, to enforce this restriction. Furthermore, the design interpretation includes the alternative interpretation, because any control expressed in the alternative interpretation can be expressed in the design interpretation.

Requirement 29.2.4 can now be clarified. The addition is in italics.

  • Requirement 29.3.1. The mechanism shall allow both restricted access and unrestricted access to a role account. For unrestricted access, the user shall have access to a standard command interpreter. For restricted access, the user shall be able to execute only a specified set of commands. The level of access (restricted or unrestricted) shall depend on the user, the role, the time, and the location.

Thus, the design phase feeds back into the requirements phase, here clarifying the meaning of the requirements. It is left as an exercise for the reader to verify that the new form of this requirement counters the appropriate threats (see Exercise 2).

Access to Roles and Commands

The user attempting access, the location (host or terminal), the time of day, and the type of access (restricted or unrestricted) control access to the role account. The access checking module returns a value indicating success (meaning that access is allowed) or failure (meaning that access is not allowed). By the principle of fail-safe defaults, an error causes a denial of access.

We consider two aspects of the design of this module. The interface controls how information is passed to the module from its caller, and how the module returns success or failure. The internal structure of the module includes how it handles errors. This leads to a discussion of how the access control data is stored. We consider these issues separately to emphasize that the interface provides an entry point into the module, and that the entry point will remain fixed even if the internal design of the module is completely changed. The internal design and structures are hidden from the caller.

Interface

Following the practice of hiding information among modules,[4] we minimize the amount of information to be passed to the access checking module. The module requires the user requesting access, the role to which access is requested, the location, the time, and the command (if any). The return value must indicate success or failure. The question is how this information is to be obtained.

The command (or request for unrestricted access) must come from the caller, because the caller provides the interface for the processing of that command. The command is supplied externally, so the principles of layering require it to pass through the program to the module.

The caller could also pass the other information to the module. This would allow the module to provide an access control result without obtaining the information directly. The advantage is that a different program could use this module to determine whether or not access had been or would be granted at some past or future point in time, or from some other location. The disadvantage is a lack of portability, because the interface is tied to a particular representation of the data. Also, if the caller of the module is untrusted but the module is trusted, the module might make trusted decisions based on untrusted data, violating a principle of integrity.[5] Either approach is reasonable. In this design, we choose to have the module determine all of the data.

This suggests the following interface.

boolean accessok(role rname, command cmd);

where rname is the name of the requested role and cmd is the command to be executed (or is empty if unrestricted access is desired). The routine returns true if access is to be granted, and false otherwise.

Internals

This module has three parts. The first part gathers the data on which access is to be based. The second part retrieves the access control information. The third part determines whether or not the data and the access control information require access to be granted.

The module queries the operating system to determine the needed data. The real user identification data is obtained through a system call, as is the current time of day. The location consists of two components: the entry point (terminal or network connection) and the remote host from which the user is accessing the local system. The latter component may indicate that the entry point is directly connected to the system, rather than using a remote host.

Part IObtain user ID, time of day, entry point, and remote host.

Next, the module must access the access control information. The access control information resides in a file. The file contains a sequence of records of the following form.

role account
user names
locations from which the role account can be accessed
times when the role account can be accessed
command and arguments

If the “command and arguments” line is omitted, the user is granted unrestricted access. Multiple command lines may be listed in a single record.

Part IIObtain a handle (or descriptor) to the access control information. The programmer will use this handle to read the access control records from the access control information.

Finally, the program iterates through the access control information. If the role in the current record does not match the requested role, it is ignored. Otherwise, the user name, location, time, and command are compared with the appropriate fields of the record. If they all match, the module releases the handle and returns success.[6] If any of them does not match, the module continues on to the next record. If the module reaches the end of the access control information, the handle is released and the module returns failure. Note that records never deny access, but only grant it. The default action is to deny. Granting access requires an explicit record.

If any record is invalid (for example, if there is a syntax error in one of the fields or if the user field contains a nonexistent user name), the module logs the error and ignores the record. This again follows the principle of fail-safe defaults, in which the system falls into a secure state when there is an error.

Part IIIIterate through the records until one matches the data or there are no more records. In the first case, return success; in the second case, return failure.

Storage of the Access Control Data

The system administrators of the local system are to control access to privileged accounts. To keep maintenance of this information simple, the administrators store the access control information in a file. Then they need only edit the file to change a user's ability to access the privileged account. The file consists of a set of records, each containing the components listed above. This raises the issue of expression. How should each part of the record be written?

For example, must each entry point be listed, or are wildcards acceptable? Strictly speaking, the principle of fail-safe defaults[7] says that we should list explicitly those locations from which access may be obtained. In practice, this is too cumbersome. Suppose a particular user was trusted to assume a role from any system on the Internet. Requiring the administrators to list all hosts would be time-consuming as well as infeasible. Worse, if the user were not allowed to access the role account from one system, the administrators would need to check the list to see which system was missing. This would violate the principle of psychological acceptability.[8] Given the dynamic nature of the Internet, this requirement would be absurd. Instead, we allow the following special host names, all of which are illegal [723].

  • *any* (a wildcard matching any system)

  • *local* (matches the local host name)

In BNF form, the language used to express location is

  • location ::= '(' location ')' | 'not' location | location 'or' location | basic

  • basic ::= '*any*' | '*local*' | '.' domain | host

where domain and host are domain names and host names, respectively. The strings in single quotation marks are literals. The parentheses are grouping operators, the 'not' complements the associated locations, and the 'or' allows either location.

A similar question arises for times. Ignoring how times are expressed, how do we indicate when users may access the role account? Considerations similar to those above lead us to the following language, in which the keyword

  • *any* (allow access at any time)

allows access at any time. In BNF form, the language used to express time is

  • time ::= '(' time ')' | 'not' time | time 'or' time | time time | time '-' time | basic

  • basic ::= day_of_year day_of_week time_of_day | '*any*' |

  • day_of_year ::= month [ day ] [',' year ] | nmonth '/' [ day '/' ] year | empty

  • day_of_week ::= 'Sunday' | ... | 'Saturday' | 'Weekend' | 'Weekday' | empty

  • time_of_day ::= hour [':' min ] [ ':' sec ] [ 'AM' | 'PM' ] | special | empty

  • special ::= 'noon' | 'midnight' | 'morning' | 'afternoon' | 'evening'

  • empty ::= ''

where month is a string naming the month, nmonth is an integer naming the month, day is an integer naming the day of the month, and year is an integer specifying the year. Similarly, hour, min, and sec are integers specifying the hour, minute, and second. If basic is empty, it is treated as not allowing access.[9]

  • Finally, the users field of the record has a similar structure.

  • *any* (match any user)

In BNF form, the language used to express the set of users who may access a role is

  • userlist ::= '(' userlist ')' | 'not' userlist | userlist ',' userlist | user

where user is the name of a user on the system.

These “little languages” are straightforward and simple (but incomplete; see Exercise 4). Various implementation details, such as allowing abbreviations for day and month names, can be added, as can an option to change the American expression of days of the year to an international one. These points must be considered in light of where the program is to be used. Whatever changes are made, the administrators must be able to configure times and places quickly and easily, and in a manner that a reader of the access control file can understand quickly.[10]

The listing of commands requires some thought about how to represent arguments. If no arguments are listed, is the command to be run without arguments, or should it allow any set of arguments? Conversely, if arguments are listed, should the command be run only with those arguments? Our approach is to force the administrator to indicate how arguments are to be treated.

Each command line contains a command followed by zero or more arguments. If the first word after the command is an asterisk (“*”), then the command may be run with any arguments. Otherwise, the command must be run with the exact arguments provided.

The user must type the command as given in the access control file. The full path names are present to prevent the user from accidentally executing the command id with bin privileges when id is a command in the local directory, rather than the system id command.[11]

Refinement and Implementation

This section focuses on the access control module of the program. We refine the high-level design presented in the preceding section until we produce a routine in a programming language.

First-Level Refinement

Rather than use any particular programming language, we first implement the module in pseudocode. This requires two decisions. First, the implementation language will be block-structured, like C or Modula, rather than functional, like Scheme or ML. Second, the environment in which the program will function will be a UNIX-like system such as FreeBSD or Linux.

The basic structure of the security module is

boolean accessok(role rname, command cmd);
    stat ← false
    user ← obtain user ID
    timeday ← obtain time of day
    entry ← obtain entry point (terminal line, remote host)
    open access control file
    repeat
             rec ← get next record from file; EOF if nonw
             if rec ≠ EOF then
                          stat ← match(rec, rname, cmd,
user, timeday, entry)
    until rec = EOF or stat = true
    close sccess control file
return stat

We now verify that this sketch matches the design. Clearly, the interface is unchanged. The variable stat will contain the status of the access control file check, becoming true when a match is found. Initially, it is set to false (deny access) because of the principle of fail-safe defaults. If stat were not set, and the access control file were empty, stat would never be set and the returned value would be undefined.

The next three lines obtain the user ID, the current time of day, and the system entry point. The following line opens the access control file.

The routine then iterates through the records of that file. The iteration has two requirements—that if any record allows access, the routine is to return true, and that if no record grants access, the routine is to return false. From the structure of the file, one cannot create a record to deny access. By default, access is denied. Entries explicitly grant access. So, iterating over the records of the file either produces a record that grants access (in which case the match routine returns true, terminating the loop and causing accessok to return with a value of true) or produces no such record. In that case, stat is false, and rec is set to EOF when the records in the access control file are exhausted. The loop then terminates, and the routine returns the value of stat, which is false. Hence, this pseudocode matches the design and, transitively, the requirements.

Second-Level Refinement

Now we will focus on mapping the pseudocode above to a particular language and system. The C programming language is widely available and provides a convenient interface to UNIX-like systems. Given that our target system is a UNIX-like system, C is a reasonable choice. As for the operating system, there are many variants of the UNIX operating system. However, they all have fundamental similarities. The Linux operating system will provide the interfaces discussed below, and they work on a wide variety of UNIX systems.

On these systems, roles are represented as normal user accounts. The root account is really a role account,[12] for example. Each user account has two distinct representations of identity:[13] an internal user type uid_t,[14] and a string (name). When a user specifies a role, either representation may be used. For our purposes, we will assume that the caller of the accessok routine provides the uid_t representation of the role identity. Two reasons make this representation preferable. First, the target systems are unable to address privilege in terms of names, because, within the kernel, process identity is always represented by a uid_t. So the routines will need to do the conversion anyway. The second reason is more complex. Roles in the access control file can be represented by numbers or names. The routine for reading the access control file records will convert the roles to uid_ts to ensure consistency of representation. This also allows the input routine to check the records for consistency with the system environment. Specifically, if the role name refers to a nonexistent account, the routine can ignore the record. So any comparisons would require the role from the interface to be converted to a uid_t.

This leads to a design decisionrepresent all user and role IDs as integers internally. Fortunately, none of the design decisions discussed so far depend on the representation of identity, so we need not review or change our design.

Next, consider the command. On the target system, a command consists of a program name followed by a sequence of words, which are the command line arguments to the command. The command representation is an array of strings, in which the first string is the program name and the other strings are the command line arguments.

Putting this all together, the resulting interface is

int accessok(uid_t rname, char *cmd[])

Next comes obtaining the user ID. Processes in the target system have several identities, but the key ones are the real UID (which identifies the user running the process) and the effective UID (which identifies the privileges with which the process runs).[15] The effective UID of this program must have root privileges (see Exercise 3), regardless of who runs the process. Hence, it is useless for this purpose. Only the real UID identifies the user running the program. So, to obtain the user ID of the user running the program:

userid = getuid();

The time of day is obtained from the system and expressed in internal format. The internal representation can be given in seconds since a specific date and time (the epoch)[16] or in microseconds since that time. It is unlikely that times will need to be specified in microseconds in the access control file, so for both simplicity of code and simplicity of the access control data,[17] the internal format of seconds will be used. So, to obtain the current time:

timeday = time(NULL);

Finally, we need to obtain the location. There is no simple method for obtaining this information, so we defer it until later by encapsulating it in a function. This also localizes any changes should we move this program to a different system (for example, the methods used on a Linux system may differ from those used on a FreeBSD system).

entry = getlocation();

Opening the access control file for reading is straightforward:

if ((fp = fopen(acfile, "r")) == NULL{
   logerror(errno, acfile);
   return(stat);
}

Notice first the error checking, and the logging of information on an error. The variable errno is set to a code indicating the nature of the error. The variable acfile points to the access control file name.

The processing of the access control records follows:

do {
   acrec = getnextacrec(fp);
   if (acrec != NULL)
        stat = match(rec, rname, cmd, user, timeday, entry);
} until (acrec == NULL || stat == 1);

Here, we read in the record—assuming that any records remain—and check the record to see if it allows permission. This looping continues until either some record indicates that permission is to be given or all records are checked. The exact internal record format is not yet specified; hence, the use of functions.

The routine concludes by closing the access control file and returning status:

(void) fclose(fp);
return(stat);

Functions

Three functions remain: the function for obtaining location, the function for getting an access control record, and the function for checking the access control record against the information of the current process. Each raises security issues.

Obtaining Location

UNIX and Linux systems write the user's account name, the name of the terminal on which the login takes place, the time of login, and the name of the remote host (if any) to the utmp file. Any process may read this file. As each new process runs, it may have an associated terminal. To determine the utmp record associated with the process, a routine may obtain the associated terminal name, open the utmp file, and scan through the record to find the one with the corresponding terminal name. That record contains the name of the host from which the user is working.

This approach, although clumsy, works on most UNIX and Linux systems. It suffers from two problems related to security.

  1. If any process can alter the utmp file, its contents cannot be trusted. Several security holes have occurred because any process could alter the utmp file [213].

  2. A process may have no associated terminal. Such a detached process must be mapped into the corresponding utmp record through other means. However, if the utmp record contains only the information described above, this is not possible because the user may be logged into multiple terminals. The issue does not arise if the process has an associated terminal, because only one user at a time may be logged into a terminal.

In the first case, we make a design decision that if the data in the utmp file cannot be trusted because any process can alter that file, we return a meaningless location. Then, unless the location specifier of the record allows access from any location, the record will not match the current process information and will not grant access. A similar approach works if the process does not have an associated terminal.

The outline of this routine is

hostname getlocation()
      myterm ← name of terminal associated with process
      obtain utmp file access control list
      if any user other than root can alter it then
      return "*nowhere*"
      open utmp file
      repeat
      term ← get next record from utmp file; EOF if none
      if term ≠ EOF and myterm = term then
                  stat ← true
              else
                  stat ← false
      until term = EOF or stat = true
      if host field in utmp record = empty
              host = "localhost"
      else
              host = host field of utmp record
      close utmp file
      return host

We omit the implementation due to space limitations.

The Access Control Record

The format of the records in the access control file affects both the reading of the file and the comparison with the process information, so we design it here.

Our approach is to consider the match routine first. Four items must be checked: the user name, the location, the time, and the command. Consider these items separately.

The user name is represented as an integer. Thus, the internal format of the user field of the access control record must contain either integers or names that the match routine can convert to integers. If a match occurs before all user names have been checked, then the program needs to convert no more names to integers. So, we adopt the strategy of representing the user field as a string read directly from the file. The match routine will parse the line and will use lazy evaluation to check whether or not the user ID is listed.

A similar strategy can be applied to the location and the set of commands in the record.

The time is somewhat different, because in the previous two cases, the process user ID and the location had to match one of the record entries exactly. However, the time does not have to do so. Time in the access control record is (almost always) a range. For example, the entry “May 30” means any time on the date of May 30. The day begins at midnight and ends at midnight, 24 hours later. So, the range would be from May 30 at midnight to May 31 at midnight, or in internal time (for example) between 1022742000 and 1022828400. In those rare cases in which a user may assume a role only at a precise second, the range can be treated as having the same beginning and ending points. Given this view of time as ranges, checking that the current time falls into an acceptable range suggests having the match routine parse the times and checking whether or not the internal system time falls in each range as it is constructed.

This means that the routine for reading the record may simply load the record as a sequence of strings and let the match routine do the interpretation. This yields the following structure.

record
     role rname
     string userlist
     string location
     string timeofday
     string commands[]
     integer numcommands
end record;

The commands field is an array of strings, each command and argument being one string, and numcommands containing the number of commands.

Given this information, the function used to read the access control records, and the function used to match them with the current process information, are not hard to write, but error handling does deserve some mention.

Error Handling in the Reading and Matching Routines

Assume that there is a syntax error in the access control file. Perhaps a record specifies a time incorrectly (for example, “Thurxday”), or a record divider is garbled. How should the routines handle this?

The first observation is that they cannot ignore the error. To do so violates basic principles of security (specifically, the principle of psychological acceptability[18] ). It also defeats the purpose of the program, because access will be denied to users who need it.[19] So, the program must produce an indication of error. If it is printed, then the user will see it and should notify the system administrator maintaining the access control file. Should the user forget, the administrator will not know of the error. Hence, the error must be logged. Whether or not the user should be told why the error has occurred is another question. One school of thought holds that the more information users have, the more helpful they will be. Another school holds that information should be denied unless the user needs to know it, and in the case of an error in the access control file, the user only needs to know that access will be denied.

Hence, the routines must log information about errors. The logged information must enable the system administrator to locate the error in the file. The error message should include the access control file name and line or record number. This suggests that both routines need access to that information. Hence, the record counts, line numbers, and file name must be shared. For reasons of modularity, this implies that these two routines should be in a submodule of the access checking routine. If they are placed in their own module, no other parts of the routine can access the line or record numbers (and none need to, given the design described here). If the module is placed under the access control routine, no external functions can read records from the access control file or check data against that file's contents.

Summary

This section has examined the development of a program for performing a security-critical function. Beginning with a requirements analysis, the design and parts of the implementation demonstrate the need for repeated analysis to ensure that the design meets the requirements and that design decisions are documented. From the point at which the derivation stopped, the implementation is simple.

We will now discuss some common security-related programming problems. Then we will discuss testing, installation, and maintenance.

Common Security-Related Programming Problems

Unfortunately, programmers are not perfect. They make mistakes. These errors can have disastrous consequences in programs that change the protection domains. Attackers who exploit these errors may acquire extra privileges (such as access to a system account such as root or Administrator). They may disrupt the normal functioning of the system by deleting or altering services over which they should have no control. They may simply be able to read files to which they should have no access.[20] So the problem of avoiding these errors, or security holes, is a necessary issue to ensure that the programs and system function as required.

We present both management rules (installation, configuration, and maintenance) and programming rules together. Although there is some benefit in separating them, doing so creates an artificial distinction by implying that they can be considered separately. In fact, the limits on installation, configuration, and maintenance affect the implementation, just as the limits of implementation affect the installation, configuration, and maintenance procedures.

Researchers have developed several models for analyzing systems for these security holes.[21] These models provide a framework for characterizing the problems. The goal of the characterization guides the selection of the model. Because we are interested in technical modeling and not in the reason or time of introduction, many of the categories of the NRL model[22] are inappropriate for our needs. We also wish to analyze the multiple components of vulnerabilities rather than force each vulnerability into a particular point of view, as Aslam's model[23] does. So either the PA model[24] or the RISOS model[25] is appropriate. We have chosen the PA model for our analysis.

We examine each of the categories and subcategories separately. We consider first the general rules that we can draw from the vulnerability class, and then we focus on applying those rules to the program under discussion.

Improper Choice of Initial Protection Domain

Flaws involving improper choice of initial protection domain arise from incorrect setting of permissions or privileges. There are three objects for which permissions need to be set properly: the file containing the program, the access control file, and the memory space of the process. We will consider them separately.

Process Privileges

The principle of least privilege[26] dictates that no process have more privileges than it needs to complete its task, but the process must have enough privileges to complete its task successfully.

Ideally, one set of privileges should meet both criteria. In practice, different portions of the process will need different sets of privileges. For example, a process may need special privileges to access a resource (such as a log file) at the beginning and end of its task, but may not need those privileges at other times. The process structure and initial protection domain should reflect this.

  • Implementation Rule 29.5.1. Structure the process so that all sections requiring extra privileges are modules. The modules should be as small as possible and should perform only those tasks that require those privileges.

The basis for this rule lies in the reference monitor.[27] The reference monitor is verifiable, complete (it is always invoked to access the resource it protects), and tamperproof (it cannot be compromised). Here, the modules are kept small and simple (verifiable), access to the privileged resource requires the process to invoke these modules (complete), and the use of separate modules with well-defined interfaces minimizes the chances of other parts of the program corrupting the module (tamperproof).

  • Management Rule 29.5.1. Check that the process privileges are set properly.

Insufficient privileges could cause a denial of service. Excessive privileges could enable an attacker to exploit vulnerabilities in the program. To avoid these problems, the privileges of the process, and the times at which the process has these privileges, must be chosen and managed carefully.

One of the requirements of this program is availability (Requirements 29.2.1 and 29.2.4). On Linux and UNIX systems, the program must change the effective identity of the user from the user's account to the role account. This requires special (setuid) privileges of either the role account or the superuser.[28] The principle of least privilege[29] says that the former is better than the latter, but if one of the role accounts is root, then having multiple copies of the program with limited privileges is irrelevant, because the program with privileges to access the root role account is the logical target of attack. After all, if one can compromise a less privileged account through this program, the same attack will probably work against the root account. Because the Drib plans to control access to root in some cases, the program requires setuid to root privileges.

If the program does not have root privileges initially, the UNIX protection model does not allow the process to acquire them; the permissions on the program file corresponding to the program must be changed. The process must log enough information for the system administrator to identify the problem,[30] and should notify users of the problem so that the users can notify the system administrator. An alternative is to develop a server that will periodically check the permissions on the program file and reset them if needed, or a server that the program can notify should it have insufficient privileges. The designers felt that the benefits of these servers were not sufficient to warrant their development. In particular, they were concerned that the system administrators investigate any unexpected change in file permissions, and an automated server that changed the permissions back would provide insufficient incentive for an analysis of the problem.

As a result, the developers required that the program acquire root permission at start-up. The access control module is executed. Within that module, the privileges are reset to the user's once the log file and access control file have been opened.[31] Superuser privileges are needed only once more—to change the privileges to those of the role account should access be granted. This routine, also in a separate module, supplies the granularity required to provide the needed functionality while minimizing the time spent executing with root privileges.

Access Control File Permissions

Biba's models[32] emphasize that the integrity of the process relies on both the integrity of the program and the integrity of the access control file. The former requires that the program be properly protected so that only authorized personnel can alter it. The system managers must determine who the “authorized personnel” are. Among the considerations here are the principle of separation of duty[33] and the principle of least privilege.[34]

Verifying the integrity of the access control file is critical, because that file controls the access to role accounts. Some external mechanism, such as a file integrity checking tool, can provide some degree of assurance that the file has not changed. However, these checks are usually periodic, and the file might change after the check. So the program itself should check the integrity of the file when the program is run.

  • Management Rule 29.5.2. The program that is executed to create the process, and all associated control files, must be protected from unauthorized use and modification. Any such modification must be detected.

In many cases, the process will rely on the settings of other files or on some other external resources. Whenever possible, the program should check these dependencies to ensure that they are valid. The dependencies must be documented so that installers and maintainers will understand what else must be maintained in order to ensure that the program works correctly.

  • Implementation Rule 29.5.2. Ensure that any assumptions in the program are validated. If this is not possible, document them for the installers and maintainers, so they know the assumptions that attackers will try to invalidate.

The permissions of the program, and its containing directory, are to be set so only root can alter or move the program. According to Requirement 29.2.2, only root can alter the access control file. Hence, the file must be owned by root, and only root can write to it. The program should check the ownership and permissions of this file, and the containing directories, to validate that only root can alter it.

Memory Protection

As the program runs, it depends on the values of variables and other objects in memory. This includes the executable instructions themselves. Thus, protecting memory against unauthorized or unexpected alteration is critical.

Consider sharing memory. If two subjects can alter the contents of memory, then one could change data on which the second relies. Unless such sharing is required (for example, by concurrent processes), it poses a security problem because the modifying process can alter variables that control the action of the other process. Thus, each process should have a protected, unshared memory space.

If the memory is represented by an object that processes can alter, it should be protected so that only trusted processes can access it. Access here includes not only modification but also reading, because passwords reside in memory after they are types. Multiple abstractions are discussed in more detail in the next section.

  • Implementation Rule 29.5.3. Ensure that the program does not share objects in memory with any other program, and that other programs cannot access the memory of a privileged process.

Interaction with other processes cannot be eliminated. If the running process obtains input or data from other processes, then that interface provides a point through which other processes can reach the memory. The most common version of this attack is the buffer overflow.

Buffer overflows involve either altering of data or injecting of instructions that can be executed later. There are a wide variety of techniques for this [13].[37] Several remedies exist. For example, if buffers reside in sections of memory that are not executable, injecting of instructions will not work. Similarly, if some data is to remain unaltered, the data can be stored in read-only memory.

  • Management Rule 29.5.3. Configure memory to enforce the principle of least privilege. If a section of memory is not to contain executable instructions, turn execute permission off for that section of memory. If the contents of a section of memory are not to be altered, make that section read-only.

These rules appear in three ways in our program. First, the implementers use the language constructs to flag unchanging data as constant (in the C programming language, this is the keyword const). This will cause compile-time errors if the variables are assigned to, or runtime errors if instructions try to alter those constants.

The other two ways involve program loading. The system's loader places data in three areas: the data (initialized data) segment, the stack (used for function calls and variables local to the functions), and the heap (used for dynamically allocated storage). A common attack is to trick a program into executing instructions injected into three areas. The vector of injection can be a buffer overflow,[38] for example. The characteristic under discussion does not stop such alteration, but it should prevent the data from being executed by making the segments or pages of all three areas nonexecutable. This suffices for the data and stack segments and follows Management Rule 29.5.3.

If the program uses dynamic loading to load functions at runtime, the system on which the program runs will load those functions into the heap. Thus, if the segments or pages of the heap are not executable, the program cannot use dynamic loading or it will not execute properly. One solution is to compile the program in such a way that it does not use dynamic loading. Thus, the heap segment or pages can also be made nonexecutable, meeting Management Rule 29.5.3. It also prevents the program from loading a module at runtime that may be missing. This could occur if a second process deleted the appropriate library. So disabling of dynamic loading also follows Implementation Rule 29.5.3.[39]

Finally, some UNIX-like systems (including the one on which this program is being developed) allow execution permission to be turned off for the stack. The boot file sets the kernel flag to enforce this.

Trust in the System

This analysis overlooks several system components. For example, the program relies on the system authentication mechanisms to authenticate the user, and on the user information database to map users and roles into their corresponding UIDs (and, therefore, privileges). It also relies on the inability of ordinary users to alter the system clock. If any of this supporting infrastructure can be compromised, the program will not work correctly. The best that can be done is to identify these points of trust in the installation and operation documentation so that the system administrators are aware of the dependencies of the program on the system.

  • Management Rule 29.5.4. Identify all system components on which the program depends. Check for errors whenever possible, and identify those components for which error checking will not work.

For this program, the implementers should identify the system databases and information on which the program depends, and should prepare a list of these dependencies. They should discuss these dependencies with system managers to determine if the program can check for errors. When this is not possible, or when the program cannot identify all errors, they should describe the possible consequences of the errors. This document should be distributed with the program so that system administrators can check their systems before installing the program.

Improper Isolation of Implementation Detail

The problem of improper isolation of implementation detail arises when an abstraction is improperly mapped into an implementation detail. Consider how abstractions are mapped into implementations. Typically, some function (such as a database query) occurs, or the abstraction corresponds to an object in the system. What happens if the function produces an error or fails in some other way, or if the object can be manipulated without reference to the abstraction?

The first rule is to catch errors and failures of the mappings. This requires an analysis of the functions and a knowledge of their implementation. The action to take on failure also requires thought. In general, if the cause cannot be determined, the program should fail by returning the relevant parts of the system to the states they were in when the program began.[40]

  • Implementation Rule 29.5.4The error status of every function must be checked. Do not try to recover unless the cause of the error, and its effects, do not affect any security considerations. The program should restore the state of the system to the state before the process began, and then terminate.

The abstractions in this program are the notion of a user and a role, the access control information, and the creation of a process with the rights of the role. We will examine these abstractions separately.

Resource Exhaustion and User Identifiers

The notion of a user and a role is an abstraction because the program can work with role names and the operating system uses integers (UIDs). The question is how those user and role names are mapped to UIDs. Typically, this is done with a user information database that contains the requisite mapping, but the program must detect any failures of the query and respond appropriately.

The designers and implementers decided to have the program fail if, for any reason, the query failed. This application of the principle of fail-safe defaults[41] ensured that in case of error, the users would not get access to the role account.

Validating the Access Control Entries

The access control information implements the access control policy (an abstraction). The expression of the access control information is therefore the result of mapping an abstraction to an implementation. The question is whether or not the given access control information correctly implements the policy. Answering this question requires someone to examine the implementation expression of the policy.

The programmers developed a second program that used the same routines as the role-assuming program to analyze the access control entries. This program prints the access control information in an easily readable format. It allows the system managers to check that the access control information is correct. A specific procedure requires that this information be checked periodically, and always after the file or the program is altered.

Restricting the Protection Domain of the Role Process

Creating a role process is the third abstraction. There are two approaches. Under UNIX-like systems, the program can spawn a second, child, process. It can also simply start up a second program in such a way that the parent process is replaced by the new process. This technique, called overlaying, is intrinsically simpler than creating a child process and exiting. It allows the process to replace its own protection domain with the (possibly) more limited one corresponding to the role. The programmers elected to use this method. The new process inherits the protection domain of the original one. Before the overlaying, the original process must reset its protection domain to that of the role. The programmers do so by closing all files that the original process opened, and changing its privileges to those of the role.

The components of the protection domain that the process must reset before the overlay are the open files (except for standard input, output, and error), which must be closed, the signal handlers, which must be reset to their default values, and any user-specific information, which must be cleared.

Improper Change

This category describes data and instructions that change over time. The danger is that the changed values may be inconsistent with the previous values. The previous values dictate the flow of control of the process. The changed values cause the program to take incorrect or nonsecure actions on that path of control.

The data and instructions can reside in shared memory, in nonshared memory, or on disk. The last includes file attribute information such as ownership and access control list.

Memory

First comes the data in shared memory. Any process that can access shared memory can manipulate data in that memory. Unless all processes that can access the shared memory implement a concurrent protocol for managing changes, one process can change data on which a second process relies. As stated above, this could cause the second process to violate the security policy.

  • Implementation Rule 29.5.5If a process interacts with other processes, the interactions should be synchronized. In particular, all possible sequences of interactions must be known and, for all such interactions, the process must enforce the required security policy.

A variant of this situation is the asynchronous exception handler. If the handler alters variables and then returns to the previous point in the program, the changes in the variables could cause problems similar to the problem of concurrent processes. For this reason, if the exception handler alters any variables on which other portions of the code depend, the programmer must understand the possible effects of such changes. This is just like the earlier situation in which a concurrent process changes another's variables in a shared memory space.

  • Implementation Rule 29.5.6Asynchronous exception handlers should not alter any variables except those that are local to the exception handling module. An exception handler should block all other exceptions when begun, and should not release the block until the handler completes execution, unless the handler has been designed to handle exceptions within itself (or calls an uninvoked exception handler).

A second approach applies whether the memory is shared or not. A user feeds bogus information to the program, and the program accepts it. The bogus data overflows its buffer, changing other data, or inserting instructions that can be executed later.

In terms of trust, the return address (a trusted datum) can be affected by untrusted data (from the input). This lowers the trustworthiness of the return address to that of input data. One need not supply instructions to breach security.

A technique in which canaries protect data, not only the return address, would work, but raises many implementation problems (see Exercise 6).

  • Implementation Rule 29.5.7. Whenever possible, data that the process trusts and data that it receives from untrusted sources (such as input) should be kept in separate areas of memory. If data from a trusted source is overwritten with data from an untrusted source, a memory error will occur.

In more formal terms, the principle of least common mechanism[45] indicates that memory should not be shared in this way.

These rules apply to our program in several ways. First, the program does not interact with any other program except through exception handling.[46] So Implementation Rule 29.5.5 does not apply. Exception handling consists of calling a procedure that disables further exception handling, logs the exception, and immediately terminates the program.

Illicit alteration of data in memory is the second potential problem. If the user-supplied data is read into memory that overlaps with other program data, it could erase or alter that data. To satisfy Implementation Rule 29.5.7, the programmers did not reuse variables into which users could input data. They also ensured that each access to a buffer did not overlap with other buffers.

The problem of buffer overflow is solved by checking all array and pointer references within the code. Any reference that is out of bounds causes the program to fail after logging an error message to help the programmers track down the error.

Changes in File Contents

File contents may change improperly. In most cases, this means that the file permissions are set incorrectly or that multiple processes are accessing the file, which is similar to the problem of concurrent processes accessing shared memory. Management Rule 29.5.2 and Implementation Rule 29.5.5 cover these two cases.

A nonobvious corollary is to be careful of dynamic loading. Dynamic load libraries are not part of this program's executable. They are loaded, as needed, when the program runs. Suppose one of the libraries is changed, and the change causes a side effect. The program may cease to function or, even worse, work incorrectly.

If the dynamic load modules cannot be altered, then this concern is minimal, but if they can be upgraded or otherwise altered, it is important. Because one of the reasons for using dynamic load libraries is to allow upgrades without having to recompile programs that depend on the library, security-related programs using dynamic load libraries are at risk.

  • Implementation Rule 29.5.8. Do not use components that may change between the time the program is created and the time it is run.

This is another reason that the developers decided not to use dynamic loading.

Race Conditions in File Accesses

A race condition in this context is the time-of-check-to-time-of-use problem. As with memory accesses, the file being used is changed after validation but before access.[47] To thwart it, either the file must be protected so that no untrusted user can alter it, or the process must validate the file and use it indivisibly. The former requires appropriate settings of permission, so Management Rule 29.5.2 applies. Section 29.5.7, “Improper Indivisibility,” discusses the latter.

This program validates that the owner and access control permissions for the access control file are correct (the check). It then opens the file (the use). If an attacker can change the file after the validation but before the opening, so that the file checked is not the file opened, then the attacker can have the program obtain access control information from a file other than the legitimate access control file. Presumably, the attacker would supply a set of access control entries allowing unauthorized accesses.

The program does exactly this. It opens the access control file and uses the file descriptor, which references the file attribute information directly to obtain the owner, group, and access control permissions. Those permissions are checked. If they are correct, the program uses the file descriptor to read the file. Otherwise, the file is closed and the program reports a failure.

Improper Naming

Improper naming refers to an ambiguity in identifying an object. Most commonly, two different objects have the same name.[51] The programmer intends the name to refer to one of the objects, but an attacker manipulates the environment and the process so that the name refers to a different object. Avoiding this flaw requires that every object be unambiguously identified. This is both a management concern and an implementation concern.

Objects must be uniquely identifiable or completely interchangeable. Managing these objects means identifying those that are interchangeable and those that are not. The former objects need a controller (or set of controllers) that, when given a name, selects one of the objects. The latter objects need unique names. The managers of the objects must supply those names.

  • Management Rule 29.5.5. Unique objects require unique names. Interchangeable objects may share a name.

A name is interpreted within a context. At the implementation level, the process must force its own context into the interpretation, to ensure that the object referred to is the intended object. The context includes information about the character sets, process and file hierarchies, network domains, and any accessible variables such as the search path.

  • Implementation Rule 29.5.9. The process must ensure that the context in which an object is named identifies the correct object.

This program uses names for external objects in four places: the name of the access control file, the names of the users and roles, the names of the hosts, and the name of the command interpreter (the shell) that the program uses to execute commands in the role account.

The two file names (access control file and command interpreter) must identify specific files. Absolute path names specify the location of the object with respect to a distinguished directory called / or the “root directory.” However, a privileged process can redefine / to be any directory.[52] This program does not do so. Furthermore, if the root directory is anything other than the root directory of the system, a trusted process has executed it. No untrusted user could have done so. Thus, as long as absolute path names are specified, the files are unambiguously named.

The name provided may be interpreted in light of other aspects of the environment. For example, differences in the encoding of characters can transform file names. Whether characters are made up of 16 bits, 8 bits, or 7 bits can change the interpretation, and therefore the referent, of a file name. Other environment variables can change the interpretation of the path name. This program simply creates a new, known, safe environment for execution of the commands.[53]

This has two advantages over sanitization of the existing context. First, it avoids having the program analyze the environment in detail. The meaning of each aspect of the environment need not be analyzed and examined. The environment is simply replaced. Second, it allows the system to evolve without compromising the security of the program. For example, if a new environment variable is assigned a meaning that affects how programs are executed, the variable will not affect how the program executes its commands because that variable will not appear in the command's environment. So this program closes all file descriptors, resets signal handlers, and passes a new set of environment variables for the command.

These actions satisfy Implementation Rule 29.5.9.

The developers assumed that the system was properly maintained, so that the names of the users and roles would map into the correct UIDs. (Section 29.5.2.1 discusses this.) This applies to Management Rule 29.5.5.

The host names are the final set of names. These may be specified by names or IP addresses. If the former, they must be fully qualified domain names to avoid ambiguity. To see this, suppose an access control entry allows user matt to access the role wheel when logging in from the system amelia. Does this mean that the system names amelia in the local domain, or any system named amelia from any domain? Either interpretation is valid. The former is more reasonable,[54] and applying this interpretation resolves the ambiguity. (The program implicitly maps names to fully qualified domain names using the former interpretation. Thus, amelia in the access control entry would match a host named amelia in the local domain, and not a host named amelia in another domain.) This implements Implementation Rule 29.5.9.[55]

As a side note, if the local network is mismanaged or compromised, the name amelia may refer to a system other than the one intended. For example, the real host amelia may crash or go offline. An attacker can then reset the address of his host to correspond to amelia. This program will not detect the impersonation.

Improper Deallocation or Deletion

Failing to delete sensitive information raises the possibility of another process seeing that data at a later time. In particular, cryptographic keywords, passwords, and other authentication information should be discarded once they have been used. Similarly, once a process has finished with a resource, that resource should be deallocated. This allows other processes to use that resource, inhibiting denial of service attacks.

A consequence of not deleting sensitive information is that dumps of memory, which may occur if the program receives an exception or crashes for some other reason, contain the sensitive data. If the process fails to release sensitive resources before spawning unprivileged subprocesses, those unprivileged subprocesses may have access to the resource.

  • Implementation Rule 29.5.10. When the process finishes using a sensitive object (one that contains confidential information or one that should not be altered), the object should be erased, then deallocated or deleted. Any resources not needed should also be released.

Our program uses three pieces of sensitive information. The first is the clear-text password, which authenticates the user. The password is hashed, and the hash is compared with the stored hash. Once the hash of the entered password has been computed, the process must delete the cleartext password. So it overwrites the array holding the password with random bytes.

The second piece of sensitive information is the access control information. Suppose an attacker wanted to gain access to a role account. The access control entries would tell the attacker which users could access that account using this program. To prevent the attacker from gaining this information, the developers decided to keep the contents of the access control file confidential. The program accesses this file using a file descriptor. File descriptors remain open when a new program overlays a process. Hence, the program closes the file descriptor corresponding to the access control file once the request has been validated (or has failed to be validated).

The third piece of sensitive information is the log file. The program alters this file. If an unprivileged program such as one run by this program were to inherit the file descriptor, it could flood the log. Were the log to fill up, the program could no longer log failures. So the program also closes the log file before spawning the role's command.

Improper Validation

The problem of improper validation arises when data is not checked for consistency and correctness. Ideally, a process would validate the data against the more abstract policies to ensure correctness. In practice, the process can check correctness only by looking for error codes (indicating failure of functions and procedures) or by looking for patently incorrect values (such as negative numbers when positive ones are required).

As the program is designed, the developers should determine what conditions must hold at each interface and each block of code. They should then validate that these conditions hold.

What follows is a set of validations that are commonly overlooked. Each program requires its own analysis, and other types of validation may be critical to the correct, secure functioning of the program, so this list is by no means complete.

Bounds Checking

Errors of validation often occur when data is supposed to lie within bounds. For example, a buffer may contain entries numbered from 0 to 99. If the index used to access the buffer elements takes on a value less than 0 or greater than 99, it is an invalid operand because it accesses a nonexistent entry. The variable used to access the element may not be an integer (for example, it may be a set element or pointer), but in any case it must reference an existing element.

  • Implementation Rule 29.5.11. Ensure that all array references access existing elements of the array. If a function that manipulates arrays cannot ensure that only valid elements are referenced, do not use that function. Find one that does, write a new version, or create a wrapper.

In this program, all loops involving arrays compare the value of the variable referencing the array against the indexes (or addresses) of both the first and last elements of the array. The loop terminates if the variable's value is outside those two values. This covers all loops within the program, but it does not cover the loops in the library functions.

For loops in the library functions, bounds checking requires an analysis of the functions used to manipulate arrays. The most common type of array for which library functions are used is the character string, which is a sequence of characters (bytes) terminating with a 0 byte. Because the length of the string is not encoded as part of the string, functions cannot determine the size of the array containing the string. They simply operate on all bytes until a 0 byte is found.

The programmers use only those functions that bound the sizes of arrays. In particular, the function fgets is used to read input, because it allows the programmer to specify that a maximum number of characters are to be read. (This solves the problem that plagued fingerd.[57]

Type Checking

Failure to check types is another common validation problem. If a function parameter is an integer, but the actual argument passed is a floating point number, the function will interpret the bit pattern of the floating point number as an integer and will produce an incorrect result.

  • Implementation Rule 29.5.12. Check the types of functions and parameters.

A good compiler and well-written code will handle this particular problem. All functions should be declared before they are used. Most programming languages allow the programmer to specify the number and types of arguments, as well as the type of the return value (if any). The compiler can then check the types of the declarations against the types of the actual arguments and return values.

  • Management Rule 29.5.6. When compiling programs, ensure that the compiler flags report inconsistencies in types. Investigate all such warnings and either fix the problem or document the warning and why it is spurious.

Error Checking

A third common problem involving improper validation is failure to check return values of functions. For example, suppose a program needs to determine ownership of a file. It calls a system function that returns a record containing information from the file attribute table. The program obtains the owner of the file from the appropriate field of the record. If the function fails, the information in the record is meaningless. So, if the function's return status is not checked, the program may act erroneously.

  • Implementation Rule 29.5.13. Check all function and procedure executions for errors.

This program makes extensive use of system and library functions, as well as its own internal functions (such as the access control module). Every function returns a value, and the value is checked for an error before the results of the function are used. For example, the function that obtains the ownership and access permissions of the access control file would return meaningless information should the function fail. So the function's return value is checked first for an error; if no error has occurred, then the file attribute information is used.

As another example, the program opens a log file. If the open fails, and the program tries to write to the (invalid) file descriptor obtained from the function that failed, the program will terminate as a result of an exception. Hence, the program checks the result of opening the log file.

Checking for Valid, not Invalid, Data

Validation should apply the principle of fail-safe defaults.[58] This principle requires that valid values be known, and that all other values be rejected. Unfortunately, programmers often check for invalid data and assume that the rest is valid.

  • Implementation Rule 29.5.14. Check that a variable's values are valid.

This program checks that the command to be executed matches one of the authorized commands. It does not have a set of commands that are to be denied. The program will detect an invalid command as one that is not listed in the set of authorized commands for that user accessing that role at the time and place allowed.

As discussed in Section 29.3.2.3, it is possible to allow all users except some specific users access to a role by an appropriate access control entry (using the keyword not). The developers debated whether having this ability was appropriate because its use could lead to violations of the principle of fail-safe defaults. On one key system, however, the only authorized users were system administrators and one or two trainees. The administrators wanted the ability to shut the trainees out of certain roles. So the developers added the keyword and recommended against its use except in that single specific situation.

  • Management Rule 29.5.7. If a trade-off between security and other factors results in a mechanism or procedure that can weaken security, document the reasons for the decision, the possible effects, and the situations in which the compromise method should be used. This informs others of the trade-off and the attendant risks.

Checking Input

All data from untrusted sources must be checked. Users are untrusted sources. The checking done depends on the way the data is received: into an input buffer (bounds checking) or read in as an integer (checking the magnitude and sign of the input).

  • Implementation Rule 29.5.15. Check all user input for both form and content. In particular, check integers for values that are too big or too small, and check character data for length and valid characters.

The program determines what to do on the basis of at least two pieces of data that the user provides: the role name and the command (which, if omitted, means unrestricted access).[59] Users must also authenticate themselves appropriately. The program must first validate that the supplied password is correct. It then checks the access control information to determine whether the user is allowed access to the role at that time and from that location.

The length of the input password must be no longer than the buffer in which it is placed. Similarly, the lines of the access control file must not overflow the buffer allocated for it. The contents of the lines of the access control file must make up a valid access control entry. This is most easily done by constraining the format of the contents of the file, as discussed in the next section.

An excellent example of the need to constrain user input comes from formatted print statements in C.

Designing for Validation

Sometimes data cannot be validated completely. For example, in the C programming language, a programmer can test for a NULL pointer (meaning that the pointer does not hold the address of any object), but if the pointer is not NULL, checking the validity of the pointer may be very difficult (or impossible). Using a language with strong type checking is another example.

The consequence of the need for validation requires that data structures and functions be designed and implemented in such a way that they can be validated. For example, because C pointers cannot be properly validated, programmers should not pass pointers or use them in situations in which they must be validated. Methods of data hiding, type checking, and object-oriented programming often provide mechanisms for doing this.

  • Implementation Rule 29.5.16. Create data structures and functions in such a way that they can be validated.

An example will show the level of detail necessary for validation. The entries in the access control file are designed to allow the program to detect obvious errors. Each access control entry consists of a block of information in the following format.

role name
    users comma-separated list of users
    location comma-separated list of locations
    time comma-separated list of times
    command command and arguments
    ...
    command command and arguments
endrole

This defines each component of the entry. (The lines need not be in any particular order.) The syntax is well-defined, and the access control module in the program checks for syntax errors. The module also performs other checks, such as searching for invalid user names in the users field and requiring that the full path names of all commands be specified. Finally, note that the module computes the number of commands for the module's internal record. This eliminates a possible source of error—namely, that the user may miscount the number of commands.

In case of any error, the process logs the error, if possible, and terminates. It does not allow the user to access the role.

Improper Indivisibility

Improper indivisibility[60] arises when an operation is considered as one unit (indivisible) in the abstract but is implemented as two units (divisible). The race conditions discussed in Section 29.5.3.3 provide one example. The checking of the access control file attributes and the opening of that file are to be executed as one operation. Unfortunately, they may be implemented as two separate operations, and an attacker who can alter the file after the first but before the second operation can obtain access illicitly. Another example arises in exception handling. Often, program statements and system calls are considered as single units or operations when the implementation uses many operations. An exception divides those operations into two sets: the set before the exception, and the set after the exception. If the system calls or statements rely on data not changing during their execution, exception handlers must not alter the data.

Section 29.5.3 discusses handling of these situations when the operations cannot be made indivisible. Approaches to making them indivisible include disabling interrupts and having the kernel perform operations. The latter assumes that the operation is indivisible when performed by the kernel, which may be an incorrect assumption.

  • Implementation Rule 29.5.17. If two operations must be performed sequentially without an intervening operation, use a mechanism to ensure that the two cannot be divided.

In UNIX systems, the problem of divisibility arises with root processes such as the program under consideration. UNIX-like systems do not enforce the principle of complete mediation.[61] For root, access permissions are not checked. Recall the xterm example in Section 23.4.5.1. A user needed to log information from the execution of xterm, and specified a log file. Before appending to that file, xterm needed to ensure that the real UID could write to the log file. This required an extra system call. As a result, operations that should have been indivisible (the access check followed by the opening of the file) were actually divisible. One way to make these operations indivisible on UNIX-like systems is to drop privileges to those of the real UID, then open the file. The access checking is done in the kernel as part of the open.

Improper indivisibility arises in our program when the access control module validates and then opens the access control file. This should be a single operation, but because of the semantics of UNIX-like systems, it must be performed as two distinct operations. It is not possible to ensure the indivisibility of the two operations. However, it is possible to ensure that the target of the operations does not change, as discussed in Section 29.5.3, and this suffices for our purposes.

Improper Sequencing

Improper sequencing means that operations are performed in an incorrect order. For example, a process may create a lock file and then write to a log file. A second process may also write to the log file, and then check to see if the lock file exists. The first program uses the correct sequence of calls; the second does not (because that sequence allows multiple writers to access the log file simultaneously).

  • Implementation Rule 29.5.18. Describe the legal sequences of operations on a resource or object. Check that all possible sequences of the program(s) involved match one (or more) legal sequences.

In our program, the sequence of operations in the design shown in Section 29.3.1.2 follow a proper order. The user is first authenticated. Then the program uses the access control information to determine if the requested access is valid. If it is, the appropriate command is executed using a new, safe environment.

A second sequence of operations occurs when privileges to the role are dropped. First, group privileges are changed to those of the role. Then all user identification numbers are changed to those of the role. A common error is to switch the user identification numbers first, followed by the change in group privileges. Because changing group privileges requires root privileges, the change will fail. Hence, the programmers used the stated ordering.

Improper Choice of Operand or Operation

Preventing errors of choosing the wrong operand or operation requires that the algorithms be thought through carefully (to ensure that they are appropriate). At the implementation level, this requires that operands be of an appropriate type and value, and that operations be selected to perform the desired functions. The difference between this type of error and improper validation lies in the program. Improper implementation refers to a validation failure. The operands may be appropriate, but no checking is done. In this category, even though the operands may have been checked, they may still be inappropriate.

Assurance techniques[62] help detect these problems. The programmer documents the purpose of each function and then checks (or, preferably, others check) that the algorithms in the function work properly and that the code correctly implements the algorithms.

  • Management Rule 29.5.8. Use software engineering and assurance techniques (such as documentation, design reviews, and code reviews) to ensure that operations and operands are appropriate.

Within our program, many operands and operations control the granting (and denying) of access, the changing to the role, and the execution of the command. We first focus on the access part of the program, and afterwards we consider two other issues.

First, a user is granted access only when an access control entry matches all characteristics of the current session. The relevant characteristics are the role name, the user's UID, the role's name (or UID), the location, the time, and the command. We begin by checking that if the characteristics match, the access control module returns true (allowing access). We also check that the caller grants access when the module returns true and denies access when the module returns false.

Next, we consider the user's UID. That object is of type uid_t. If the interface to the system database returns an object of a different type, conversion becomes an issue. Specifically, many interfaces treat the UID as an integer. The difference between the types int and uid_t may cause problems. On the systems involved, uid_t is an unsigned integer. Since we are comparing signed and unsigned integers, C simply converts the signed integers to unsigned integers, and the comparison succeeds. Hence, the choice of operation (comparison, here) is proper.

Checking location requires the program to derive the user's location, as discussed above, and pass it to the validator. The validator takes a string and determines whether it matches the pattern in the location field of the access control entry. If the string matches, the module should continue; otherwise, it should terminate and return false.

Unlike the location, a variable of type time_t contains the current time. The time checking portion of the module processes the string representing the allowed times and determines if the current time falls in the range of allowed times. Checking time is different than checking location because legal times are ranges, except in one specific situation: when an allowed time is specified to the exact second. A specification of an exact time is useless, because the program may not obtain the time at the exact second required. This would lead to a denial of service, violating Requirement 29.2.4. Also, allowing exact times leads to ambiguity.

The use of signal handlers provides a second situation in which an improper choice of operation could occur. A signal indicates either an error in the program or a request from the user to terminate, so a signal should cause the program to terminate. If the program continues to run, and then grants the user access to the role account, either the program has continued in the face of an error or it has overridden the user's attempt to terminate the program.

Summary

This type of top-down analysis differs from the more usual approach of taking a checklist of common vulnerabilities and using it to examine code. There is a place for each of these approaches. The top-down approach presented here is a design approach, and should be applied at each level of design and implementation. It emphasizes documentation, analysis, and understanding of the program, its interfaces, and the environment in which it executes. A security analysis document should describe the analysis and the reasons for each security-related decision. This document will help other analysts examine the program and, more importantly, will provide future developers and maintainers of the program with insight into potential problems they may encounter in porting the program to a different environment, adding new features, or changing existing features.

Once the appropriate phase of the program has been completed, the developers should use a checklist to validate that the design or implementation has no common errors. Given the complexity of security design and implementation, such checklists provide valuable confirmation that the developers have taken common security problems into account. The following lists summarize the implementation and management rules above.

List of Implementation Rules

Implementation Rule 29.5.1. Structure the process so that all sections requiring extra privileges are modules. The modules should be as small as possible and should perform only those tasks that require those privileges.

Implementation Rule 29.5.2. Ensure that any assumptions in the program are validated. If this is not possible, document them for the installers and maintainers, so they know the assumptions that attackers will try to invalidate.

Implementation Rule 29.5.3. Ensure that the program does not share objects in memory with any other program, and that other programs cannot access the memory of a privileged process.

Implementation Rule 29.5.4. The error status of every function must be checked. Do not try to recover unless the cause of the error, and its effects, do not affect any security considerations. The program should restore the state of the system to the state before the process began, and then terminate.

Implementation Rule 29.5.5. If a process interacts with other processes, the interactions should be synchronized. In particular, all possible sequences of interactions must be known and, for all such interactions, the process must enforce the required security policy.

Implementation Rule 29.5.6. Asynchronous exception handlers should not alter any variables except those that are local to the exception handling module. An exception handler should block all other exceptions when begun, and should not release the block until the handler completes execution, unless the handler has been designed to handle exceptions within itself (or calls an uninvoked exception handler).

Implementation Rule 29.5.7. Whenever possible, data that the process trusts and data that it receives from untrusted sources (such as input) should be kept in separate areas of memory. If data from a trusted source is overwritten with data from an untrusted source, a memory error will occur.

Implementation Rule 29.5.8. Do not use components that may change between the time the program is created and the time it is run.

Implementation Rule 29.5.9. The process must ensure that the context in which an object is named identifies the correct object.

Implementation Rule 29.5.10. When the process finishes using a sensitive object (one that contains confidential information or one that should not be altered), the object should be erased, then deallocated or deleted. Any resources not needed should also be released.

Implementation Rule 29.5.11. Ensure that all array references access existing elements of the array. If a function that manipulates arrays cannot ensure that only valid elements are referenced, do not use that function. Find one that does, write a new version, or create a wrapper.

Implementation Rule 29.5.12. Check the types of functions and parameters.

Implementation Rule 29.5.13. Check all function and procedure executions for errors.

Implementation Rule 29.5.14. Check that a variable's values are valid.

Implementation Rule 29.5.15. Check that a variable's values are valid.

Implementation Rule 29.5.16. Create data structures and functions in such a way that they can be validated.

Implementation Rule 29.5.17. If two operations must be performed sequentially without an intervening operation, use a mechanism to ensure that the two cannot be divided.

Implementation Rule 29.5.18. Describe the legal sequences of operations on a resource or object. Check that all possible sequences of the program(s) involved match one (or more) legal sequences.

List of Management Rules

Management Rule 29.5.1. Check that the process privileges are set properly.

Management Rule 29.5.2. The program that is executed to create the process, and all associated control files, must be protected from unauthorized use and modification. Any such modification must be detected.

Management Rule 29.5.3. Configure memory to enforce the principle of least privilege. If a section of memory is not to contain executable instructions, turn execute permission off for that section of memory. If the contents of a section of memory are not to be altered, make that section read-only.

Management Rule 29.5.4. Identify all system components on which the program depends. Check for errors whenever possible, and identify those components for which error checking will not work.

Management Rule 29.5.5. Unique objects require unique names. Interchangeable objects may share a name.

Management Rule 29.5.6. When compiling programs, ensure that the compiler flags report inconsistencies in types. Investigate all such warnings and either fix the problem or document the warning and why it is spurious.

Management Rule 29.5.7. If a trade-off between security and other factors results in a mechanism or procedure that can weaken security, document the reasons for the decision, the possible effects, and the situations in which the compromise method should be used. This informs others of the trade-off and the attendant risks.

Management Rule 29.5.8. Use software engineering and assurance techniques (such as documentation, design reviews, and code reviews) to ensure that operations and operands are appropriate.

Testing, Maintenance, and Operation

Testing provides an informal validation of the design and implementation of the program. The goal of testing is to show that the program meets the stated requirements. When design and implementation are driven by the requirements, as in the method used to create the program under discussion, testing is likely to uncover only minor problems, but if the developers do not have well-articulated requirements, or if the requirements are changed during development, testing may uncover major problems, requiring changes up to a complete redesign and reimplementation of a program. The worst mistake managers and developers can make is to take a program that does not meet the security requirements and add features to it to meet those requirements. The problem is that the basic design does not meet the security requirements. Adding security features will not ameliorate this fundamental flaw.

Once the program has been written and tested, it must be installed. The installation procedure must ensure that when a user starts the process, the environment in which the process is created matches the assumptions embodied in the design. This constrains the configuration of the program parameters as well as the manner in which the system is configured to protect the program. Finally, the installers must enable trusted users to modify and upgrade the program and the configuration files and parameters.

Testing

The results of testing a program are most useful if the tests are conducted in the environment in which the program will be used (the production environment). So, the first step in testing a program is to construct an environment that matches the production environment. This requires the testers to know the intended production environment. If there are a range of environments, the testers must test the programs in all of them. Often there is overlap between the environments, so this task is not so daunting as it might appear.

The production environment should correspond to the environment for which the program was developed. A symptom of discrepancies between the two environments is repeated failures resulting from erroneous assumptions. This indicates that the developers have implicity embedded information from the development environment that is inconsistent with the testing environment. This discrepancy must be reconciled.

The testing process begins with the requirements. Are they appropriate? Do they solve the problem? This analysis may be moot (if the task is to write a program meeting the given requirements), but if the task is phrased in terms of a problem to be solved, the problem drives the requirements. Because the requirements drive the design of the program, the requirements must be validated before designing begins.

As many of the software life cycle models indicate, this step may be revisited many times during the development of the program. Requirements may prove to be impossible to meet, or may produce problems that cannot be solved without changing the requirements. If the requirements are changed, they must be reanalyzed and verified to solve the problem.

Then comes the design. Section 29.4 discusses the stepwise refinement of the program. The decomposition of the program into modules allows us to test the program as it is being implemented. Then, once it has been completed, the testing of the entire program should demonstrate that the program meets its requirements in the given environment.

The general philosophy of testing is to execute all possible paths of control and compare the results with the expected results. In practice, the paths of control are too numerous to test exhaustively. Instead, the paths are analyzed and ordered. Test data is generated for each path, and the testers compare the results obtained from the actual data with the expected results. This continues until as many paths as possible have been tested.

For security testing, the testers must test not only the most commonly used paths but also the least commonly used paths.[63] The latter often create security problems that attackers can exploit. Because they are relatively unused, traditional testing places them at a lower priority than that of other paths. Hence, they are not as well scrutinized, and vulnerabilities are missed.

The ordering of the paths relies on the requirements. Those paths that perform multiple security checks are more critical than those that perform single (or no) security checks because they introduce interfaces that affect security requirements. The other paths affect security, of course, but there are no interfaces.

First, we examine a module that calls no other module. Then we examine the program as a composition of modules. We conclude by testing the installation, configuration, and use instructions.

Testing the Module

The module may invoke one or more functions. The functions return results to the caller, either directly (through return values or parameter lists) or indirectly (by manipulation of the environment). The goal of this testing is to ensure that the module exhibits correct behavior regardless of what the functions returns.

The first step is to define “correct behavior.” During the design of the program, the refinement process led to the specification of the module and the module's interface. This specification defines “correct behavior,” and testing will require us to check that the specification holds.

We begin by listing all interfaces to the module. We will then use this list to execute four different types of tests. The types of test are as follows.

  1. Normal data testsThese tests provide unexceptional data.The data should be chosen to exercise as many paths of control through the module as possible.

  2. Boundary data testsThese tests provide data that tests any limits to the interfaces. For example, if the module expects a string of up to 256 characters to be passed in, these tests invoke the module and pass in arrays of 255, 256, and 257 characters. Longer strings should also be used in an effort to overflow internal buffers. The testers can examine the source code to determine what to try. Limits here do not apply simply to arrays or strings. In the program under discussion, the lowest allowed UID is 0, for root. A good test would be to try a UID of –1 to see what happens. The module should report an error.

  1. Exception testsThese tests determine how the program handles interrupts and traps. For example, many systems allow the user to send a signal that causes the program to trap to a signal handler, or to take a default action such as dumping the contents of memory to a core file. These tests determine if the module leaves the system in a nonsecure state—for example, by leaving sensitive information in the memory dump. They also analyze what the process does if ordinary actions (such as writing to a file) fail.

  1. Random data testsThese tests supply inputs generated at random and observe how the module reacts. They should not corrupt the state of the system. If the module fails, it should restore the system to a safe state.[65]

Throughout the testing, the testers should keep track of the paths taken. This allows them to determine how complete the testing is. Because these tests are highly informal, the assurance they provide is not as convincing as the techniques discussed in Chapter 19. However, it is more than random tests, or no tests, would provide.

Testing Composed Modules

Now consider a module that calls other modules. Each of the invoked modules has a specification describing its actions. So, in addition to the tests discussed in the preceding section, one other type of test should be performed.

  1. Error handling testsThese tests assume that the called modules violate their specifications in some way. The goal of these tests is to determine how robust the caller is. If it fails gracefully, and restores the system to a safe state, then the module passes the test. Otherwise, it fails and must be rewritten.

Testing the Program

Once the testers have assembled the program and its documentation, the final phase of testing begins. The testers have someone follow the installation and configuration instructions. This person should not be a member of the testing team, because the testing team has been working with the program and is familiar with it. The goal of this test is to determine if the installation and configuration instructions are correct and easy to understand. The principle of psychological acceptability[66] requires that the tool be as easy to install and use as possible. Because most installers and users will not have experience with the program, the testers need to evaluate how they will understand the documentation and whether or not they can install the program correctly by following the instructions. An incorrectly installed security tool does not provide security; it may well detract from it. Worse, it gives people a false sense of security.

Distribution

Once the program has been completed, it must be distributed. Distribution involves placing the program in a repository where it cannot be altered except by authorized people, and from which it can be retrieved and sent to the intended recipients. This requires a policy for distribution. Specific factors to be considered are as follows.

  1. Who can use the program? If the program is licensed to a specific organization, or to a specific host, then each copy of the program that is distributed must be tied to that organization or host so it cannot be redistributed or pirated. This is an originator controlled policy.[67] One approach is to provide the licensee with a secret key and encipher the software with the same key. This prevents redistribution without the licensee's consent, unless the attacker breaks the cryptosystem or steals the licensee's key.[68]

  2. How can the integrity of the master copy be protected? If an attacker can alter the master copy, from which distribution copies are made, then the attacker can compromise all who use the program.

    Part of the problem is credibility. If an attacker can pose as the vendor, then all who obtain the program from the attacker will be vulnerable to attack. This tactic undermines trust in the program and can be surprisingly hard to counter. It is analogous to generating a cryptographic checksum for a progam infected with a computer virus.[70] When an uninfected program is obtained, the integrity checker complains because the checksum is wrong. In our example, when the real vendor contacts the duped customer, the customer usually reacts with disbelief, or is unwilling to concede that his system has been compromised.

  3. How can the availability of the program be ensured? If the program is sent through a physical medium, such as a CD-ROM, availability is equivalent to the availability of mail or messenger services between the vendor and the buyer. If the program is distributed through electronic means, however, the distributor must take precautions to ensure that the distribution site is available. Denial of service attacks such as SYN flooding[71] may hamper the availability.

Like a program, the distribution is controlled by a policy. All considerations that affect a security policy affect the distribution policy as well.

Conclusion

The purpose of this chapter was to provide a glimpse of techniques that provide better than ordinary assurance that a program's design and implementation satisfy its requirements. This chapter is not a manual on applying high-assurance techniques. In terms of the techniques discussed in Part 6, “Assurance,” this chapter describes low-assurance techniques.

However, given the current state of programming and software development, these low-assurance techniques enable programmers to produce significantly better, more robust, and more usable code than they could produce without these techniques. So, using a methodology similar to the one outlined in this chapter will reduce vulnerabilities and improve both the quality and the security of code.

Summary

This chapter discussed informal techniques for writing programs that enforce security policies. The process began with a requirements analysis and continued with a threat analysis to show that the requirements countered the threats. The design process came next, and it fed back into the requirements to clarify an ambiguity. Once the high-level design was accepted, we used a stepwise refinement process to break the design down into modules and a caller. The categories of flaws in the program analysis vulnerability helped find potential implementation problems. Finally, issues of testing and distribution ensured that the program did what was required.

Research Issues

The first research issue has to do with analysis of code. How can one analyze programs to discover security flaws? This differs from the sort of analysis that is performed in the development of high-assurance systems, because the program and system are already in place. The goal is to determine what, and where, the problems are. Some researchers are developing analysis tools for specific problems such as buffer overflows and race conditions. Others are using flow analysis tools to study the program for a wide variety of vulnerabilities.

Related to this issue is the development of languages that are safer with respect to security. For example, some languages automatically create an exception if a reference is made beyond the bounds of an array. How much overhead does this add? Can the language use special-purpose hardware to minimize the impact of checking the references? What else should a language constrain, and how should it do so?

Further Reading

All too little has been written about robust programming—the art of writing programs that work correctly and handle errors gracefully. Kernighan and Plauger's book [564] describes the principles and ideas underlying good programming style. Stavely's book [965] combines formalisms with informal steps. Maguire's book [652] is much more informal, and is a collection of tips on how to write robust programs. Koenig [588] focuses on the C programming language.

Viega and McGraw's book [1014] is somewhat general, with many examples focusing on UNIX and Linux systems. Its design principles give good advice. Although they are dated, Wood and Kochan's book [1060] and Bishop's paper [107] cover principles and techniques that are still valid. Braun [145] also provides a good overview, as do Garfinkel and Spafford [382]. The latter book has a wonderful section on trust, which is must reading for anyone interested in security-related programming. Wheeler [1040] also provides valuable information and insight.

Several books discuss aspects of secure programming in a Windows environment. Howard and LeBlanc's book [493] illuminates many of the problems that programmers must be aware of. It is good reading even for those who work in non-Windows environments. Other books [182, 492] discuss security in relation to various aspects of the Windows environment.

Exercises

1:

Consider the two interpretations of a time field that specifies “1 A.M.” One interpretation says that this means exactly 1:00 A.M. and no other time. The other says that this means any time during the 1 A.M. hour.

  1. How would you express the time of exactly 1 A.M. in the second interpretation?

  2. How would you express “any time during the 1 A.M. hour” in the first interpretation?

  3. Which is more powerful? If they are equally powerful, which do you think is more psychologically acceptable? Why?

2:

Verify that the modified version of Requirement 29.2.4 shown as Requirement 29.3.1 on page 875 counters the appropriate threats.

3:

Currently, the program described in this chapter is to have setuid-to-root privileges. Someone observed that it could be equally well-implemented as a server, in which case the program would authenticate the user, connect to the server, send the command and role, and then let the server execute the command.

  1. What are the advantages of using the server approach rather than the single program approach?

  2. If the server responds only to clients on the local machine, using interprocess communication mechanisms on the local system, which approach would you use? Why?

  3. If the server were listening for commands from the network, would that change your answer to part (b)? Why or why not?

  4. If the client sent the password to the server, and the server authenticated, would your answers to any of the three previous parts change? Why or why not?

4:

The little languages presented in Section 29.3.2.3 have ambiguous semantics. For example, in the location language, does “not host1 or host2” mean “not at host1 and not at host2” or “not at host1, or at host2”?

  1. Rewrite the BNF of the location language to make the semantics reflect the second meaning (that is, the precedence of “not” is higher than that of “or”). Are the semantics unambiguous now? Why or why not?

  2. Rewrite the BNF of the time language to make the semantics reflect the second meaning (that is, the precedence of “not” is higher than that of “or”). Are the semantics unambiguous now? Why or why not?

5:

Suppose an access control record is malformed (for example, it has a syntax error). Show that the access control module would deny access.

6:

The canary for StackGuard simply detects overflow that might change the return address. This exercise asks you to extend the notion of a canary to detection of buffer overflow.

  1. Assume that the canary is placed directly after the array, and that after every array, access is checked to see if it has changed. Would this detect a buffer overflow? If so, why do you think this is not suitable for use in practice? If not, describe an attack that could change a number beyond the buffer without affecting the canary.

  2. Now suppose that the canary was placed directly after the buffer but—like the canary for StackGuard—was only checked just before a function return. How effective do you think this method would be?



[1] See Section 13.2.1, “Principle of Least Privilege,” for an explanation of how the existence of the root account violates the principle of least privilege.

[2] See Section 13.2.1, “Principle of Least Privilege.”

[3] See Chapter 22, “Malicious Logic.”

[4] This is one aspect of the principle of least common mechanism (see Section 13.2.7).

[5] This follows from Biba's low-water-mark policy (see Section 6.2.1).

[6] If the time interval during which access is allowed expires after the access control check but before the access is granted, Requirement 29.2.1 is met (as it refers to the time of request). This eliminates a possible race condition.

[9] By the principle of fail-safe defaults (see Section 13.2.2).

[10] See Section 13.2.8, “Principle of Psychological Acceptability.”

[11] See Chapter 22, “Malicious Logic.”

[12] See Section 14.4, “Groups and Roles.”

[13] See Section 14.3, “Users.”

[14] On Linux systems, and on most UNIX-like systems, this is an integer.

[15] See Section 14.3, “Users.”

[16] On Linux and most other UNIX-like systems, the epoch is midnight on January 1, 1970 (GMT).

[17] See Section 13.2.3, “Principle of Economy of Mechanism,” and Section 13.2.8, “Principle of Psychological Acceptability.”

[18] See Section 13.2.8, “Principle of Psychological Acceptability.”

[19] Note that a record with a syntax error will never grant access (see Exercise 5).

[20] See Chapter 23, “Vulnerability Analysis.”

[21] See Section 23.4, “Frameworks.”

[22] See Section 23.4.3, “The NRL Taxonomy.”

[23] See Section 23.4.4, “Aslam's Model.”

[24] See Section 23.4.2, “Protection Analysis Model.”

[25] See Section 23.4.1, “The RISOS Study.”

[26] See Section 13.2.1, “Principle of Least Privilege.”

[27] See Section 19.1.2.2, “Building Security In or Adding Security Later.” Programs implemented following this rule are not reference monitors.

[28] See Section 14.3, “Users.”

[29] See Section 13.2.1, “Principle of Least Privilege.”

[30] See Section 24.3, “Designing an Auditing System.”

[31] Section 13.2.4, “Principle of Complete Mediation,” provides detail on why this works.

[32] See Section 6.2, “Biba Integrity Model.”

[34] See Section 13.2.1, “Principle of Least Privilege.”

[35] Specifically, if the group field of the password file entry for matt is 30, and the group file lists the members of group 30 as root, the user matt is still in group 30, but a query to the group file (the standard way to determine group membership) will show that only root is a member.

[36] See Section 13.2.3, “Principle of Economy of Mechanism.”

[37] However, alternative techniques involving corrupting data, causing the flow of control to change improperly, do work. See Section 29.5.6, “Improper Validation.”

[38] Buffer overflows can also alter data. See Section 29.5.3.1, “Memory,” for an example.

[39] Other considerations contributed. See Section 29.5.4, “Improper Naming.”

[40] See Section 13.2.2, “Principle of Fail-Safe Defaults.”

[41] See Section 13.2.2, “Principle of Fail-Safe Defaults.”

[42] See Section 14.3, “Users.”

[43] If the goal is to alter data on the stack other than the return address, the canary will not be altered. This technique will not detect the change. (See Exercise 6.)

[44] See Section 12.2, “Passwords.”

[45] See Section 13.2.7, “Principle of Least Common Mechanism.”

[46] If the access control information or the authentication information came from servers, then there would be interaction with other programs (the servers). The method of communication would need to be considered, as discussed above.

[47] Section 23.3.1, “Two Security Flaws,” discusses this problem in detail.

[48] See Section 14.2, “Files and Objects.”

[49] See Section 23.3.1, “Two Security Flaws.”

[50] The system call used would be fstat(2).

[51] See the example on page 613 in Section 22.1.

[52] Specifically, the system call chroot(2) resets / to mean the named directory. All absolute path names are interpreted with respect to that directory. Only the superuser, root, may execute this system call.

[53] The principle of fail-safe defaults (see Section 13.2.2) supports this approach.

[54] According to the principle of least privilege (see Section 13.2.1).

[55] As discussed in Section 14.6.1, “Host Identity,” host names can be spoofed. For reasons discussed in the preceding chapters, the Drib management and security officers are not concerned with this threat on the Drib's internal network.

[56] If the string in y is longer than n characters, strncpy will not add a 0 byte to the characters copied into x.

[57] See Section 23.4.5.2, “fingerd Buffer Overflow.”

[58] See Section 13.2.2, “Principle of Fail-Safe Defaults.”

[59] See Section 13.2.6, “Principle of Separation of Privilege.”

[60] This is often called “atomicity.”

[61] See Section 13.2.4, “Principle of Complete Mediation.”

[62] See Chapter 19, “Building Systems with Assurance.”

[63] See Section 19.3.3.1, “Security Testing.”

[64] See Section 12.2.1, “Attacking a Password System.”

[65] See Section 13.2.2, “Principle of Fail-Safe Defaults.”

[66] See Section 13.2.8, “Principle of Psychological Acceptability.”

[67] See Section 7.3, “Originator Controlled Access Control.”

[68] See Section 13.2.5, “Principle of Open Design.”

[70] See Section 22.7.4, “Malicious Logic Altering Files.”

[71] See Section 26.4, “Availability and Network Flooding.”

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.200.14