Chopped Symbolic Execution

Chopped Symbolic Execution

Chopped Symbolic Execution

David Trabish

Tel Aviv University
Israel

Andrea Mattavelli

Imperial College London
United Kingdom

Noam Rinetzky

Tel Aviv University
Israel

Cristian Cadar

Imperial College London
United Kingdom

ABSTRACT

the code with symbolic values instead of concrete ones. Symbolic

execution engines thus replace concrete program operations with

ones that manipulate symbols, and add appropriate constraints on

the symbolic values. In particular, whenever the symbolic executor

reaches a branch condition that depends on the symbolic inputs, it

determines the feasibility of both sides of the branch, and creates

two new independent symbolic states which are added to a worklist

to follow each feasible side separately. This process, referred to as

forking, refines the conditions on the symbolic values by adding

appropriate constraints on each path according to the conditions

on the branch. Test cases are generated by finding concrete values

for the symbolic inputs that satisfy the path conditions. To both

determine the feasibility of path conditions and generate concrete

solutions that satisfies them, symbolic execution engines employ

satisfiability-modulo theory (SMT) constraint solvers [19].

Symbolic execution is a powerful program analysis technique that

systematically explores multiple program paths. However, despite

important technical advances, symbolic execution often struggles to

reach deep parts of the code due to the well-known path explosion

problem and constraint solving limitations.

In this paper, we propose chopped symbolic execution, a novel form of symbolic execution that allows users to specify uninteresting parts of the code to exclude during the analysis, thus only targeting the exploration to paths of importance. However, the excluded parts are not summarily ignored, as this may lead to

both false positives and false negatives. Instead, they are executed

lazily, when their effect may be observable by code under analysis. Chopped symbolic execution leverages various on-demand

static analyses at runtime to automatically exclude code fragments

while resolving their side effects, thus avoiding expensive manual annotations and imprecision.

The Challenge. Symbolic execution has proven to be effective at

finding subtle bugs in a variety of software [

3

,

11

  • ,
  • 12, 25, 39], and

Our preliminary results show that the approach can effectively

improve the effectiveness of symbolic execution in several different

scenarios, including failure reproduction and test suite augmenta-

tion.

  • has started to see industrial take-up [13 15, 25]. However, a key
  • ,

remaining challenge is scalability, particularly related to constraint

solving cost and path explosion [14].

Symbolic execution engines issue a huge number of queries to the

constraint solver that are often large and complex when analyzing

real-world programs. As a result, constraint solving dominates

CCS CONCEPTS

• Software and its engineering → Software testing and de-

  • runtime for the majority of non-trivial programs [30 33]. Recent
  • ,

bugging;

research has tackled the challenge by proposing several constraint

  • solving optimizations that can help reduce constraint solving cost [
  • 5,

KEYWORDS

Symbolic execution, Static analysis, Program slicing

ACM Reference Format:

David Trabish, Andrea Mattavelli, Noam Rinetzky, and Cristian Cadar. 2018.

Chopped Symbolic Execution. In ICSE ’18: 40th International Conference on

Software Engineering, May 27-June 3, 2018, Gothenburg, Sweden. ACM, New

York, NY, USA, 11 pages. https://doi.org/10.1145/3180155.3180251

12, 21, 27, 33–35, 41, 45].

Path explosion represents the other big challenge facing symbolic execution, and the main focus of this paper. Path explosion

refers to the challenge of navigating the huge number of paths in

real programs, which is usually at least exponential to the number

of static branches in the code. The common mechanism employed

by symbolic executors to deal with this problem is the use of search

heuristics to prioritise path exploration. One particularly effective heuristic focuses on achieving high coverage by guiding the

  • 1
  • INTRODUCTION

exploration towards the path closest to uncovered instructions [10 12 43]. In practice, these heuristics only partially alleviate the path

explosion problem, as the following example demonstrates.

–

Symbolic execution lies at the core of many modern techniques to software testing, automatic program repair, and reverse engi-

,

  • neering [ 11, 16, 24, 32, 35]. At a high-level, symbolic execution
  • 3,

systematically explores multiple paths in a program by running

Motivating Example. The extract_octet() function, shown in

Figure 1, is a simplified version of a function from the libtasn1

Permission to make digital or hard copies of all or part of this work for personal or

