GAURIAR, PRACHI. Automating and Simplifying Memory Corruption Attack Response
Using Failure-Aware Computing. (Under the direction of Assistant Professor Jun Xu).
Over the last two decades, advances in software engineering have produced new
ways of creating robust, reliable software. Unfortunately, the dream of bug-free software
still eludes us. When bugs are discovered in deployed software, software failures and service
disruption can lead to significant losses, both monetary and otherwise. The typical failure
response process is composed of three phases: failure detection, cause analysis, and solution
formulation. To minimize the impact of software failures, it is critical that each of these
phases be completed as quickly as possible.
This thesis is separated into two parts. In the first part, we propose a general
conceptual approach called failure-aware computing that aims to automate as much of the failure response process as possible. We describe the architecture of this proposed
framework, some possible applications, and challenges if it were implemented. We then
describe how this framework can be applied to responding to memory corruption errors. In
the second part, we describe and evaluate an implementation of part of this framework for
diagnosing memory corruption failures. In particular, we discuss a root cause analysis tool
we have created that analyzes a program’s source code to determine which memory-related
program events potentially lead to a memory corruption error. Our tool then monitors
the afflicted program’s execution and outputs useful information to aid the developer in
understanding the root cause of the failure. We finally evaluate our tool’s effectiveness in
identifying the root cause of memory access errors in both self-written and open-source
by
Prachi Gauriar
A thesis submitted to the Graduate Faculty of North Carolina State University
in partial fulfillment of the requirements for the Degree of
Master of Science
Computer Science
Raleigh
2006
Approved By:
Dr. Frank Mueller Dr. Laurie Williams
Dr. Jun Xu
Biography
Prachi Gauriar is from Texarkana, Arkansas. After attending high school at the Arkansas
School for Mathematics and Sciences in Hot Springs, AR, he went to the University of
Arkansas to study Computer Engineering in 2000. After graduating summa cum laude with a Bachelor of Science degree in 2004, he enrolled in the graduate program at North
Carolina State University. There, he performed research in the field of system security
under the supervision of Dr. Jun Xu. He will graduate from NC State with a Master of
Acknowledgements
I would like to thank the members of the NCSU System Security research group, particularly
Chongkyung Kil and Emre (John) Sezer, for their support while writing this thesis. Without
their opinions, ideas, and support, this thesis would not be of the quality it is today. John in
particular provided invaluable help during the development of the data dependence analysis
component of our root cause analysis tool.
I would also like to thank my advisor, Dr. Jun Xu, who inspired what would
eventually become failure-aware computing. His advice, suggestions, and encouragement
helped make my entire graduate school experience more useful and enjoyable, and his impact
on this thesis is immeasurable.
Without the feedback of my committee members, Dr. Frank Mueller and Dr.
Laurie Williams, the presentation and organization of this thesis would be far less clear. In
addition, Dr. Williams’ feedback helped make the evaluation of our root cause analysis tool
much stronger.
I owe a deep amount of gratitude to my Flying Saucer mates, Travis Breaux,
Paul Breimyer, James Niehaus, and Matt Schmidt, with whom I shared many pints and
silly conversations. Our visits to the pub served as a welcome distraction that kept me
motivated.
Finally, I would like to thank all my friends and family, both near and far, for
their patience and support. Without their love, kind words, and encouragement, I would
Contents
List of Figures vi
List of Tables vii
1 Introduction 1
2 Literature Review 4
2.1 Failure Response . . . 4
2.1.1 Capture/Replay Tools . . . 6
2.2 Memory Corruption Attacks . . . 6
2.2.1 Static Analyzers . . . 7
2.2.2 Runtime-Based Detection Technologies . . . 8
2.2.3 Combined Static Analysis and Runtime Checking Tools . . . 9
2.2.4 Randomization . . . 10
3 Failure-Aware Computing - A Proposal 12 3.1 Failure-Aware Computing Framework . . . 13
3.2 Application to Memory Corruption Attacks . . . 17
3.2.1 Attack Detection . . . 17
3.2.2 Vulnerability Signature Generation . . . 19
3.2.3 Root Cause Analysis . . . 21
4 Root Cause Analysis Tool 24 4.1 Conceptual Overview . . . 24
4.2 Implementation Details . . . 28
4.2.1 Data Dependence Analyzer . . . 28
4.2.2 Runtime Monitor . . . 31
4.3 Evaluation . . . 33
4.3.1 Off-By-One . . . 34
4.3.2 strncpyError . . . 36
4.3.3 Clone Bitmap . . . 37
4.3.5 Null HTTPd . . . 41 4.3.6 ghttpd . . . 44
5 Conclusion 47
5.1 Failure-Aware Computing Limitations . . . 48 5.2 Failure-Aware Computing Future Work . . . 49 5.3 Root Cause Analysis Tool Future Work . . . 49
List of Figures
3.1 Failure-aware computing application runtime environment . . . 15
3.2 Failure-aware computing component interaction . . . 16
3.3 Abbreviated Python source for our memory access error detector . . . 20
3.4 Sample of error types detected by CCured . . . 21
3.5 Example demonstrating the separation of an error’s origin and manifestation 23 4.1 Code example to illustrate data dependence analysis . . . 26
4.2 Figure 4.1’s data dependency graph . . . 26
4.3 Root cause analysis tool component interaction . . . 28
4.4 Recursive algorithm used in our data dependence analyzer . . . 29
4.5 Analyzer output for Figure 4.1 . . . 30
4.6 Monitor output for Figure 4.1 . . . 33
4.7 Faulty code in Off-By-One test case . . . 35
4.8 Tool output for Off-By-One test case . . . 35
4.9 Faulty code instrncpy Error test case . . . 36
4.10 Tool output forstrncpy Error test case . . . 37
4.11 Faulty code in Clone Bitmap test case . . . 38
4.12 Tool output for Clone Bitmap test case . . . 39
4.13 Faulty code in PGCFoundation . . . 40
4.14 Tool output for PGCFoundation . . . 41
4.15 Faulty code in Null HTTPd . . . 42
4.16 Tool output for Null HTTPd . . . 43
4.17 Faulty code in ghttpd . . . 44
List of Tables
Chapter 1
Introduction
The last several decades have been witness to vast changes in the way that software
is written. Software engineering research and system building experience have brought
us new ways of designing, writing, testing, and maintaining complex software systems.
Unfortunately, these methods have not been able to keep up with the growing complexity of
software. Bugs still manage to find their way into deployed software, causing semantically
incorrect behavior, degraded performance, or service interruption. When these bugs are
discovered, their effects can be costly. This is especially true when the bugs occur on servers
that are important to an organization’s infrastructure. In such cases, software failures have
the potential to severely limit productivity, which can in turn lead to monetary losses.
The failure response process typically includes three discrete phases: detection,
cause analysis, and solution formulation. Before one may respond to a failure, its existence
must obviously be detected. For overtly observable errors like crashes, detection is simple
and can be performed automatically. However, in more subtle cases like the slow degradation
of performance over time, discovery usually requires that a human notices the error. In
either case, to minimize the error’s effect on service and resource availability, the afflicted
software must usually be restarted.
Once a failure is detected, formulating a solution is less difficult if one can
de-termine the sequence of events that triggered the error. This is the purpose of the cause
analysis phase. In this phase, incident reports, log files, stack traces, and core dumps are
Unfortu-nately, the quality of the data collected in this phase often makes it difficult to create a
reliable series of steps that reproduces the error. Incident reports and log files may include
too little reliable, relevant detail, and if stack traces and core dumps are even available,
they may not be useful for complex errors that are only manifested after a series of failures
occur.
After the cause analysis phase is complete and a failure-inducing sequence of events
has been produced, the final step in the failure response process is to generate a remedy that
prevents the failure from reoccurring. While it may be ideal to solve the problem by applying
a software patch to the afflicted application, such patches take time to develop, which in the
meantime leaves customers vulnerable to repeated occurrences of the failure. As such, it is
often beneficial to develop a temporary solution that filters out error-inducing input until
a more permanent solution can be deployed. In this case, input can be analyzed for
failure-inducing characteristics that can be used to produce an input filter, as in [4] and [14]. These
filters discard input that possess some or all of the aforementioned characteristics, preventing
failure-inducing input from reaching the application. For software patches, a developer may
manually attempt to isolate and fix the bug using standard debugging techniques or by
employing one of the many static analysis and runtime monitoring tools that simplify this
process. For both temporary and permanent failure remedies, the ability to reliably replay
the failure using the failure-inducing event sequence from the previous step is crucial.
It is critical to perform each phase of the failure response process as quickly and
accurately as possible. Slow failure detection can lead to prolonged service disruption.
If one can not expeditiously determine what triggered the error, fixing it is much more
difficult. Finally, and perhaps most obviously, if one can not quickly produce a solution to
the problem, customers are left vulnerable to repeated failures, and vendor reputations can
subsequently suffer.
The need for a swiftly-executed failure response is especially pronounced when the
software failures are memory corruption errors in C programs. Memory corruption errors
occur when a programmer dereferences a pointer whose associated memory object differs
from its referent object, i.e., the object the programmer intended to access. These errors
may be exploited by attackers to gain control of a system, view or modify private data,
or disrupt service. While many tools exist to help diagnose or prevent these errors from
being exploited, they typically provide incomplete protection[7, 5, 11, 25] or incur too much
vulnerabilities to exist in deployed software for some time to come. Thus, there appears to
be a need for a system that can help software users and vendors react quickly when such
errors are exploited.
This thesis consists of two parts. In the first part, we describe a general conceptual
approach called failure-aware computing that aims to address and simplify each aspect of the failure response effort. This approach facilitates the automation of much of the failure
response process by providing mechanisms to automatically detect failures as they occur; log
all external input without requiring source code changes; and aid in the quick formulation
of both temporary and permanent failure remedies. By automating or simplifying each of
these tasks, we can reduce the high cost typically associated with discovering, reproducing,
analyzing, and fixing a failure. We describe the general architecture of this approach, some
of its potential applications, and some challenges were it to be implemented. We also propose
a system that applies this approach to the problem of responding to memory corruption
attacks against C programs. This system uses existing technologies to help detect errors at
runtime and find where the error was manifested during analysis.
In the second part of this thesis, we implement part of the failure-aware computing
framework for diagnosing memory corruption errors. In particular, we describe the design,
implementation, and evaluation of a root cause analysis tool that can be used to help find
the root cause of an error, rather than just the location where the bug was manifested. It
does so by performing source code analysis to identify source code statements that may
have had an effect on the memory object that was accessed incorrectly. It then monitors
the afflicted application’s execution to provide the developer with information that may
illuminate why the bug is occurring, thus facilitating faster remedy formulation.
The rest of this thesis is organized as follows. Chapter 2 describes current work
in detecting and preventing memory corruption attacks and recording/replaying program
executions. Chapter 3 describes the architecture and benefits of failure-aware computing in
detail. In this same chapter, we propose a system that applies our conceptual framework to
the task of responding to memory corruption attacks and describe some of the challenges
if this system were to be implemented. In Chapter 4, we describe the conceptual model,
implementation, and evaluation of the aforementioned root cause analysis tool. Finally, we
Chapter 2
Literature Review
2.1
Failure Response
Current approaches in failure response focus on tolerating software failures so that
the afflicted application can continue to execute. In [21], Sidiroglou et al. describe a
reactive approach that uses software probes to detect when a software failure occurs and
isolate the code regions involved in the failure. These regions are then instrumented to
execute in an emulator, which records all memory changes and pre-determines the
side-effects of emulated instructions. The software is then automatically re-executed in a testing
environment. During this test, if the emulator detects that an instruction will cause an
error, it restores all memory modifications that were performed during emulation and forces
the currently executing function to return an error value. If the instrumented application
crashes upon this forced return, the application is re-instrumented to also emulate the
current function’s calling function. This process continues until the application no longer
crashes on a forced return. Once this occurs, the production version of the application is
updated to automatically replace the vulnerable call sequence with the “safe” control-flow
when given the same input. This approach focuses on transaction-based input, but the
authors do not describe how inputs are recorded or whether the original application must
be modified for input gathering. Overhead of emulation resulted in executions that were
Rx[15] is a tool that aims to recover from software failures by treating them as
allergies. Rx checkpoints the target application to record a snapshot of the application. It
uses error detectors during runtime to determine when an error occurs and rolls back the
application to the last checkpoint. Based on the type of error that occurred, Rx then makes
minor changes to the application’s runtime environment in an effort to prevent the bug from
reoccurring. These changes include zeroing out newly allocated memory regions, delaying
reuse of freed memory regions, and allocating memory in a different location. Once these
changes are made, failure-inducing input is replayed. If the applied environmental
modifi-cations prevent reoccurrence of the failure, the modifimodifi-cations are removed, and execution
continues as before. Otherwise, additional environmental changes are made, or execution
is rolled back to an older checkpoint. Rx focuses on preventing failures in client-server
applications, which simplifies its input-logging requirements. Rx uses a proxy application
that logs incoming messages for replay and buffers outgoing messages to prevent resending
messages to clients. While Rx requires much less time to recover from errors than
restart-ing afflicted applications, its focus is on runtime recovery. As such, it does not provide any
analysis mechanisms, although it can output additional information about environmental
changes, which can be useful for post-mortem analysis.
In [16], Rinard et al. describe an approach called failure-oblivious computing,
that prevents memory errors from affecting server execution. Their approach involves the
use of a “safe” C compiler, which instruments application code to detect memory access
errors. When an access error is detected, their approach does not generate an exception
or abort execution. Instead, it simply generates values to return to invalid read operations
and discards invalid writes. While this approach avoids memory corruption, it can also
lead to incorrect execution via unexpected execution paths. To reduce the risk of such
executions, the authors concentrate on applications with short error propagation distances
and assume that incorrect executions will be identified quickly. Due to the additional
runtime checks inserted by failure-oblivious computing, applications experienced runtime
slowdowns ranging from 1.03-8.1 times normal execution speed. Additionally, while
failure-oblivious applications are resilient against errors, failure-failure-oblivious computing does not aid
2.1.1 Capture/Replay Tools
A number of tools exist to capture information about program execution that can
later be used to replay the execution for analysis and testing. The scope and implementation
techniques for these tools varies greatly. Some tools work transparently for all executables
while others only work for programs that use particular languages or API. Some are used
for debugging, while others are used for testing.
Jockey[18] is a tool for transparently recording and replaying programs on Linux.
It is designed for use with debugging, and aims to be easy-to-use and generic enough to
handle any Linux program. Jockey uses a shared library that is loaded into the target
application upon application launch. When the library is initialized, it replaces system
calls and CPU instructions that have timing- and context-dependent effects with its own
functions calls. These function calls include instrumentation to record data in the capture
phase and emit it during the replay phase. In general, Jockey performed well, with recording
overhead ranging from less than 5% to 15% .
jRapture[23] is a tool for capturing and replaying Java programs. It is intended
for use as a means to unobtrusively gather program executions during beta testing. These
executions can then be replayed and analyzed to determine which executions require a
manual review. jRapture is implemented by providing modified versions of standard Java
classes that directly interact with the underlying OS, such as Socket. This modified class
includes modified methods that record their effects on the application. These effects include
values returned and modifications to parameters and class fields. jRapture records these
values by recording serialized versions of objects during capture and restoring these serialized
version during replay. This serialization can lead to a large amount of data being written
to disk. Performance testing yielded program overheads between 3 and 10 times that of
normal execution.
2.2
Memory Corruption Attacks
Memory corruption attacks, which include buffer overflows, integer overflows,
for-mat string attacks, and double frees, have been a key research area in the field of system
security for the past several years. An inspection of vulnerabilities reported to
dominant form of attack[24]. These attacks are almost always aimed at C programs, since
the C programming languages lacks memory- and type-safety guarantees.
As a result, a considerable number of tools and technologies have been developed
by the research community to mitigate these attacks. A large variety of approaches exist,
including analyzing code to find potential vulnerabilities, providing enhanced memory
pro-tection, modifying the executability of certain memory regions, or augmenting C to make it
safer. These tools can be placed into three broad categories: static analysis tools,
runtime-based error detection technologies, and combined static analyzers and runtime checkers.
2.2.1 Static Analyzers
Static analyzers perform analysis on a program’s source code to identify where
potential security errors may occur. The key benefit to using such analyzers is that potential
errors can be identified before an application is released. These tools are typically quite successful in identifying the location of potential errors, but usually produce a large number
of false positives as well. In addition, many tools impose constraints on source code to
guarantee the soundness of their analysis; these constraints are oftentimes unrealistic or
impractical for use in real-world software.
In [26], Wagner et al. first proposed the idea of using static analysis techniques to
identify buffer overflows in C programs. They developed a tool called BOON that models
each string buffer in a C program as a pair of integer ranges that describe the possible
number of bytes allocated and used. Assignments and string operations, such as calls to
strcpy or strcat, modify a buffer’s ranges to reflect the changes that those operations
may effect. After generating constraints for each statement in the program source, BOON
then ensures that no a buffer’s allocated size is less than the number of bytes it uses.
When analysis indicates that this condition may not be met, it warns of the possibility of
a buffer overflow vulnerability. Unfortunately, due to BOON’s use of context- and
flow-insensitive analysis and its inattention to pointer arithmetic, function pointers,
doubly-indirected pointers, and pointer aliasing, its analysis is imprecise and can lead to both false
negatives and false positives. In fact, when analyzingsendmail8.9.3, only four of forty-four
warnings emitted by BOON were real errors.
Rather than generate constraints automatically, Larochelle and Evans developed
orannotations, embedded in code to determine if a program could potentially misbehave. These annotations describe a function’s pre- and postconditions, side-effects, and other
behavior. Using a flow-sensitive, intraprocedural analysis, SPLINT determines the range of
safely readable and writable indices for each buffer in a function. At function call sites, a
function’s preconditions are checked, and its postconditions are assumed to be true after the
function’s return. Whenever SPLINT detects that a precondition or postcondition may be
false or that a buffer may be read or written to unsafely, it warns of a potential vulnerability.
Because of SPLINT’s reliance on programmers to provide source code annotations, its
analysis may be unsound. Additionally, some programmers may consider the effort required
to annotate program source too high to justify SPLINT’s use.
2.2.2 Runtime-Based Detection Technologies
Runtime-based error prevention technologies modify an application’s runtime
envi-ronment in an attempt to prevent errors from occurring. While they generally run efficiently,
runtime-based prevention technologies are usually targeted at very specific types of errors,
and thus have limited utility. While they eliminate the possibility of an error actually being
exploited, they do not usually identify where the errors occur in source code, so the program
still contains an error. However, they can often be used with existing applications without
modification.
StackGuard[7] is a runtime-based technology that aims to eliminate the thread
of stack smashing buffer overflows[1]. To corrupt a function’s return address in a stack
smashing attack, attackers must overwrite all data between the target buffer and the return
address. Therefore, any value between a function’s local variables and its return address are
necessarily overwritten. StackGuard defeats these attacks by placing a random “canary”
value next to the function return address after a function is called. Immediately before
the function returns, StackGuard ensures that the canary value has not changed; if it has,
execution is aborted.
FormatGuard[5] prevents format string attacks[19] from being successful by
ensur-ing that the number of % modifiers in aprintf-like function’s format string is no more than
the number of arguments actually passed to the function. It does this by replacing calls
toprintf-like functions with a macro that firsts counts the number of function arguments
format string, counts the number of % modifiers it contains, and checks that the number
of arguments is at least as large as this value. If not, FormatGuard logs the erroneous call
and aborts execution. While FormatGuard provides thorough coverage of format string
vulnerabilities, there is one significant gap: for programs that dynamically construct vararg
stacks and directly call v*printf, FormatGuard can provide no protection. This can be a
problem in libraries that provideprintf-like functions for date formatting or string
manip-ulation. Because the argument lists are dynamically constructed, FormatGuard simply can
not count the arguments. Additionally, FormatGuard may require some changes to source
code because it replaces function calls with macros.
In [6], Cowan et al. describe a tool called PointGuard that protects against all
memory corruption attacks that target pointers. PointGuard works by encrypting pointer
values while in memory and decrypting them only when they are loaded into registers.
While this approach works well in avoiding many kinds of memory corruption attacks, it
still leaves program vulnerable to non-control-data attacks[2].
2.2.3 Combined Static Analysis and Runtime Checking Tools
Hybrid static analysis and runtime checking tools attempt to combine the benefits
of both static analysis and runtime checking to eliminate whole classes of software errors
with minimal false positives. In general, these tools use static analysis when possible, but
use runtime checking when their static analysis is incapable of verifying the existence of
errors. Hybrid tools work very well, though they often incur significant runtime overhead
that precludes their use in deployed software.
CCured[13, 3] is a program transformation system that adds type- and
memory-safety to C programs. At the heart of CCured is a pointer type-inference system that
statically determines how safely a given pointer is used. After classifying a pointer into
one of several safety classes, it then instruments dereferences of those pointers with runtime
checks to avoid errors, such asNULLpointer dereferencing and out-of-bounds buffer accesses.
CCured additionally provides type-safe vararg functionality and optional garbage collection
to avoid format string attacks and temporal memory errors, respectively. CCured requires
that programmers make some changes to source code to facilitate analysis and maintain
compatibility with external library functions. These changes typically require that
multiword pointer representation to C’s conventional pointer representation. While CCured
provides full type- and memory-safety, it also incurs an average performance overhead of
31% and memory overhead ranging from 1-161%.
Cyclone[9] is a dialect of C that restricts and extends C’s semantics to ensure
that source code is memory safe. Like CCured, it augments C with features that prevent
dangling pointers, unsafe pointer arithmetic, out-of-bounds buffer access, and format string
vulnerabilities, among others. Cyclone adds a number of pointer types that restrict the way
a pointer may be used. Based on a pointer’s type, appropriate runtime checks are added
to the program to ensure that memory-safety is preserved. Due to Cyclone’s additional
restrictions, porting software to Cyclone is nontrivial. Unlike CCured, Cyclone requires
that programmers manually specify a pointer’s type. Additionally, Cyclone disallows the
use of certain constructs likesetjmpandlongjmp, which can make porting difficult. Finally,
developer tools like debuggers and parser generators must be modified to be compatible with
Cyclone.
In [17], Ruwase and Lam present a buffer overrun detector called CRED (C Range
Error Detector). CRED, which is based on Jones and Kelly’s bounds checker[10]. CRED
uses a referent object based approach to add bounds checking for string buffers without modifying the representation of a pointer. It tracks memory allocations for stack, heap,
and static memory objects and inserts bounds checks at pointer dereferences. Unlike Jones
and Kelly’s bounds checker, CRED allows for the use of out-of-bounds pointer arithmetic,
in which an out-of-bounds memory address may be used to compute an in-bounds address
that can later be dereferenced. For performance reasons, CRED only bounds checks string
buffers. While this yields relatively good performance for programs without much string
processing, string-intensive software can suffer from overhead between 60-130%.
2.2.4 Randomization
Memory corruption attacks target specific memory locations to modify program
behavior, such as function return addresses or function pointers. If the attacker does not
have precise information regarding the location of this targeted data, writing an effective
attack could be impossible. Xu et al.[27] take advantage of this by transparently
random-izing the runtime locations of several important memory regions, including the user stack,
random-ization (ASLR), is applied at runtime by the dynamic program loader, implying that each
program invocation potentially has a different memory layout. This invalidates attackers’
assumptions about the location of attack target addresses, which makes it very unlikely
that attacks will succeed. Attacks that fail typically cause the target application to crash
with a segmentation fault, illegal instruction error, or bus error. Furthermore, ASLR only
incurs overhead at application launch. This overhead is on the order of 1 or 2µs.
Due to the low amount of entropy provided by ASLR, it was shown that a brute
force attack can defeat address space randomization in the Apache web server in less than
four minutes[20]. However, in this time, the target application crashed many times. Thus,
one way to avoid an entropy attack is to simply monitor if an application is crashing a great
deal in a small period of time. If this is the case, the application can be relaunched using
Chapter 3
Failure-Aware Computing - A
Proposal
Failure-aware computing is a general approach to automating and simplifying the
failure response process described in Chapter 1. To create a concrete implementation of
failure-aware computing, one must determine what failures should be detected and how
those failure should be analyzed. Based on these decisions, individual components in the
architecture are implemented differently. However, the general architecture of failure-aware
computing remains the same despite these details. In this chapter, we will describe
failure-aware computing’s architecture and benefits in more detail.
Failure-aware computing has four broad goals. First, it should provide a
mecha-nism to automatically detect software errors as they occur. Second, it should simplify cause
analysis and failure reproduction. Third, it should simplify the generation of temporary
and/or permanent failure remedies. Finally, no mechanisms should significantly impact the
3.1
Failure-Aware Computing Framework
To accomplish these goals, we propose an architecture that is comprised of three
major components, each of which corresponds to one of three aforementioned phases in
the failure response process. For the failure detection phase, we install lightweight error detectors into the application’s runtime environment. To simplify cause analysis and error reproduction, we log and replay all external input using a logging/replay infrastructure. Finally, to simplify the production of failure remedies, we provide a separate analysis and response environment in which failures can be replayed and analyzed without affecting application runtime performance.
Lightweight error detectors are installed to transparently monitor the application
during runtime in order to quickly determine when a failure has occurred. To maintain high
performance, these detectors should consume as few resources as possible. Additionally, a
detector should be configurable enough that it can perform basic tasks upon failure
detec-tion, such as launching a shell script or executable. For example, administrators may want
to be notified when a failure occurs and automatically restart the process. By providing
this basic level of configurability, detectors can reduce the human effort needed to detect
and initially respond to a failure.
To simplify the implementation of detectors and lower their associated runtime
overhead, each detector should be specialized to only recognize a small number of error
types. For example, rather than having a single error detector handle all forms of buffer
overflows, one may have a separate error detector for stack buffer overflows and heap buffer
overflows. A side effect of specialized error detectors is that they allow system
administra-tors to very finely select which types of errors to monitor for their application. However,
if general detectors can be implemented with low overhead, they should be preferred over
specialized detectors. Additionally, error detectors do not have to directly detect a failure;
if a failure condition can be identified by observing a side-effect of an error, this is a
com-pletely acceptable method of failure detection. However, since one of the major purposes
of lightweight error detectors is to minimize the time between a failure occurrence and its
detection, indirect failure detection should detect the failure in a timely fashion.
Failure-aware computing’s logging/replay infrastructure records all external input
to the monitored application, including input from the keyboard, mouse, disk, and network.
during debugging and testing. For example, if a corrupt configuration file caused an error
to occur, all data read from that file is available during analysis. This helps reduce the
variability between a user’s runtime environment and that of the developer, improving
the possibility that the failure can be reliably reproduced. Also, because we only log an
application’s external input, the input logging infrastructure can be implemented so that it
is completely transparent to the target application.
In addition to logging external input, one major function of the logging/replay
infrastructure is to provide mechanisms to replay logged input in order to reproduce a
failure. At any point in a process’s execution, the logging/replay infrastructure can be
instructed to produce a replay report, which provides a log of the source and content of all external input and, if available, additional information such as stack traces and core
dumps. These reports can later be used as input into the logging/replay infrastructure
to re-run the application with the previously logged input and reproduce the failure. It
should be noted that replay reports can be valuable even after a failure remedy has been
produced. Replay reports essentially provide developers with real-world failure-inducing
input. As such, software vendors could keep databases of these failures and replay them
during regression testing. These reports could also be analyzed to gain a better sense on
how an application is being used, which can aid in performing code profiling and evaluating
future product directions.
The lightweight error detectors and logging/replay infrastructure combine to form
the failure-aware computingapplication runtime environment, illustrated in Figure 3.1. In this figure,D1,· · ·, Dn represent the numerous error detectors monitoring the application.
This environment encapsulates the components of failure-aware computing that gather
in-formation while an application is running. Both components must be implemented below
the application layer in the system in order to transparently provide functionality to the
monitored application. Beyond this requirement, however, each component’s
implementa-tion is quite flexible. For example, they could be implemented in the operating system
kernel, as a shared object file, or as part of a thin virtual machine on which the application
runs. While the implementation of error detectors depends largely on the types of errors
being detected, the logging/replay infrastructure could be implemented by adding
function-ality to system calls that get external input from the operating system, such as open and
read.
en-Application
Operating System Logging/
Replay Infrastructure Lightweight
Detectors
D1D2D3 … Dn
Figure 3.1: Failure-aware computing application runtime environment
vironment. This environment aids the developer in analyzing application execution and developing a remedy using analysis tools. The constraints on this environment are few; it
should merely provide an insulated setting that is distinct from the application runtime
environment and in which failure analysis technologies may be leveraged to produce a
rem-edy. Ideally, the employed analysis technologies will aid in finding the root cause of the
software failure, thus simplifying the process of finding a solution. The only constraint on
this solution is that, once applied, the software failure must no longer occur for the
failure-inducing input. Note that the analysis and response environment does not have to reside
on a different machine than the application runtime environment. That is, the separation
between these environments is virtual; its main purpose is to serve as a way to separate the
concerns of runtime and analysis.
There are several reasons for maintaining separate deployment and analysis
envi-ronments. Most importantly, it provides us with more flexibility when choosing the types
of analysis tools we wish to employ. Many analysis tools have operating requirements or
side effects that preclude their use with deployed software. For example, a tool may
signif-icantly degrade the runtime performance of the analyzed software or require the presence
of special libraries. For tools that provide valuable analysis and require few, if any, source
code changes, the potential to identify program errors is too great to entirely negate their
use. In these cases, maintaining two different environments allows us to completely bypass
the effects that such tools have on the runtime environment. Thus, tools that were once
deemed unsuitable for use due to their performance overhead can now be reevaluated on
their analysis strengths.
One may also leverage the dual-environment nature of failure-aware computing to
failure-Analysis and Response Environment Start
analysis
Analyze/ reduce
input
Replay app with failure-inducing input
Analyze
execution Produce remedy analysisExit Analysis
Tool Application Runtime Environment
Exit runtime Start
runtime applicationRun Detect failure
Produce failure report Lightweight
Detectors Logging/replayInfrastructure
Figure 3.2: Failure-aware computing component interaction
aware computing detects out-of-bounds buffer accesses in C programs. When a failure of
this sort is detected, an instrumented version of the application could be run that correlated
the length and characteristics of the input with the length of the buffer that was incorrectly
accessed. This information could be used to produce an input filter that blocked input with
those characteristics. Furthermore, this instrumented version could be distributed with the
application so that customers could produce input filters without involving the developer.
The interaction between failure-aware computing’s two environments and three
components is illustrated in Figure 3.2. Initially, the monitored application is run in the
application runtime environment, which may have more than one lightweight error detector
running in addition to the logging/replay infrastructure. When a software failure occurs,
the appropriate lightweight detector recognizes that an error has occurred and instructs the
logging infrastructure to produce a replay report. If the error detector has been configured
to do so, it may also restart the process or take another appropriate action as configured
by the system administrator.
At this point, failure analysis and response begins. In the analysis environment,
This analysis may be performed manually or automatically, but is not required. Then, the
application is invoked with the failure-inducing input, and its execution is analyzed by the
analysis tool. After analysis is complete, a remedy is formulated and applied to the version
of the application in the runtime environment.
3.2
Application to Memory Corruption Attacks
Failure aware computing provides a conceptual framework for speeding the
re-sponse to particular types of software failures. In this section, we propose a way to apply
the failure-aware computing framework to the task of responding to memory corruption
attacks against C programs. While we describe how this system could be implemented.
we focus our implementation efforts on a root cause analysis tool for use with the system,
which we describe in Chapter 4.
The system we are proposing has three main goals. First, it should efficiently
de-tect memory corruption attacks before the vulnerabilities are exploited. Next, the system
should be able to automatically generate vulnerability signatures that can be used
tem-porarily until a software patch for the error is available. Finally, our analysis and response
environment should be capable of not only identifying the location of an error, but also
helping the programmer quickly determine its root cause. In this section, we describe how
these goals may be achieved. However, due to time constraints, we do not implement
vul-nerability signature generation and assume that failure-aware computing’s logging/replay
infrastructure is already present.
3.2.1 Attack Detection
As we are proposing our system as an application of failure-aware computing, our
first task is to create lightweight error detectors to identify memory corruption attacks at
runtime. Here, we must balance three practical concerns: protection from likely attacks,
efficient runtime performance, and ease-of-implementation. It is not necessary for our
detec-tion method to specifically identify a detected memory corrupdetec-tion attack’s type; our system
considers all memory corruption attacks equally harmful.
error occurrences. ASLR provides protection against most memory corruption attacks and
incurs no runtime overhead after the application has been launched. Additionally, there are
several kernel- and user-level ASLR implementations available for use with our detector.
Address space randomization is not enough to detect memory errors by itself.
When a memory corruption error occurs, address space randomization almost always causes
the target application to crash with a segmentation fault, bus error, or illegal instruction
error. When our detector observes these abnormal program terminations, it surmises that
a memory corruption error has occurred; instructs the logging/replay infrastructure to
generate a replay report; and then restarts the target application. Whether the abnormal
termination was induced by malicious input is irrelevant; from the perspective of failure
response, all memory corruption errors require a response.
Due to our use of ASLR, our detector must also make special considerations
re-garding entropy attacks against randomization. As mentioned in Section 2.2.4, address
space randomization can be defeated in a few minutes with a brute-force entropy attack.
Our detector overcomes these shortcomings by simply noting when several crashes have
occurred in a short time interval. In this case, instead of restarting the server, the detector
simply runs a special user-specified script, which can be used to notify the system
adminis-trator of the situation. Even if an entropy attack is not occurring, several successive crashes
can safely be regarded as anomalous behavior that warrants administrator intervention.
The implementation of our detection mechanism depends on the implementation
of failure-aware computing’s logging/replay infrastructure. If that infrastructure is
imple-mented in a virtual machine, we can simply implement our detector as part of the same
VM. If the logging/replay infrastructure is implemented elsewhere, our detector can be
im-plemented as a program that launches the target application as a child process and then
performs the appropriate actions if the child process terminates abnormally. Figure 3.3
shows a Python implementation of the detector using this latter approach. This
imple-mentation first parses its command-line arguments, which include the target application to
launch, its launch parameters, and scripts to run when errors are detected. It then launches
the target application and waits for the program to terminate (line 7). If the target
appli-cation terminates abnormally, the time of the crash is recorded (line 12). This crash time
is then used to determine if an anomalous number of crashes has occurred. If this is the
case, a user-specified script specific to this scenario is executed, and the application is not
the target application is restarted per the user’s settings.
3.2.2 Vulnerability Signature Generation
One of the major goals of our system is to provide interim input filtering while
more permanent failure remedies are being produced. This can be accomplished by giving
customers an instrumented version of the target application that can be used to generate
filters that block failure-inducing input. While implementing a general signature generator
that can automatically produce filters is a very difficult problem, limiting signature
gen-eration to simpler bugs, such as stack-smashing buffer overflows, makes this task possible.
Current techniques typically accomplish this by using heuristic models to determine which
parts of program input should be filtered. For example, the technique proposed in [12]
extracts a program model from the target application from which contextual information
about input operations is extracted. Their approach gathers information about both
previ-ous and current calls for the same input operation in order to determine what behavior is
anomalous. Using this data, they estimate the length of unsafe inputs and filter them out
accordingly.
By taking advantage of failure-aware computing, we can provide more precise
signatures than these previous approaches. We can distribute an instrumented version of
the target application to customers, which can use bounds checking software to identify
buffer overflows. For simpler buffer overflows in which a single input operation causes the
overflow, we can use this information to identify the exact input operation that caused the
overflow and the maximum input length that the buffer can safely accommodate. We can
then filter out all input that is longer than the buffer’s allocated size. We can also scan
the failure-inducing input for bit patterns that are common in code injection attacks and
generate a signature to filter out that input.
The actual input filtering mechanism can be implemented as part of the
log-ging/replay infrastructure. Since all program input passes through this infrastructure, it is
a natural place to perform filtering. With our proposed system, when a failure is detected,
the detector can instruct the signature generator to construct a signature. If signature
generation is successful, the signature is loaded into the system before restarting the
appli-cation. The use of signature generation also provides additional protection against ASLR
1 crashTimes = []
2 app, args, anomalyScript, shouldRestart, \
3 errorScript, anomalyCount, timeInterval = ParseArgs(argv[1:])
4
5 while True:
6 # Execute the target application
7 exitSignal = Execute(app, args)
8 if exitSignal not in [SIGILL, SIGBUS, SIGSEGV]: break
9
10 # <INSTRUCT LOGGING INFRASTRUCTURE TO PRODUCE REPLAY REPORT HERE> 11
12 crashTime = datetime.datetime.now()
13 crashTimes = RemoveOldCrashes(crashTimes, crashTime, timeInterval)
14 crashTimes.append(crashTime)
15
16 # If a script was provided for an anomalous number of crashes,
17 # execute it. Even if no such script was provided, exit.
18 if len(crashTimes) >= anomalyCount:
19 if anomalyScript is not None:
20 Execute(anomalyScript, [])
21 sys.exit(0)
22
23 # If a script was provided for a normal number of memory
24 # errors, execute it.
25 elif errorScript is not None:
26 Execute(errorScript, [])
27
28 if not shouldRestart: break
• NULL pointer dereference
• Uninitialized pointer dereference
• Out-of-bounds array access
• Out-of-bounds pointer access due to pointer arithmetic errors
• Double-free
• String is not NULL-terminated
• String is less thanncharacters long
• Buffer does not havenreadable/writable bytes
Figure 3.4: Sample of error types detected by CCured
be filtered out. Note that while we believe vulnerability signature generation is important,
it is not the focus of this thesis. Instead, we direct the bulk of our attention to developing
permanent solutions.
3.2.3 Root Cause Analysis
The final aspect of our system is to determine the root cause of failures in the
anal-ysis and response environment. In choosing what analanal-ysis techniques to use, our primary
concerns are complete, sound analysis and ease-of-use. With regards to ease-of-use, analysis
techniques that yield many false positives or require a large amount of code changes are
undesirable. Also, tools that can more quickly identify the root cause of the error are more
useful when developing a remedy.
Given these criteria, we believe that combined static analysis and runtime
check-ing tools like those described in Section 2.2.3 provide the best balance of completeness and
ease-of-use. CCured in particular is appealing because it provides full memory protection,
works well with external libraries, and can be used to identify the type and location of
memory access errors. Figure 3.4 shows a partial list of error types that CCured can detect.
While porting an application to use CCured does require a significant amount of effort
ini-tially, maintaining CCured compatibility is quite simple. The CCured version of the target
application can also be used as to perform bounds-checking during signature generation.
some memory access errors before deployment.
If we simply use traditional debugging techniques with a CCured version of the
target application, we may still expend a considerable amount of effort identifying the root
cause of a memory corruption error. This is because CCured is primarily intended for use
during application runtime, and thus can only identify an error when memory safety is
actually violated. When this occurs, CCured aborts after outputting a cryptic message
describing the type of error that occurred. For example, if an attempt were made to read
past the end of an array, CCured would output:
Failure UBOUND at lib/ccuredlib.c:3816: __read_at_least_q(): Ubound Abort (core dumped)
Using a debugger, a stack trace could be used to determine the last line of
ap-plication code that was executed, thus identifying on where the memory safety violation
occurred. While this information is certainly useful, for all but the simplest errors, it will
not uncover the root cause of the error. In fact, it is often the case that the origin of a
memory error and its manifestation are greatly separated, both in program source and the
execution timeline. The code in Figure 3.5 shows a simple example of this situation. For
each line in a file, this code splits that line into a list of words, performs some additional
operations, and then prints each word. Suppose that after executing the CCured version of
this code, a memory access violation is detected on line 6. With this information alone, it
is not clear why the error occurred. Perhaps theGetElementfunction on line 5 returned an
erroneous value, or theListLength function evaluated incorrectly on line 4. Alternatively,
the Split function call on line 3 may have introduced the memory error. A particularly
subtle possibility is that the memory allocated for the return value of theReadlinefunction
is only valid until the next time it is called. Unfortunately, CCured alone does not provide
the mechanisms required to identify the flaw that caused the bug,
To find the cause of an error detected by CCured, the developer must go through
the potentially lengthy process of identifying program statements that may have played a
role in the error; tracing program execution with a debugger or other tools; and determining
when unintended behavior first occurs. To ease this task, we have implemented a tool that
automates much of this laborious process, thus aiding the developer to find the root cause
1 List *words = NULL;
2 for (line = Readline(file); line; line = Readline(file)) {
3 words = Split(line, " \t\n"); ...
4 for (i = 0; i < ListLength(words); i++) {
5 char *word = GetElement(words, i);
6 printf("%s\n", word); ...
Chapter 4
Root Cause Analysis Tool
In Chapter 3, we described a system that applies failure-aware computing to the
task of responding to memory corruption attacks against C programs. In this chapter,
we describe the implementation and evaluation of a tool we have developed for use in the
analysis and response environment of this system. The purpose of this tool is to help
identify the root cause of a memory corruption error so that a programmer may more
quickly formulate a software patch. To do this, we leverage CCured to identify the location
of a memory access violation in the target application. We then perform data dependence
analysis on the target application’s source code to identify the program statements that
potentially contributed to the error. We monitor these statements while replaying the
application with failure-inducing input to provide the developer with a timeline of events
that potentially contributed to the error, thus aiding the developer understand why the
failure occurred.
4.1
Conceptual Overview
The tool we have developed is designed to pick up where CCured’s analysis left
off. Namely, after we have identified the location of a memory access error using CCured,
our tool aids in tracking down the cause of the error. The tool’s functionality can be
data dependence analysis, our tool identifies program locations and expressions that may be
of interest in tracking down the error. These locations and expressions are then input into
the runtime monitoring component. This component replays failure-inducing input on the
target program and monitors execution to note when the aforementioned program locations
are executed and when the aforementioned expressions’ values change.
To help the developer track down the root cause of the memory error, our tool
per-forms program analysis to determine where important memory-related events that affect the
afflicted memory object occur. These events include allocation changes; pointer arithmetic
that changes the afflicted buffer’s pointer; and modifications to index variables, variables
used to determine allocation/reallocation sizes, and variables in the aforementioned pointer
arithmetic. The purpose of identifying these lines is to monitor them later to help determine
why the error is occurring.
Unfortunately, the imprecision of the above criteria leads to monitoring too many
program locations in some cases and too few in others. First, monitoringall modifications of a variable could produce spurious output. For example, in Figure 4.1, suppose a memory
access error occurred on line 6. The criteria above would identify lines 1, 2, 4, 5, and 6
as important program locations. However, the loop on line 4 has absolutely no impact on
line 6. While i is an index variable that is used on line 6 and modified on line 4, i is
assigned a completely new value on line 5. Therefore, no changes to ibefore line 5 are of
any interest. We can make our criteria more precise by only considering modifications that
can actually affect the buffer at the failure location. If we only monitor these modifications
of i, known as its reaching definitions, we can remove line 4 from our set of important program locations.
Suppose that the memory error in our example occurred because the value of i
on line 6 was one greater than expected. If we only monitored lines 1, 2, 5 and 6, we may
recognize that the error occurred becauseendwas too large on line 5, but we could not see
why that was the case. This is because our criteria do not include modifications to variables
that indirectly affect the afflicted buffer. A na¨ıve approach to solving this problem would be
to monitor modifications ofall variables on which the afflicted buffer depends. This criteria is equivalent to monitoring modifications to every variable that is connected to the afflicted
buffer in the program’s data dependency graph (DDG). In this very simple case, that would
surely solve the problem. Unfortunately, for larger programs, data dependency graphs can
1 len = getLength();
2 buf = malloc(len * sizeof(char *)); ...
3 end = (len < 256) ? len : len / 2; ...
4 for (i = 0; i < x; i++) other[i] = 0; ...
5 i = end; ...
6 buf[i] = ’\0’;
Figure 4.1: Code example to illustrate data dependence analysis
4
1 2 3 5 6
Vertexv Code corresponding tov
1 len = getLength();
2 buf = malloc(len * sizeof(char *));
3 end = (len < 256) ? len : len / 2;
4 for (i = 0; i < x; i++) other[i] = 0;
5 i = end;
6 buf[i] = ’\0’;
Figure 4.2: Figure 4.1’s data dependency graph
developer with spurious output.
The key to solving this problem is to consider the way many developers manually
debug applications. For the error in our aforementioned example, many developers first set
a breakpoint on line 6 to determine the values of bufandiat the time of the crash. If they
recognize that i is too large, they may then attempt to see where i’s value was last set,
namely line 5. After recognizing thatendis too large on this line, they may then go to line
3, on which end is set. Here they would see that for the particular failure-inducing input,
they assign len to endinstead of len - 1. Thus, they make the change to line 3 and fix
the bug. This process is equivalent to following a path between data dependency vertices
in the program’s DDG. In this example, the programmer incrementally traced a path from
vertex 6 to vertex 5 to vertex 3 in the data dependency graph in Figure 4.2.
Our tool attempts to simulate this process so that the developer may
Table 4.1: Figure 4.1’s data dependence ordinalities
Vertexv DDO(v,6) Code corresponding tov
1 2 len = getLength();
2 1 buf = malloc(len * sizeof(char *));
3 2 end = (len < 256) ? len : len / 2;
4 ∞ for (i = 0; i < x; i++) other[i] = 0;
5 1 i = end;
6 0 buf[i] = ’\0’;
automating this task is slightly more difficult because we can not predict at analysis time
what data dependency edge the developer will want to follow. To approximate this, we
instead allow the developer to choose the length l of the path to be followed. Then, we only include statements in our analysis whose corresponding DDG vertex is connected to
the failure location’s vertex by a path of length l or less. We call the shortest distance to
DDG vertex vi from DDG vertex vj thedata dependence ordinality ofvi relative tovj.
More formally, letvsandvtbe vertices in a program’s data dependency graph. We
define the data dependence ordinality ofvsrelative tovt, written DDO(vs, vt), as follows: if
vs=vt, DDO(vs, vt) = 0; if there is a data dependency edge fromvttovs, DDO(vs, vt) = 1;
if there exists some vertex vu such that DDO(vs, vu) = m and DDO(vu, vt) = n, then
DDO(vs, vt) = m+n. Finally, if there is no path from vt to vs in the data dependency
graph, DDO(vs, vt) =∞.
Table 4.1 illustrates the data dependence ordinalities relative to line 6 (the failure
location) for all statements in our ongoing example. Constructing these values simply
requires that one construct the shortest path between vertex 6 and every other vertex in
Figure 4.2. Because line 2 has a DDO of 2 relative to line 6, we know that the developer
must run our analysis with a DDO threshold of 2 to locate the root cause of the error. While
in this example, that threshold includes all but one line, in larger, more realistic examples,
the vast majority of data dependencies will be excluded.
After our analysis identifies source code locations and variables that may
con-tribute to the error, we then re-execute the application with failure-inducing input. In this
re-execution, we detect when the aforementioned locations are executed. When execution
completes for a given line, we display the variables on that line that may have contributed
display-Dependence
Analyzer Runtime Monitor
Failure Location
Source Code Monitored
Execution Output
Executable Monitor
Points
DDO Threshold
Figure 4.3: Root cause analysis tool component interaction
ing the resulting memory objects’ base addresses and the value of their arguments. This
effectively gives the user a timeline of program events that may have contributed to the
error. By tracing through those events, the user can quickly find where a memory error
originated.
4.2
Implementation Details
The implementation of our tool is separated into two components to match the
division of functionality. The first component, the data dependence analyzer, performs the
source code analysis described in the previous section. When this component is invoked
with an error location and DDO threshold, it generates a list of monitor points. These monitor points consist of a source code line and expressions that are to be monitored upon
execution of that line. This list of monitor points is then used as input to our tool’s second
component, the runtime monitor. The runtime monitor executes the original application
and monitors the value of each monitor point’s expressions as its source code location is
executed. Figure 4.3 shows the interaction between these two components.
4.2.1 Data Dependence Analyzer
We have implemented the data dependence analyzer on Linux as a plug-in to
CodeSurfer[8]. CodeSurfer is a commercial static analysis package with a programming
interface that allows plug-ins to analyze a program’s abstract syntax tree, control-flow
dependency graph, and data dependency graph. We use CodeSurfer to first build a data
1: procedure Visit(v, depth)
2: if v has been visited ordepth=ddoT hreshold then
3: return
4: end if
5: Mark v as visited
6: for allu∈Def(u)∪Use(u)do
7: if u is a variablethen
8: Mark u’s value for display when Location(v) is executed
9: else if u is the result of a call tomalloc,calloc, orreallocthen
10: Mark u’s arguments for display when Location(v) is executed
11: end if
12: end for
13: for allvariables u∈Use(v)do
14: Visit(LastDefined(u), depth+ 1)
15: end for
16: end procedure
Figure 4.4: Recursive algorithm used in our data dependence analyzer
with arguments that describe the location of the error in source code and the desired DDO
threshold. We then translate the source code location into a DDG vertex and use a recursive
algorithm to determine what source code locations and program variables to monitor.
The pseudo-code for our recursive algorithm can be seen in Figure 4.4. For each
DDG vertex v that the analyzer visits, it marks each variable defined or used atv’s source
code location for monitoring. If a variable defined atv is assigned the return value of a call
tomalloc,calloc, orrealloc, the analyzer takes note of this and records the expressions
representing the function call’s arguments. This enables the monitor to later distinguish
these important memory-related events from other assignments. For each variable used at
v, we then recursively visit the location of the variable’s last definition. Recursion stops
when the depth of recursion is equal to the user-supplied DDO threshold. To avoid infinite
recursive loops due to cycles in the DDG, we maintain a list of vertices that have been
visited and check if the current vertex is in that list before proceeding with dependence
calculations.
One difficulty in implementing this algorithm is in determining where a variable
was last defined. CodeSurfer provides functionality to find the last code location where a
variable was assigned a value. However, it does not have any knowledge of external library
Example.c:1%%%len
Example.c:2:m%%%buf%%%len * sizeof(char *)%%%len Example.c:3%%%end%%%len
Example.c:5%%%i%%%end
Example.c:6%%%buf%%%i%%%buf[i]
Figure 4.5: Analyzer output for Figure 4.1
analysis mechanisms do not account for variables changing as a result of calls to standard
library functions, likestrcpyorsprintf. The incorrect use of these functions often lead to
memory corruption vulnerabilities, so it is imperative that our analysis takes their effects
into account. To do this, we maintain a list of standard library functions that can modify
their arguments. The first time we visit a vertex v in a function f, we locate all function
calls withinf. For each call, we check if the called function is in the aforementioned list. If
so, we then check ifvis reachable from the call site. If this is the case, we mark the function
call site as a definition for the arguments that the function modifies. When determining
where a variable was last defined (line 14 in Figure 4.4), we retrieve the last assignment
using CodeSurfer’s analysis. If a function call also modified the variable, we determine if
the function call occurred after the assignment using the control-flow graph. If so, we visit
the function call site; otherwise, we visit the site of the assignment.
Once analysis is complete, the analyzer outputs a file containing a list of monitor
points. Figure 4.5 shows the analyzer’s output if it were run on the code in Figure 4.1 with a
DDO threshold of 2. The string%%%is used to delimit data in the output file. Each line lists
the source code location first, followed by the expressions whose values should be displayed
when that location is executed. File locations that end with:m, such as the second line in
the output, denote calls to malloc. File locations that end with:c and :r denote calls to
callocand realloc, respectively. In these cases, the first field listed denotes the variable
that stores the result of the call, and subsequent fields denote the call’s arguments, in order.