Chapter 3: Program Security

advertisement
Chapter 3: Program Security
The theme of the third chapter seems to be very simply that low-quality software is less
secure than well written software. The author of these notes has no quarrel with that claim;
there is just too much evidence for it. The topics for this chapter fall into four categories.
1.
2.
3.
4.
Programming errors with security implications – mostly buffer overflows.
Malicious code that often targets these errors, including viruses, worms, etc.
Software engineering practices that might mitigate security issues.
Controls against program threats.
Bugs, Errors, and Security Flaws
Security faults in a computer program are only one type of error, although one can postulate
that any program error can be leveraged into a security flaw. Regrettably, there is a culture
in the development of software that all programs will contain errors and that there is nothing
to be done about it. This attitude appears excessively defeatist, although it seems to reflect
not only the actual state of affairs but also any reasonably expected state of affairs.
One of the most insidious aspects of software quality control is the “software bug”. The
term seems to have originated in one of the early (1940’s era) relay-operated computers when
a moth jammed a relay in the open position, causing the program to malfunction. The moth
was removed and taped into a research notebook with the caption “program bug”. One
wonders if the term gained widespread acceptance as a psychological crutch: “The program
did not have any bugs in it when I developed it, they must have crept in when nobody was
looking”. Yea, sure.
The major issue in the development of quality software appears to be proper software testing.
The old “penetrate and patch” or “tiger team” approaches remain valid, but need to be
supplemented by other methods. We shall return to the topic of software testing when we
discuss software engineering a bit later.
Program errors, even those representing security vulnerabilities, are normally considered to
be non-malicious in that they were not deliberately generated. We shall now consider a few
of these non-malicious errors, some due either to laziness or pressures to develop on time.
Buffer Overflows
The software error that most commonly leads to security vulnerabilities seems to be that of
buffer overflows. This occurs when a buffer is allocated to hold data and the code that
places data into the buffer does not have protections against overfilling the buffer.
The term “stack smashing” refers to a specific attack that exploits a buffer overflow. This
attack is effective against programs written in modern program languages that use the stack
structure in invoking programs and subroutines. Whenever a program or subroutine is
invoked, the run-time system builds a stack including both data and return addresses. The
goal of stack smashing is to overflow a data buffer in the stack and alter a return address.
One should ask why buffer overflows are so common. There are a number of reasons, but
the main factor seems to be an obsession with program efficiency. The code required to
insure against buffer overflows makes the array operations a bit slower. To those of us who
are dedicated to quality software, this is a bit like taking the lifeboats off the cruise liner so
that it can make better time. Perhaps the author of these notes is too cynical.
The student should be aware that many of the programming languages contain constructs that
allow buffer overflow if used carelessly. Modern programming languages, such as Ada™,
that automatically generate code to prevent buffer overflows, have not been widely accepted.
The C and C++ programming languages have a large number of features that will allow
buffer overflow. The following table is adapted from the figure on pages 152 and 153 of the
book Building Secure Software by John Viega and Gary McGraw published by AddisonWesley in 2002. The ISBN is 0201-72152-X.
Function
gets
strcpy
strcat
sprintf
scanf
sscanf
fscanf
vfscanf
vsprintf
vscanf
vsscanf
streadd
strecpy
strtrns
realpath
syslog
getopt
getpass
Severity
Most risky
Very risky
Very risky
Very risky
Very risky
Very risky
Very risky
Very risky
Very risky
Very risky
Very risky
Very risky
Very risky
Risky
Very risky
Very risky
Very risky
Very risky
Comments (from the book by Viega & McGraw)
Use fgets (buf, size, stdin). This is almost always a big problem.
Use strncpy instead.
Use strncat instead.
Use snprintf instead or use precision specifiers.
Use precision specifiers or do your own parsing.
Use precision specifiers or do your own parsing.
Use precision specifiers or do your own parsing.
Use precision specifiers or do your own parsing.
Use vsnprintf instead or use precision specifiers.
Use precision specifiers or do your own parsing.
Use precision specifiers or do your own parsing.
Make the destination at least four times the size of the source.
Make the destination at least four times the size of the source.
Check that the destination is at least as big as the source string.
Manually check the size of the input arguments.
Truncate all string inputs to a reasonable size.
Truncate all string inputs to a reasonable size.
Truncate all string inputs to a reasonable size.
The author of these notes has become paranoid about string handling in the C and C++
programming languages. These languages use “null terminated” strings rather than storing
the length of the string explicitly. Failure to terminate the string properly can lead to large
chunks of the program code being appended to the string, with possibly amusing results. The
following code fragment, taken from the book referenced above, shows preferred practice.
// Copy into locations 0 through (dst_size – 2) only.
strncpy( destination, source, (dst_size – 1) ) ;
// Force the last character to be null.
destination[dst_size – 1] = ‘\0’ ;
The author of these notes is guilty of many buffer overflows in his programming, most often
in code written in FORTRAN, a language specialized for scientific processing. Consider the
following code fragment.
SUBROUTINE BUSTIT
DIMENSION A(10)
DO 20, J = 1, 80
A(J) = 0.0
20 CONTINUE
RETURN
END
One does not need to be a FORTRAN guru to note that the array is being overfilled. The
behavior of this code depends on the fact that early FORTRAN compilers allocated memory
statically, with code and data interspersed. The effect of this error is to overwrite a big block
of executable code, which when encountered will immediately terminate the program.
Other non-malicious code problems
The book discusses two other non-malicious code problems: incomplete mediation and
TOCTTOU. Incomplete mediation seems to be a fancy name for not verifying that user input
is valid for the problem. One key rule of software engineering is never to trust user input.
TOCTTOU (or TOCTOU) stands for “time-of-check to time-of-use” and refers to using
input that has been checked but changed since the time of its being checked.
Inadequate documentation can be considered another non-malicious code problem. This
problem arises when the limitations or assumptions of the code are not documented and made
obvious to future developers. This might as well be a problem with the code itself.
Malicious Code
Malicious code probably would not exist if it were not for non-malicious code errors. This
may be a simplistic view, but it does reflect the fact that much of the existing malware (a new
term for malicious code) is designed to take advantage of non-malicious errors.
The two main types of malware are viruses and worms. A virus is a fragment of code that
attaches itself to a running program, “infects” that program, and uses that program to “infect”
other programs. Viruses used to propagate via swapping disk drives, but now propagate
mostly via the internet. A worm is stand-alone program that propagates itself without the
help of another program. Worms generally propagate through the internet.
As an aside, we mention that the safety precautions once suggested to prevent spread of
computer viruses by swapping disks sounded very much like the suggestions to avoid spread
of sexually transmitted disease in humans, leading to the slogan “safe hex” – where the
reference is to “hexadecimal”.
There are other classifications of malicious code, including Trojan horses, logic bombs,
trapdoors, and rabbits. A Trojan Horse is a program that contains unexpected malicious
code that possibly operates in a stealthy mode. Many Trojan horses are spread by worms, but
this is not necessary. One of the earliest Trojan horses was described in a paper by Ken
Thompson in 1983, where he described a way to create a compiler that would infect the
system login script with malicious code to allow unauthorized logins.
A logic bomb is a program that is triggered when some event occurs. A time bomb is a
logic bomb that is triggered by a specific date and time. One of the more famous time bombs
was detected before it could be triggered on the anniversary of the creation of the state of
Israel. A trapdoor is an undocumented feature in a program or operating system that allows
certain persons (usually the system developers) to gain access independently of the normal
security system. These trapdoors are often inserted for maintenance or diagnostic purposes,
but can be discovered by malicious hackers and misused in any number of ways.
The term “rabbit” seems to have been coined in 1989, but is not much used. This malware,
which operates by replicating with the goal of exhausting some computer resource, has also
been called a “bacterium”, another term not much used any more.
More On Viruses
The textbook presents a good discussion of viruses, focusing on a number of important
topics. One of the more important topics is that of virus signatures and virus scanners.
Many computer viruses have sequences of bytes that can be used to identify them. It is these
sequences that are the targets for the virus scanners. Of course, a virus scanner can detect
only those viruses for which it has patterns – so keep your scanner up to date.
Polymorphic viruses are an attempt to deceive virus scanners by presenting the same virus
with a different signature. Most scanners will be updated as soon as a new variant of an
existing virus is detected, so again – keep your scanner up to date.
There is a new class of anti-virus software that the author of these notes has seen advertised,
this is a behavior-based scanner. Rather than having a list of virus signatures, the scanner
has a list of dubious behaviors, such as writing to the boot sector of a hard disk or attempting
to change certain system files. The user is allowed to define a list of “dubious actions”, each
of which will be trapped and not allowed without an additional user input. This class of antivirus software appears promising, but should be used with the pattern-based scanners as a
part of a defense-in-depth strategy.
Cryptographic hash sums are another tool for detecting virus infections on a disk. These
sums are the output of cryptographic hash functions that we have already seen in Chapter 2
of the textbook. For those who ask, a cryptographic hash function is just a hash function that
is of high-enough quality not to be broken easily by standard cryptanalysis techniques. One
identifies a file or set of files, performs a hash sum of these files, stores that hash sum in a
secure place (perhaps off-line), and periodically checks the hash of the files to see if
something has changed. In this case, as in many others, a change does not automatically
mean that something bad has happened, just that the change needs to be explained.
The idea of defense in depth is that one has multiple layers of protection against malicious
attacks, so that if one layer is breached there is a chance that the attack may still be stopped.
One obvious part of such a strategy is that each breach be detected and remedied as soon as
possible, so that one is not depending on one strong link in a weak chain for protection. The
idea of defense in depth comes from military strategy, where one does not want a single
penetration by the enemy to lead to a complete rout.
Defense in depth is best illustrated by cases in which it was not applied. One famous case
comes from the battle of New York, which began on August 27, 1776. General Washington
had arrayed his troops in a well-fortified line and was awaiting an attack by the British, who
seemed content to make a big display but not to advance. Unknown to the Americans, the
British had sent ten thousand men and fourteen fieldpieces on a circuitous march along the
Jamaica road that lead behind the American lines. Apparently, Washington thought that the
road would be patrolled, but nobody actually implemented that plan. While the British were
keeping his attention to the front of his lines, they were attacking the rear in a “sandwich
attack”, rarely attempted because few commanders were foolish enough to allow it to
succeed. Washington barely escaped with his army; New York City fell to the British on
September 16, 1776 and was occupied for the duration of the war.
More on Worms
The textbook then discusses two of the more famous worms from the recent past: the Morris
worm of November 2, 1988 and the Code Red worm from 2001. Each seems to have been
written just to propagate itself and not to attack any systems directly. Should the worm have
carried a malicious “payload” (code to attack an infected system), either could have done
much greater harm. As it was, each achieved its effect by spreading rapidly and consuming
internet resources. The most embarrassing fact related to the Code Red worm is that it
exploited a widely-known vulnerability for which a patch had been available for some time.
Trapdoors
The term “trapdoor” has several uses in information assurance. Within the context of
cryptography, it may refer to a trapdoor function – one that is invertible and easily
computer but for which the inverse function is very hard to compute. Within the context of
this discussion, a trapdoor is hidden (and often undocumented) code that can be invoked by
specific calls that also are not documented. Trapdoors in this context are often remnants of
the software testing process, in that they were used to allow the developers to remove errors.
These remnants are usually left in the code by mistake.
Two of the more famous trapdoors are the VAX/VMS special accounts and the Unix
sendmail trapdoor. A VAX computer, freshly delivered from DEC (the Digital Equipment
Corporation – now defunct) was configured with the VMS operating system having three
privileged accounts – SYSADMIN, FIELD, and one other that I forget. Each of these
accounts had “superuser” privileges (DEC did not actually use that term) in that a process
running on the account could access and modify any of the computer’s resources, including
all of the files. The fixed password for the FIELD account was “SERVICE”. Each of the
other two accounts also had pre-defined passwords. The expectation was that each of these
passwords would be changed very early in the system generation process. In fact the
SYSADMIN password was often changed but the other two accounts were ignored. Thus, a
clever malicious hacker had two well-known trapdoors into the system. That which
facilitates system maintenance also facilitates hacking.
The author of these notes is still doing some research on the Sendmail trapdoor. It is
obvious that the trapdoor was a feature added by the developers with the goal of facilitating
the debugging of the sendmail code. The sendmail facility was created in order to allow
remote users to send and receive e-mail. The trapdoor refers to the fact that a remote user
could place sendmail in a debug mode (some documents refer to this as “wizard mode” in
distinction to the usual debug mode) and gain root access to the system running the program.
While this greatly facilitated correcting any program errors, it certainly opened the door wide
for malicious hackers. Recall that a person or process running with root access on a UNIX
system can access and modify any system resource.
The Salami Attack
The book discusses this attack method in the context of banking, where it is most often seen.
Steal a penny from enough people and you may have accumulated a lot of money.
Covert Channels
The textbook presents a covert channel in terms of transmitting data at a small, but
significant, rate. To make the covert channel threat appear a bit more real, I present the
following scenario in which a two-bit message can be worth millions of dollars.
Suppose I am a staffer in the FDA (U.S. Food & Drug Administration) who is involved in
examining a cancer drug for possible approval. Just for fun, let’s imagine the company
developing the drug is called “ImClone” (now where did I get that name?). Suppose that I
have a friend, called “Martha” (the name is fictitious, picked purely at random – I assure
you), who is heavily invested in ImClone stock and who would certainly appreciate a tip
from me before the approval or lack of approval is announced publicly.
Martha’s investment choices might be based on prior knowledge of the announcement. The
stock has already gained significant value based on the preliminary announcement of the
cancer drug and the fact that it has been submitted for FDA approval. Should the drug be
approved, the stock value will go up; in that case Martha would want to buy more stock.
Should the drug not be approved, the stock value will decrease significantly; in that case
Martha would want to sell the stock at the current high price and possibly repurchase the
stock after its value had fallen. My passing such confidential information to Martha before it
is made public, and her acting on it, is an illegal practice called “insider trading”.
Here is the covert channel for communicating the decision. I create two text files, call them
zonk,txt and bonk.txt (good luck on finding any significance in those names) and apply file
locking with the following agreed interpretation. The files are assumed to contain textual
information that is completely innocuous and not likely to arouse suspicion.
zonk.txt locked?
No
No
Yes
Yes
bonk.txt locked?
No
Yes
No
Yes
Interpretation
The FDA has not yet decided
on the approval of the drug.
The FDA will not approve the drug.
The FDA will approve the drug.
Now my friend Martha can agree to check these files often and get enough warning to profit
illegally based on inside information. We assume that Martha is otherwise squeaky clean
regarding this transaction, so that the proof of insider information would be quite difficult.
The textbook discusses other ways of covert transmission of information. Some, such as
timing channels, might be more of a theoretical worry as random variations in system
timings may make such channels unreliable. The bottom line is that covert channels are very
hard to detect and almost impossible to prevent.
One might say that the best deterrent to covert channels is happy employees. We shall
discuss organizational security policies a bit later in the course. The bottom line here is
quite simple: everybody having access to the software must want it to be secure.
Software Engineering
The discipline of software engineering arose from the experience that people at IBM had in
the development of the OS/360 operating system for the new 360 line of computers. Some of
the lessons learned in that project were reported in the book The Mythical Man Month:
Essays on Software Engineering, by Frederick P. Brooks. The book was first published in
1975 and reprinted in 1995 by Addison-Wesley (ISBN 0-201-835959).
There have been a number of approaches to software engineering since its origin as what
probably would be called Structured Analysis / Structured Design (SA/SD) today. Other
approaches include data-flow analysis and object-oriented analysis and design. In this
author’s opinion, each approach to software engineering brings something to the design
process. The book discusses a few topics of importance.
The first design topic has negative overtones; it might be called the commercial imperative.
Most company managers want software delivered on time and under budget; these often have
little experience in the development of software. This commercial imperative often leads to
pressures to deliver a product despite a number of obvious problems. In this context, quality
software engineering may be seen as a hindrance that is barely to be endured. No company
will officially admit such practices, but the author of these notes has been there.
The lessons of structured analysis and design that remain valid today focus on modular
development of software. One idea is that there is a maximum size for each module, be it
subroutine or function. It used to be decreed that no module have length greater than 65
lines. This gave rise to numerous discussions – was this total lines or lines of code, and what
defined a line of code in a language (such as C++) allowing for multi-line expressions. After
some legalistic wrangling, it was decided to allow the maximum number to be a bit vague.
In this author’s opinion, 500 lines of code in a single module is probably excessive and 25 to
50 lines may be a bit small.
Two other ideas from SA/SD were functional cohesion and module coupling. Functional
cohesion measured the “focus” of a module – how clearly defined its goal was. Thus a
function to calculate the sine of an angle (and nothing else) would rate as high cohesion,
while a module containing the sine, cosine, and tangent functions would rate only a bit lower
in cohesion, but still acceptably high. Add a square root function to that module and the
cohesion becomes questionable, then add a sort routine and the cohesion would reach the
lowest value of “coincidental” – just a bunch of code with no specific purpose.
Modular coupling is a measure of how one module depends on the implementation of
another module. The desired goal is loose coupling, in which modules interact only by
argument lists. Modules that share external storage are slightly more coupled (less desirable,
though this might be acceptable), while module that depend on the internal data structures of
other modules are unacceptably coupled. The goal here is to be able to change the
implementation of any module while preserving its interface to the rest of the system. This
goal has been adopted by object-oriented design methods, where it is called encapsulation, a
term that probably was not used in SA/SD.
One of the more interesting approaches to software engineering is called the spiral model,
although there are quite a few variants of this one. In order to understand this model, one
should understand the overly-optimistic waterfall model, in which software development
progresses in a number of well-defined steps.
1)
2)
3)
4)
5)
Requirements analysis and definition of system tests
System analysis
System design
System coding
System testing
The problem with this approach is that there are very few systems for which the requirements
are well enough understood in advance for the approach to be applied. Quite often, the
process of design and coding of a system will bring new system requirements to light. The
spiral model allows for this in that the steps of the waterfall model are undertaken numerous
times until a stable set of requirements is agreed upon.
Software testing is definitely an art in that no completely systematic approach may be
defined. There are a number of standard approaches that should be followed, but the student
should be aware that these are merely necessary and not sufficient. The first requirement
(and one of the least followed) is that a complete system test be specified at the same time as
the requirements. This suite of tests should be refined as the system requirements change and
be developed and available at the same time as first code on the system itself is delivered.
Too often it is the case that the tests are written after the system code is delivered, and quite
often they are carefully designed so that the system will pass – not a responsible idea.
Who should do the testing? It is obvious that each developer should do some testing,
focusing on errors that are easy to identify. It is at this state of development that most buffer
overflow errors should be identified and eliminated, for example. At the same time, there
must be an independent testing organization as a part of any group designing software of
even moderate complexity. This author worked for a company that developed software to be
used to manage hotel and motel properties. The development team included an independent
testing team, all members of which were drawn from the lodging industry. None of the
members of this testing team had experience in software development, but each knew what
the software had to do. This team developed a very good product, although a bit late.
An important aside here is that system security requirements are an essential part of the
system requirements. The system security must be completely understood before any code is
written or it is likely to be relegated to a side project, and security holes will abound.
Peer reviews (structured walk-throughs) are other tools that are rarely used. In these, a
developer presents his or her software design to the group, under the understanding that the
topic for review is what the module does and how well it adheres to the specification. These
reviews are supposed to be “egoless” in that developers not take it personally when the
software is criticized – fat chance of that.
Configuration Management and Associated Testing
One of the major concerns in the development of major software projects will come as a
complete surprise to a programmer who has worked alone during college years. This is the
management of changes made by a number of people.
One of the better tools is Visual Source-Safe, marketed by Microsoft. This is a repository of
documents (code and text documents) that allows check-out by individual developers. This
enforced serialization of software changes should prevent proliferation of multiple copies of
the software. This author has been a part of two teams using Visual Source Safe; it is only a
minor nuisance and it does seem to do the job.
Two important types of tests are regression testing and what I call targeted testing.
Regression testing is a by-product of software testing and occurs in the following context.
When a software error is discovered in the normal process of testing, it is fixed and should
never occur again. To insure that it never occurs again, a test specific to the fault just
discovered is created and entered into the test suite. The modified software is run through
the modified test suite to insure that the error has indeed been fixed and that no previous
errors reappear due to problems in writing software patches.
Note that regression testing targets only those errors that have been discovered. The
targeted test is based on a developer’s understanding of what might go wrong in software
and devising tests based only on intuition. The author of these notes, being tasked with
writing tests for a software system, made just an assumption and created a valid data input
that caused the system to crash. This error was acknowledged with some grumbling and
fixed. Never think that being a proficient tester will make you popular.
We end our notes on this chapter with a big dose of cynicism. Proofs of program
correctness are impossible and most often a waste of time. Certain errors, such as buffer
overflow and incorrect input, can and should be anticipated. The program should be
considered logically, apart from “debugging”, and visually scanned in an attempt to
demonstrate correctness. If you have the courage, have another developer read your code.
This can be an enlightening and often humorous experience.
Final rule – do not give in to the pessimism that it is impossible to create error-free software.
While it probably is impossible, we should keep at it.
Download