classroom use is granted without fee provided that copies are not made or distributed

for profit or commercial advantage and that copies bear this notice and the full citation

on the first page. Copyrights for components of this work owned by others than the

author(s) must be honored. Abstracting with credit is permitted. To copy otherwise, or republish, to post on servers or to redistribute to lists, requires prior specific permission

and/or a fee. Request permissions from [email protected].

1

library which parses ASN.1 encoding rules from an input string.

The ASN.1 protocol is used in many networking and cryptographic

applications, such as those handling public key certificates and

electronic mail. Versions of libtasn1 before 4.5 are affected by a

heap-overflow security vulnerability that could be exploited via a

ICSE ’18, May 27-June 3, 2018, Gothenburg, Sweden

2

crafted certificate. Unfortunately, given a time budget of 24 hours,

©

2018 Copyright held by the owner/author(s). Publication rights licensed to the

Association for Computing Machinery. ACM ISBN 978-1-4503-5638-1/18/05...$15.00 https://doi.org/10.1145/3180155.3180251
1https://www.gnu.org/software/libtasn1 2https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-3622

  • ICSE ’18, May 27-June 3, 2018, Gothenburg, Sweden
  • Trabish, Mataaelli, Rinetzky and Cadar

12

int extract_octet(asn_t asn, char *str, int str_len) {

int len3, counter, counter_end, result; int len2 = get_length(str, str_len, &len3); counter = len3+1;

functions that construct the corresponding ASN.1 representation,

such as append_value. Therefore, we could have quickly reached

the bug if we had skipped the irrelevant functions that build the

AST.

345

counter_end = str_len;

In this paper, we propose a novel form of symbolic execution

called chopped symbolic execution that provides the ability to spec-

ify parts of the code to exclude during the analysis, thus enabling

symbolic execution to focus on significant paths only. The skipped

code is not trivially excluded from symbolic execution, since this

may lead to spurious results. Instead, chopped symbolic execution

lazily executes the relevant parts of the excluded code when ex-

plicitly required. In this way, chopped symbolic execution does not

sacrifice the soundness guarantees provided by standard symbolic

execution—except for non-termination of the skipped functions, which may be considered a bug on its own—in that only feasible

paths are explored, but effectively discards paths irrelevant to the

task at hand.

6

while (counter < counter_end) {
// call to get_length() leads to a heap overflow: len2 = get_length(str+counter, str_len, &len3);

if (len2 >= 0) {

DECR_LEN(str_len, len2+len3); append_value(asn, str+counter+len3, len2);

} else {

789
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

DECR_LEN(str_len, len3); result = extract_octet(asn, str+counter+len3, str_len);

if (result != SUCCESS) return result;

len2 = 0;
}// str_len should have been decremented at the // beginning of the while block DECR_LEN(str_len, 1);

We developed a prototype implementation of chopped symbolic

execution and report the results of an initial experimental evaluation that demonstrates that this technique can indeed lead to

efficient and effective exploration of the code under analysis.

counter += len2+len3+1;
}

return SUCCESS;

}

Figure 1: A simplified excerpt from the extract_octet rou-

tine in libtasn1. The invocation of get_length() in line 8

leads to a heap overflow because str_len has not been decremented before the call.

Main Contributions. In summary, in this paper we make the

following contributions:

(1) We introduce chopped symbolic execution, a novel form of sym-

bolic execution that leverages a lightweight specification of

uninteresting code parts to significantly improve the scalability

of symbolic execution, without sacrificing soundness.

(2) We present Chopper, a prototype implementation of our tech-

nique within KLEE [11], and make it publicly available.

(3) We report on an experimental evaluation of Chopper in two

different scenarios: failure reproduction and test suite augmen-

tation, and show that chopped symbolic execution can improve

and respectively complement standard symbolic execution.

This paper is organised as follows. Section 2 gives a high-level

overview of chopped symbolic execution, and Section 3 presents

our technique in detail. Section 4 briefly discusses our implementa-

tion inside the KLEE symbolic execution engine. Section 5 presents

the experimental evaluation of our technique, and in particular it shows that chopped symbolic execution can overcome the limita-

tions of state-of-the-art symbolic executors. Section 6 surveys the

main approaches related to this work. Section 7 summarises the

