• No results found

Automating and Simplifying Memory Corruption Attack Response Using Failure-Aware Computing

N/A
N/A
Protected

Academic year: 2020

Share "Automating and Simplifying Memory Corruption Attack Response Using Failure-Aware Computing"

Copied!
61
0
0

Loading.... (view fulltext now)

Full text

(1)

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

(2)

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

(3)

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

(4)

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

(5)

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

(6)

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

(7)

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

(8)

List of Tables

(9)

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

(10)

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

(11)

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

(12)

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

(13)

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

(14)

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

(15)

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

(16)

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

(17)

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

(18)

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,

(19)

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

(20)

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

(21)

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.

(22)

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.

(23)

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

(24)

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,

(25)

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.

(26)

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

(27)

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

(28)

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

(29)

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

(30)

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

(31)

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

(32)

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

(33)

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

(34)

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

(35)

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

(36)

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

(37)

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

(38)

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.

References

Related documents

In 2008, the data reporting and data collection process was standardised, and periodically analysed in line with the establishment of the BokSmart National Rugby Safety Programme,

are: events, states, or what have you. I assume in what follows that they are some sort of mental state, but nothing here bears on that... Perceptual experience also has

Moreover, it reflects upon the social make-up of travellers, who made their way to Denmark and discusses the ways the Danish press as well as state-related institutions saw

The imposition of a continuous and moderate drought stress to non-mycorrhizal lettuce plants induced the accumulation of anthocyanins in both outer and inner leaves of the green

This is accomplished by expressing all figures in the statements as percentages of an important item such as total assets (in the balance sheet) or percentages

It is not the aim of this study to estimate the prevalence of hearing impairment in the older population in England but to exam- ine SEP gradients in the health-seeking

The statistical test for an equal difference between the two methods in 2007 and 2008 confirms the superior dynamic targeting properties of CBT: the three double differences

For 12 month contracts there is a charge of £12 per month for the remaining term of the contract, for cancellation at any point between the broadband service start date and the