contributions of the paper and describes ongoing research work.

the analysis of the extract_octet() function using the state-ofthe-art symbolic execution engine KLEE [11] fails to identify the

vulnerability due to path explosion.

At each loop iteration (lines 6–23), the function decodes the

length of the current data element with get_length (line 8). Func-

tion get_length scans through the input string and decodes the

ASN.1 fields. Then, the execution either recursively iterates over the

input string (line 14), or invokes append_value (line 11). Function

append_value creates the actual node in the Abstract Syntax Tree

(AST) by decoding the input string given the obtained length. This

function scans once more over the input string, performs several

checks over the selected element, and allocates memory for the

node in the recursive data structure.

Path explosion in this function occurs due to several nested

function calls. Symbolically executing function get_length alone

with a symbolic string of n characters leads to 4 ∗ n different paths.

Function append_value increases even more the number of paths

and also affects the efficiency of the symbolic execution engine due

to a huge number of constraint solver invocations. As a result, the

symbolic executor fails to identify the heap-overflow vulnerability

at line 8.

  • 2
  • OVERVIEW

In this section, we give a high-level overview of chopped symbolic

execution using the simple program in Figure 2. In particular, Fig-

ure 2a shows the entry point of the program (function main), while

Figure 2c shows the uninteresting code which we would like to

skip (function f).

Our Approach. Identifying the vulnerability from the entry point

of the library is not trivial: To reach the faulty invocation of func-

tion get_length, the input triggering the vulnerability traverses

2,945 calls to 98 different functions, for a total amount of 386,727

instructions. Our key observation is that most of the functions

required during the execution are not relevant for finding the vul-

nerability. The vulnerability occurs due to an incorrect update of

the remaining bytes for parsing (line 21), which results in a memory

out-of-bound read when calling get_length. The bug thus occurs

in code which deals with the parsing, which is independent from the

We start the chopped execution by executing main symbolically.

When a state reaches the function call for at line 7, we create a

f

snapshot state by cloning the current state, and skip the function

call. The snapshot state is shown graphically in Figure 2b, where

each gray oval represents a symbolic execution state.

With a snapshot created, we then continue the execution on

the current state, but from this point we must consider that some

  • Chopped Symbolic Execution
  • ICSE ’18, May 27-June 3, 2018, Gothenburg, Sweden

12

struct point { int x, y, z;};

3
1

Recovery

3

int main() {
15 void f(struct point *p, int k) {

Snapshot

4

struct point p = {0, 0, 0}; int j, k; // symbolic

  • k≤0
  • k>0

16 17 18 19 20 21 22 23

if (k % 2)

p->z++;

5

Recovery’ Recovery’’

6

  • j≤0
  • j>0

7

f(&p, k); // skip

if (j > 0) if (p.y)

bug();

2

if (k > 0)

p->x++;

else

p->y++

8

Dependent

  • 4
  • 5

9

Recovery’

  • k≤0
  • k>0

1
=

10 11 12 13 14

  • y
  • p.

p->y++;

return

else

6

}

7

allgood();

return 0;

}

—

(c)

Dependent + Recovery’’
Dependent + Recovery’

  • (b)
  • (a)

Figure 2: Graphical illustration of chopped symbolic execution on a simple example.

load instructions may depend on the side effects of the skipped function f, i.e. the memory locations that may update. In our example, the side effects of are the memory locations pointed

to by p.z, p.x, and p.y, which are updated at lines 17, 20, and 22

respectively. (We compute the side effects of using conservative

static pointer analysis [ 26 37] before the symbolic exploration

example, if line 8 were changed from if (j>0) to if (k>0), then

the dependent state would have k > 0 in its path condition, ren-

dering the dependent state incompatible with the path in f where

k ≤ 0.

ff

f

One way to filter such incompatible paths would be to execute

4

  • ,
  • ,

all possible paths thorough during recovery, and later filter the

f

starts, see §3.) We define those instructions that read from the side

effects of the skipped functions as dependent loads.

ones that are incompatible with the dependent state. However, this would potentially lead to the exploration of a large number of infeasible paths. We thus designed a more efficient solution:

Each state maintains a list of guiding constraints, which are those

constraints added since the call to the skipped function. In our

example, the guiding constraints for the dependent state are j > 0.

Before we execute a recovery state, we add these guiding constraints from the dependent state to the path condition of the recovery state. By doing this, we guarantee that every path explored in the recovery

state is consistent with respect to its dependent state.

On some paths, symbolic execution does not encounter such dependent loads. For example, the path following the else side of the branch at line 8 accesses neither p.x nor p.y nor p.z, so no further action is needed on those paths, and the exploration

  • may correctly terminate without ever going through the code of
  • f.

Indeed, in real programs there are often paths that do not depend

on the skipped functions, and in such cases symbolic execution

immediately benefits from our approach: irrelevant paths are safely

skipped, thus reducing path explosion.

During recovery, one could execute all possible paths through

However, on other paths symbolic execution encounters depen-

dent loads. This happens for our example on the path which explores

the then side of the branch at line 8, when it loads the value of

p.y at line 9. At this point, the current state needs to be suspended

until the relevant paths in function f are explored, and becomes a

dependent state. To recover a path, we create a new recovery state

the skipped function which are compatible with the dependent

f

state, as we could in the example above. However, for real programs

this could be unnecessarily expensive, as many paths do not influence the dependent load which started the recovery. To avoid this

possible path explosion, and reduce the cost of constraint solving,

we aim to only execute the paths that could influence the depenwhich inherits the snapshot state generated before skipping

line 7 and start executing symbolically the function.

f

at

dent load. We accomplish this by statically slicing [

function with respect to the store instructions that write to the

memory location read by the dependent load, that is, the side effects observable from the dependent load. Note that function could call

  • 7,
  • 40, 42, 44] the

f

While symbolic execution is in the recovery state, if the execution forks, then the same fork is performed in the dependent

state. Furthermore, as we run the recovery state, any stores to the

memory location read by the dependent load are also performed

f

other functions, so the slicing is done for all these functions too. In

our example, the slicing would likely be able to completely remove

the if statement at lines 16–17, which would halve the number of

explored paths, thus reducing path explosion. It would also likely remove the then side of the if statement at line 19, which in this

case does not bring significant benefits, but it could, if that side of

the branch were replaced by say, an expensive loop. Slicing away

these code parts is possible because they do not update p.y on

in the dependent state. For example, if the symbolic execution of

f

traverses the else branch at lines 21–22, then the value of p.y (the

memory location pointed to by p->y) is set to 1 in the dependent

state too. If the recovery state returns successfully, the dependent

state is resumed successfully. If an error occurs while executing

the recovery state (e.g., an invalid memory access or a division by

zero error, which could have occurred if p->z were set in line 17 to

4/p->y) the corresponding dependent state is terminated.

3

which the dependent load on line 9 relies.

When we execute a recovery state, not all paths might be com-

patible with the execution which the dependent state reached. For

3In practice, the success of the slicing algorithm in reducing the size of the code

depends on the precision of the underlying pointer analysis.

  • ICSE ’18, May 27-June 3, 2018, Gothenburg, Sweden
  • Trabish, Mataaelli, Rinetzky and Cadar

Algorithm 1 Chopped symbolic execution (simplified).

Figure 2b shows how chopped symbolic execution works on

our example in a graphical way. To recapitulate, when the call to

1: worklist ← ∅

f

is reached at line 7, a snapshot state is created by cloning the

2: function cse(s , skipFunctions)

0

⃝

1

  • current state (step
  • in the figure). Then, on the execution state

3: worklist ← worklist ∪ {s }

0

that reaches line 9, the current state becomes a dependent state and

4: while worklist , ∅ do

⃝

is suspended (step

2

), and a recovery state is created by cloning

View Full Text

Details

  • File Type
    pdf
  • Upload Time
    -
  • Content Languages
    English
  • Upload User
    Anonymous/Not logged-in
  • File Pages
    11 Page
  • File Size
    -

Download

Channel Download Status
Express Download Enable

Copyright

We respect the copyrights and intellectual property rights of all users. All uploaded documents are either original works of the uploader or authorized works of the rightful owners.

  • Not to be reproduced or distributed without explicit permission.
  • Not used for commercial purposes outside of approved use cases.
  • Not used to infringe on the rights of the original creators.
  • If you believe any content infringes your copyright, please contact us immediately.

Support

For help with questions, suggestions, or problems, please contact us