KLEESpectre is a symbolic execution engine with speculation semantic and cache modelling
Spectre attacks disclosed in early 2018 expose data leakage scenarios via cache side channels. Specifically, speculatively executed paths due to branch mis-prediction may bring secret data into the cache which are then exposed via cache side channels even after the speculative execution is squashed. Symbolic execution is a well-known test generation method to cover program paths at the level of the application software. In this paper, we extend symbolic execution with modelingof cache and speculative execution. Our tool KLEESPECTRE, built on top of the KLEE symbolic execution engine, can thus provide a testing engine to check for the data leakage through cache side-channel as shown via Spectre attacks. Our symbolic cache model can verify whether the sensitive data leakage due to speculative execution can be observed by an attacker at a given program point. Our experiments show that KLEESPECTREcan effectively detect data leakage along speculatively executed paths and our cache model can further make the leakage detection much more precise.READ FULL TEXT VIEW PDF
Cache timing attacks allow third-party observers to retrieve sensitive
Out-of-order speculation, a technique ubiquitous since the early 1990s,
The disclosure of the Spectre speculative-execution attacks in January 2...
Constant-time programming is a countermeasure to prevent cache based att...
Cache-based side channels enable a dedicated attacker to reveal program
Quantitative information flow (QIF) is concerned with measuring how much...
The Spectre vulnerability in modern processors has been reported earlier...
KLEESpectre is a symbolic execution engine with speculation semantic and cache modelling
Speculative execution in modern super-scalar microprocessors improves the program performance (by reducing execution time and by increasing throughput) compared to a non-speculative processor by predicting both the outcome and the target of branching instructions. The processor continues executing instructions after the branch where the number of speculatively executed instructions depends on how soon the actual branch condition is evaluated and also on the size of the buffer that holds the resulting states during speculative execution.
If the prediction of a branching instruction is incorrect, all effects due to the speculatively executed instructions after the branch instruction are rolled back. To this end, the buffer and pipeline stages are flushed which hold these instructions or their results. However, if the cache content is also modified due to speculatively executed load instructions, the cache state is not fully rolled back. This opens up the possibility of a cache side channel through which an attacker can obtain sensitive information from a user who shares the same platform with the attacker. The family of Spectre attacks (Kocher et al., 2018) shows that this vulnerability is present in all modern general purpose processors. Such a vulnerability thus poses major concerns from the stand-point of software security.
Symbolic execution (King, 1976) is a well-known path exploration method that can be used for program testing and verification. Given a program with un-instantiated or symbolic inputs, it constructs a symbolic execution tree by expanding both directions of every branch whose outcome depends on symbolic variable(s). The leaf nodes of the tree correspond to program paths, and by solving the constraint accumulated along a program path (also called a path condition), a test input to explore the path can be generated.
Symbolic execution can be used to cover program paths (modulo a time budget). However, it does not consider behaviors induced by performance enhancing features of the underlying processor, specifically cache and branch prediction. Due to branch mis-prediction, certain paths may be speculatively executed and then squashed. Such speculatively executed paths are not covered in symbolic execution. However, one may argue that there is no need to cover the speculatively executed paths since they are ultimately squashed and they have no impact on the observable behavior of the program. However, in the presence of caches, certain sensitive data may be brought into a cache in a speculatively executed path. This data may linger in the cache even after the speculative path is squashed. Such sensitive data may then be potentially ex-filtrated by attackers via cache side channels. Current generation symbolic execution engines, as embodied by tools like KLEE (Cadar et al., 2008) do not demonstrate the presence or absence of such side channel scenarios. This is because the reasoning in current day symbolic execution engines is solely at the program level.
In this paper, we extend symbolic execution with the modeling of speculative execution as well as cache accesses. For an unresolved branch involving a symbolic variable, classical symbolic execution considers two possibilities - the branch is either taken or not taken. In the presence of speculative execution, note that for every unresolved branch we need to consider four possibilities, namely: taken and correctly predicted, taken and mis-predicated, not taken and correctly predicted, not taken and mis-predicted. As explained earlier, since the mis-predicted execution paths are squashed, they only need to be considered in symbolic execution in the presence of cache modeling. We model the behavior of the cache by capturing memory accesses to concrete or symbolic memory addresses; the symbolic memory accesses occur when the accessed memory address depends on a symbolic input such as accessing array element a[i] when i is a symbolic input variable. Given such symbolic memory accesses, the possible cache conflicts (two memory accesses to the same cache set) can be captured as a symbolic formula. By solving such symbolic formula, we can enunciate whether a secret brought into cache in a speculative path continues to linger in the cache (this is when it has not been evicted from the cache due to cache conflicts). Hence we can detect and infer the cache side-channel leakage in Spectre attacks.
We present KLEESpectre , our methodology to extend state-of-the-art symbolic execution engines with micro-architectural features, specifically speculative execution and caches (Section 4).
We present a symbolic cache model embodied in KLEESpectre to precisely detect and highlight cache side-channel leakage through speculative execution paths, resulting in potential Spectre style attacks (Section 4).
We evaluate KLEESpectre on litmus tests provided by Kocher (Kocher, 2018) as well as on real-world cryptographic programs from libTomCrypt, Linux-tegra,
openssl and hpn-ssh. Our evaluation reveals that KLEESpectre can effectively and efficiently detect Spectre vulnerable code. Moreover, the cache modeling embodied in KLEESpectre results in a precise leakage detection by ruling out false positives.
In this section, we introduce the necessary background regarding the speculative execution and our targeted threat model.
Speculative execution (González and González, 1997) is an indispensable micro-architectural optimization for performance enhancement in modern superscalar processors. Speculative execution allows the processor pipeline to continue execution even in the presence of some data or control dependency between the current instruction and the unfinished instructions instead of stalling the pipeline. Branch predictor is one of the prediction unit in processor supporting speculative execution. The branch predictor predicts the execution path based on the history of the executed branch instructions. The processor stores a record of the speculatively executed instructions in a so-called Reorder Buffer (ROB). This buffer mainly helps the processor to commit all instructions in-order though they are executed out-of-order. If the outcome of a branch prediction is correct, then the instructions in ROB are committed to the architectural state, otherwise, the results of these instructions are squashed. However, the effect of the load execution unit i.e. the bytes that are read from memory during speculative execution may reside in the cache. The state of the cache is usually not squashed due to performance reasons. Thus, for a mis-predicted branch, even though the functional effects of all speculatively executed instructions are rolled back, the cache state may hold unexpected memory addresses. This phenomenon opens the potential vulnerability of cache side-channel attack.
Spectre-style attacks have proven that the computer can leak secret data through the cache side channel when it performs the speculative execution. Bound Check Bypass (BCB, also called Spectre variant 1) attack is one such Spectre attack. BCB attack can be performed by mis-training a vulnerable branch in the victim’s process to leak data from the victim.
Listing 1 shows an example code vulnerable to BCB attacks. In this example code, if the condition x < array1_size holds, then the statement at line 2 loads array1[x] to variable y. Finally, the statement at line 3 reads data from array2 where the accessed address depends on the value array1[x]. Normally, the boundary check at line 1 guarantees the absence of out-of-bound memory access. However, in the presence of the speculative execution, such guarantees do not hold. For example, the mis-prediction of the branch instruction at line 1 allows a memory access array1[x] where x array1_size. Such a memory access may point to a sensitive value. Thus, y may hold a sensitive value when the branch is mis-predicted. Finally, the statement at line 3 changes the cache state using the potential sensitive value y. By observing this cache state, the attacker can reconstruct the potentially sensitive value y. For simplicity, we name the branch potentially causing the BCB attack as Vulnerable Branch (VB), the instruction loading the potential sensitive data as Read Secret (RS) (e.g statement at line 2) and the instruction leaking the sensitive data to cache state as Leak Secret (LS) (e.g statement at line 3).
Similar to the existing literature on cache side-channel attacks (Liu et al., 2015), in this work, we assume the victim and the attacker coexist on a machine, and they share the cache. The attacker can execute any code in its security domain (e.g. a process or a virtual machine) and it can learn information from the shared cache side-channel. Besides, in our threat model, we do not consider the data leakage in the normal execution path. Instead, we focus on data leakage only due to the speculative execution.
We assume that all conditional branches in a program are potentially vulnerable. This is in line with the existing works on Spectre-style attacks (Canella et al., 2018) that show the possibility of a branch to be mis-trained either by the victim process or outside the victim process (e.g. by an attacker-controlled process). As a result, any branch in the victim process is potentially vulnerable to mis-training by the attacker. To consider the implication of our threat model, consider the code in listing 2. Since the conditional branch at line 6 is unsatisfiable, the code at lines 7–8 will never be executed without speculation. However, in our considered threat model, the code at lines 7–8 can leak data if the branch at line 6 is mis-trained and the branch is subsequently mis-predicted (thus pointing outside the array bound of array1). We also note that neither the branch nor the memory access at line 7 is controlled by any external input.
Finally, we assume that the attacker can either perform the access-based cache side-channel attack or the trace-based cache side-channel attack (Reineke et al., 2007). The ability of such attackers depend on which execution points (s)he observes cache states. In particular, the access-based attack assumes that an attacker can probe the cache only upon the termination of a program. On the contrary, the trace-based attack assumes that an attacker can snoop the cache after any executed instruction from the victim process.
Intuitively, KLEESpectre is an effort to consider and expose the micro-architectural execution semantics at software layer. Specifically, KLEESpectre enhances the machinery of symbolic execution with branch speculation and cache modeling. In the following, we will use a running example to show the motivation behind the design of KLEESpectre and briefly outline the KLEESpectre work-flow. We use the term normal execution to capture the execution semantics embodied in classic symbolic execution tools.
The example: We consider the example code shown in Figure 1(a). The variable x is a user controlled input. The code performs several memory related operations on two arrays array1 and array2. Although x is user controlled, we note that the access to array1[x] is protected by the bound check (i.e. ). Thus, considering the normal execution, the example does not exhibit any out-of-bound access. Figure 1(b) captures the execution tree generated by any classic symbolic execution tool.
Enhancing symbolic execution: Consider the code fragments labelled A in Figure 1(a). Such a code has the following problems that only appear in the presence of branch speculation. Assume the value of the user controlled input is such that . If the branch is mis-predicted, then the memory access array1[x] exhibits an out-of-bound reference. Moreover, if array1[x] captures a sensitive value (e.g. a secret), then the subsequent memory access array2[array1[x]] (cf. Figure 1(a)) refers to a memory address dependent on secret value. Memory addresses that depend on secret values are potentially exposed to cache side-channel attacks. For example, consider the access-based attacker who probes the state of the cache after the end of execution. For such an attack, the attacker might be successful to ex-filtrate the value of array1[x] (potentially holding a sensitive value) only if array2[array1[x]] remains in the cache after the execution.
It is evident from the preceding example that detecting the potential leakage of array1[x] is beyond the capability of classic symbolic execution. Specifically, to detect this side channel scenario, it is crucial to capture both the branch speculation and the cache behaviour while exploring symbolic execution states. In KLEESpectre , we enhance the power of symbolic execution along these two dimensions.
Speculative symbolic execution in KLEESpectre : In KLEESpectre , the purpose of speculative symbolic execution is to explore any potential secret that might be accessed due to branch speculation. To investigate the mechanism, consider again the example in Figure 1(a). To incorporate the branch speculation within symbolic execution, consider the branch (i.e. ). In the presence of branch speculation, KLEESpectre encounters the following four scenarios:
: is satisfiable and the branch is correctly predicted. In this case, the symbolic execution will fork a new state with constraint and proceeds by executing the code fragment A.
: is satisfiable and the branch is correctly predicted. In this case, the symbolic execution will fork a new state with constraint and proceeds by executing the code fragment C.
: is satisfiable and the branch is mis-predicted. In this case, KLEESpectre forks a new state with constraint , but proceeds by executing the code fragment A.
: is satisfiable and the branch is mis-predicted. KLEESpectre forks a new state with constraint , but proceeds by executing the code fragment C.
and are the additional symbolic states explored by KLEESpectre at branch . Figure 1(b) and (c) capture the symbolic execution trees explored by normal symbolic execution and KLEESpectre , respectively, for the code in Figure 1(a).
The symbolic execution along a speculative path spans across only a limited number of instructions. This is because the maximum number of speculatively executed instructions is bounded by the size of the re-order buffer (ROB). In KLEESpectre , we use Speculative Execution Window () to limit the number of speculatively executed instructions at any branch. It is worthwhile to note that a speculatively executed path may still span over multiple branch instructions (cf. Figure 1(c)) despite the limited size of .
KLEESpectre prunes speculative symbolic states if they do not pose any risk of data leakage. For example, in Figure 1(c), only the execution of code fragment A under the branch exhibits such risk. This is due to the access of array elements array1[x] and array2[array1[x]]. Also note that, the symbolic states , , and are all discarded once KLEESpectre reaches the limit of speculation window . In this fashion, KLEESpectre can control the explosion in the number of symbolic states due to speculation. Specifically, for the example in Figure 1(c), KLEESpectre only keeps the record of executing code A under the speculative state .
The next stage of KLEESpectre computes whether the secret accessed in can potentially be ex-filtrated by a cache side-channel attacker.
Cache modeling in KLEESpectre : KLEESpectre computes the set of memory access sequences that are potentially vulnerable to a cache side-channel attack. Each such memory access sequence may involve at least one memory access along the speculative path and multiple memory accesses along the normal execution path. Moreover, along the speculative path, we only record memory accesses that are dependent on secret. This is because KLEESpectre focuses to discover data leakage due to branch speculation.
For example, in Figure 1(c), KLEESpectre computes the following sequence of memory accesses for inspecting the leakage of data:
The triplet captures that the memory address was accessed with the symbolic constraint in code fragment A. The sequence of memory accesses capture the accesses in the speculative state followed by a memory access in the normal state (cf. Figure 1(c)). Even though the functional states in do not affect the computation in , the cache state influenced in remains unchanged when the branch is resolved and the execution continues through code fragment C (cf. Figure 2).
Through our cache modeling, we check the presence of the address in the cache when the code segment C finishes execution. To this end, we check whether memory access can replace from the cache. For the sake of simplicity, let us assume a 1-way associative (i.e. direct-mapped) cache. For direct-mapped caches, a memory address maps to exactly one cache line. In particular, the following symbolic condition is satisfiable if and only if the terminating cache state holds the memory address :
where and capture the cache line and cache tag, respectively, for a memory address . Intuitively, the constraints in Formula 1 can be presented to a satisfiability modulo theory (SMT) solver. KLEESpectre formulates such constraints for each memory access sequence that may access secrets along a speculative path. These constraints are then discharged by an SMT solver to check the presence of data leakage due to speculation.
In the subsequent section, we will elaborate the individual sub-systems within KLEESpectre in detail.
In this section, we describe the design of KLEESpectre . First, we describe the overall speculative symbolic execution process augmented with a symbolic cache model. Subsequently, we discuss in detail the features of the cache model to accurately detect the cache side-channel leakage along a speculative execution path.
Algorithm 1 outlines the overall process involved in KLEESpectre . Our methodology takes a program and symbolically executes by taking into account the speculation at branches. Moreover, KLEESpectre records memory accesses along the speculatively executed paths to check whether any such memory access may refer to a secret. Finally, the sequence of memory accesses are used to formulate a symbolic cache model . The model is satisfiable if a possible secret , accessed along a speculative path, remains in the cache after the program execution. This is because the presence of a speculatively accessed secret in the cache might result in ex-filtrating via a cache side-channel attack. The construction of the speculative execution revolves around the concept of speculative execution window (). Such a bounded window captures the number of instructions that a processor might speculatively execute beyond a branch before the outcome of the branch is resolved. We note that may span across multiple unresolved branch instructions.
At a broader perspective, Algorithm 1 intercepts each conditional branch instruction and explores all possible speculatively executed instructions from this branch. To this end, we compute . After handling a conditional branch , each element in is a possible sequence of memory accesses that might have occurred during a speculative execution from . Moreover, only records memory accesses that may refer to a secret. If memory accesses do not refer to secrets along speculatively executed paths, then they do not impose any risk related to the leakage of information. Algorithm 1 terminates when the time budget exceeds or KLEESpectre explores all (speculatively) executed paths and is constructed for every conditional branch . In the following, we will discuss some critical features of KLEESpectre .
In this work, we identify secrets as follows. For each memory-related instruction , we consider that accesses a secret if and only if points to an out-of-bound memory location. Although all such memory accesses may not refer to secrets, these memory accesses capture illegal accesses, a typical target for attacks exploiting speculative execution. Nevertheless, KLEESpectre can easily be configured for explicitly marked secret data, such as a secret key in an encryption routine. We use the function to capture whether some memory address is data-dependent on secret . Concretely, is true if and only if is data-dependent on the secret .
Algorithm 1 outlines the symbolic execution process embodied in KLEESpectre . Intuitively, KLEESpectre modifies the handling of branch instructions within a classic symbolic execution process. For each conditional branch , KLEESpectre maintains a structure . Upon encountering a conditional branch instruction , KLEESpectre explores all possible execution paths that might occur due to branch speculation. This is accomplished via the procedure ExpandSpecTree. Consider the symbolic state before the branch instruction is and the branch condition is . If captures the partial path condition before , then a speculative execution may proceed in the following two scenarios. Firstly, the true leg of the branch might be explored with the constraint . Secondly, the false leg of the branch might be explored with the constraint . These explorations are accomplished via the two calls to procedure ExpandSpecTree in Algorithm 1. Upon termination of ExpandSpecTree for a branch instruction , the structure contains the set of memory access sequences that depend on some secret. Therefore, these memory accesses are candidates that may leak secret information via cache side channel. Each memory access captures a triplet of the form where points to the instruction in the execution trace, captures the symbolic constraint with which was executed and captures the symbolic expression of the accessed memory address. Finally, KLEESpectre records all memory accesses that influence the cache state for memory blocks in . Thus, after termination of a symbolically executed path, each list contains all memory accesses that may replace a memory block accessed during the speculation at .
Algorithm 2 outlines the overall process of exploring the set of speculative execution paths. In summary, ExpandSpecTree performs the following operations. First, it explores all speculative paths until the speculation depth . We note that such an exploration may involve nested speculation. Secondly, while exploring the speculative paths, we record memory addresses for checking information leakage through the cache. These are the set of memory accesses that may depend on some secret . In our framework, we consider that any out-of-bound memory access along a speculative path points to a secret. Thus, the procedure ExpandSpecTree also records the potential secrets during exploration.
The execution of speculative state can be terminated in the following ways:
The speculation window expires. Since captures the maximum number of instructions that can be executed speculatively, we terminate the exploration of a speculative execution state after exploring instructions.
A memory fence instruction is executed. The memory fence can stop the speculative execution triggered due to branch mis-prediction.
An exception is raised. When an exception (e.g. divide by zero) is raised, the speculative execution terminates. This is analogous to the termination of normal execution.
Algorithm 1 satisfies the following crucial properties:
Consider an instance of the procedure call
ExpandSpecTree(, , , , ). Upon termination of this call, let us assume . During an arbitrary execution, further assume that the conditional branch was mispredicted and memory address was accessed speculatively. If is data-dependent on some secret, then for some . In short, is guaranteed to be an over-approximation of speculatively accessed memory addresses that are dependent on secret.
Consider after the termination of a symbolically executed path with the symbolic state . Let where is data-dependent on some secret. Assume captures the set of elements in the sequence post the element . If , then the memory block must be accessed following the access to for any concrete execution realizing the symbolic state .
In this section, we will model the cache behaviour of an execution path to check whether a secret remains in the cache after program execution. Note that our modified symbolic execution already takes into account the speculative execution semantics. Thus, the obtained execution path already accounts for memory references accessed speculatively. Concretely, the input to our cache model is any memory access sequence (see Algorithm 1) where is constructed for every conditional branch instruction . In the following, we show the cache modeling for a memory access sequence . Since is arbitrary, the same modeling principle is employed for all the memory access sequences recorded. Concretely, any memory access sequence is captured by a sequence of triplets as follows:
where is a memory-related instruction, is the symbolic constraint with which was executed and is the memory address accessed by . We note that can be accessed along a speculative path or a normal path (cf. Algorithm 1). Before discussing the cache model, we first explain the basic design principle behind caches.
Caches are fast memory employed between the CPU and the main memory. While accessing a memory location, the CPU first checks whether the memory location is cached. If the location is cached, then the CPU fetches the respective value from the cache. Otherwise, it accesses main memory and updates the cache with the accessed memory location and its value. The design parameters of a cache can be captured via a triplet: . captures the number of cache sets and captures the size of cache line (in bytes). Each cache set can hold cache lines while is called the associativity of the cache. For any memory-related instruction , let us assume it accesses the memory address . The address is mapped to the cache set . Since multiple memory addresses can be mapped to the same cache set, each cache line in a cache set stores a tag. This tag is identified via the most-significant bits of the memory address . Once a cache set is full (i.e. holds cache lines) and a new memory location is mapped to the same cache set, then a replacement policy is employed to evict a cache line and make room for fresh memory locations. In this work, we instantiate KLEESpectre for the least recently used (LRU) replacement policy. In LRU, the least recently accessed memory location in a cache set is chosen for eviction to accommodate fresh memory blocks. We define a cache set state as an ordered -tuple where the rightmost element captures the least recently used cache line. For example, in a two-way associative cache, the state captures that (respectively, ) is the least (respectively, most) recently used cache line.
In line with the preceding description of cache design, we will assume the following notations throughout the section:
The number of cache sets in the cache.
Size of the cache line.
The set of memory-related instructions accessing symbolic memory addresses (i.e. potential secrets accessed along speculative paths) in memory access sequence (cf. Equation 2).
The set of memory-related instructions exhibited along normal path in memory access sequence (cf. Equation 2). We note that holds.
Associativity of the cache.
Memory address accessed by instruction .
Cache set accessed by memory-related instruction .
Cache tag related to the memory-related instruction .
It is worthwhile to note that the symbolic address is defined to be a memory address that is dependent on a secret value. Moreover, as mentioned in the preceding section, we only consider secrets that might be accessed along speculative paths. Thus, if any address dependent on secrets remains in the cache after program execution, the respective program is vulnerable to Spectre attacks. The set of instructions accessing such symbolic addresses, i.e., were identified during our novel symbolic execution stage.
The symbolic model of the cache revolves around the notion of cache conflict. Intuitively, the phenomenon of cache conflict influences the states of each cache set. This, in turn, decides whether a value is cached during or after the execution. In the following, we first formally define the notion of cache conflict.
(Cache Conflict): Consider memory-related instructions and . Let (respectively, ) be the cache state immediately after (respectively, ) is executed. generates a cache conflict to only if is executed after and executing can influence the relative position of memory block within the cache state .
The preceding definition of cache conflict works for arbitrary memory-related instructions and . In KLEESpectre , however, our objective is to check whether any symbolic address remains in the cache. To this end, we only need to capture the cache conflict when and . The cache conflicts within normal paths and within the speculative paths are ignored. Similarly, we do not need to check whether a memory block accessed in normal path can be replaced via a memory block accessed along speculative paths. Thus, we can ignore the cache conflict when and . We formalize the aforementioned notion of cache conflict in KLEESpectre via the following definition:
(Cache Conflict in KLEESpectre ): Consider memory-related instructions and . For KLEESpectre , we consider a cache conflict from to if and only if generates a cache conflict to according to Definition 3 and and .
By considering the notion of cache conflict, as defined in Definition 4, we greatly simplify the size of the symbolic cache model and keep the overall complexity of KLEESpectre under check. In the next sections, we shall elaborate the crucial conditions required for the generation of cache conflicts and usage of such conditions to check the residency of a memory block in the cache. Subsequently, we build upon such conditions to formulate the symbolic model for identifying Spectre vulnerabilities.
: We note that due to the symbolic memory addresses, and can be symbolic expressions. Specifically, and are computed as follows:
: Our objective is to discover whether any symbolic memory address can be evicted from the cache after being accessed. As stated in Definition 4, KLEESpectre only considers cache conflict from memory accesses along normal path (i.e. set ) to the memory accesses along speculative paths (i.e. set ). However, it is not sufficient to check the cache conflict from () to () to precisely identify Spectre vulnerabilities. To check whether the conflict actually influences the relative position of the memory block till the end of the execution, we need to check whether the memory block accessed by can be reloaded after and before the end of the execution. If is reloaded after , then the cache conflict generated by is not propagated until the end of the execution. Finally, we need to check whether the memory block accessed by is replaced from the cache before the execution terminates. This is accomplished by checking whether the number of unique cache conflicts to that propagate till the end of execution exceeds the cache associativity (). In the following, we will model these phenomenon symbolically.
If generates a cache conflict to , then the following condition must hold: and access the same cache set, but have different memory-block tags. This is formalized as follows:
Additionally, we need to check whether is a unique cache conflict. To this end, we check that none of the memory accesses after accesses the same memory block as . Thus, we only account for the last memory-related instruction accessing the block . This is formalized as follows:
Finally, we need to check that is not reloaded after . Otherwise, the memory block accessed by will be reloaded to the cache and the conflict due to would be nullified. This is formalized as follows:
Combining Equations 5-7, we can obtain the symbolic condition where changes the relative position of the memory block accessed by and such a change in the relative position of the memory block is also propagated until the end of the execution. Thus, when all the conditions , and hold, we can say that the conflict generated by to is propagated until the end of the execution. This is symbolically captured as follows:
: We note that is arbitrary in the preceding discussion. To check whether the memory block accessed by can be replaced, we need to repeat the computation of and for any where is the total number of memory accesses in the trace. Finally, we need to check whether the collective sum of for exceeds the cache associativity. Let us assume that is true if and only if the memory block accessed by may remain in the cache after program execution, thus exhibiting a potential Spectre attack. The truth value of can be symbolically computed as follows:
: Finally, spectre attacks can be targeted for any memory-related instruction accessing a symbolic address. Therefore, Equations 5-10 need to account for all such symbolic memory accesses. Recall that captures the set of all memory-related instructions in the trace that access symbolic memory address. Thus, to check the possibility of Spectre attacks for an arbitrary (combination) of memory addresses, the following symbolic model is used:
We note that is true if and only if any of the symbolic memory address remains in the cache after program execution, thus leading to a potential spectre attack.
KLEESpectre is primarily implemented on top of the state-of-the-art symbolic execution engine KLEE v2.0 (Cadar et al., 2008). KLEESpectre is built from CLang v6.0 and it takes the LLVM bitcode generated with LLVM 6.0 as input. If a subject program contains external function calls, then the program is linked with KLEE- uClibc (1) first, before being passed to KLEESpectre . We used the SMT solver STP (Ganesh and Dill, 2007) to check the satisfiability of the path constrains and the symbolic cache model. Broadly KLEESpectre makes three major changes in KLEE: generating speculative symbolic states, propagating potentially sensitive data and symbolically modeling the cache behaviour.
A symbolic execution engine interprets a single instruction symbolically subject to the constraints imposed on the respective symbolic state. The initial symbolic state is constrained via the logical formula true. If the constraint imposed on the current symbolic state is and the engine encounters a branch instruction with condition , then traditional symbolic execution engines check the satisfiability of constraints and . If such a constraint is satisfiable, then the engine creates a new symbolic state with the constraint. The new state inherits the state before encountering the branch instruction, but proceeds interpreting the subsequent instructions independently. Our KLEESpectre
approach generates two extra symbolic states to model the speculative execution. These states are generated to model the speculative paths and they also model nested speculative execution. We also modify the path selection heuristic in KLEE to take into account the newly generated speculative symbolic states. Specifically, when the scheduler selects a normal stateto execute, we check whether the state may be immediately preceded by any speculative state. If such is the case, then KLEESpectre selects a speculative state to process. The normal state is not processed until all preceding speculative states of are handled. KLEESpectre can use all existing state selection strategies in KLEE, such as Depth First Search (DFS), Breadth First Search (BFS), random path selection (random-path) for both the normal state selection and speculative state selection.
KLEESpectre propagates the sensitive data along the execution path to identify the addresses that may leak the sensitive data to the cache state. When a memory load instruction reads a variable from an out-of-bound memory location, we mark as sensitive. All new expressions dependent on are subsequently marked sensitive as well. By tracking these sensitive expressions, we can detect if a memory access leads to the leakage of sensitive data. This is accomplished by checking whether the accessed memory address is constructed from any sensitive expressions.
Our KLEESpectre tool models the cache to further check whether a cache state impacted by a sensitive address can be observed in an execution point, in particular, at the termination of a program for the access-based cache side-channel attack. Each execution state contains a cache state that symbolically records the cache content along the execution path. The cache modeling of KLEESpectre collects all memory load and store addresses except the memory store addresses in a speculative execution. There exists multiple reasons for such a design choice. Firstly, the memory store is not visible to the cache until the speculatively executed instructions are committed in the real execution of a processor. Secondly, our assumption is that all speculative executions in KLEESpectre are caused by the branch mis-prediction and all speculatively executed instructions are rolled back. Upon the termination of an execution, the symbolic cache model is constructed in line with the explanation in Section 4.2 and we call the STP solver to check whether the sensitive address may still stay in the cache.
In this section, we perform the effectiveness evaluation of KLEESpectre in detecting the Bounds Check Bypass (BCB or Spectre variant 1) vulnerabilities. We aim to answer the following research questions:
RQ1: Can KLEESpectre effectively detect various kinds of BCB vulnerabilities?
RQ2: How efficient is KLEESpectre in detecting the BCB vulnerabilities?
RQ3: How effective is our cache model in detecting cache side-channel leakage through speculative path?
Note that BCB vulnerability has not been reported in the wild yet. Therefore, we first run KLEESpectre on the litmus tests created by Kocher (Kocher, 2018). These litmus tests are different types of Spectre vulnerable code patterns. Secondly, we run KLEESpectre on a set of security-critical benchmarks to check whether KLEESpectre can find the potential BCB vulnerabilities. Finally, we evaluate the effectiveness of our cache model in KLEESpectre by modifying the litmus tests and the security-critical benchmarks appropriately.
No real BCB vulnerability has been reported in the wild. So we first rely on fifteen litmus test programs with Spectre vulnerability created by Kocher (Kocher, 2018). We aim to check whether KLEESpectre can successfully detect these different variations of BCB vulnerabilities. These litmus tests were originally developed to evaluate the effectiveness of the Spectre mitigation in Microsoft C/C++ compiler. The Microsoft compiler uses static analysis to identify the vulnerable code fragments and inserts lfence to repair the vulnerable code. Kocher reports (Kocher, 2018)
that the Microsoft compiler can only identify two out of 15 vulnerable programs. This is because instead of using precise static analysis, Microsoft C/C++ compiler only performs a simple code pattern matching to identify code fragments related to the BCB vulnerabilities. In contrast,KLEESpectre can correctly detect all the BCB variants in 15 litmus tests produced by Kocher.
The programs used in litmus tests contain no memory access after the sensitive data is leaked and brought into the cache along the speculative path. As a result, our cache modeling has no impact on the detection results and all the litmus tests are correctly confirmed to contain Spectre vulnerability by KLEESpectre . Thus, we design additional experiments to showcase the power of cache modeling in KLEESpectre by introducing memory access code in the litmus test programs after the spectre vulnerability.
For evaluating our cache model, we use a 32 KB set-associative cache with the LRU replacement policy and each cache line has 64 bytes data. We configure the cache to be 2-way, 4-way or 8-way in our experiments. We mainly consider the PRIME + PROBE attack on L1 cache. This attack is used to target both data (Osvik et al., 2006; Percival, 2005) and instruction cache (Acıiçmez et al., 2010).
A modified litmus test code is outlined in Listing 3. The code contains a vulnerable function victim_fun() that receives an integer idx as an argument. The if statement at line 8 checks whether idx is less than the array1 size array1_size. If the condition holds, then idx is used to access array1. The code between line 8 and 10 exposes a typical BCB vulnerability. Specifically, if the branch at line 8 is mis-predicted, then the access of array1 with idx value greater than array1_size can bring in potentially sensitive data. This is because array1[idx] can point outside of array1 when the branch at line 8 is mis-predicted. The sensitive data can subsequently be leaked to the cache state by accessing array2 at line 9. The question remains whether the leaked data will still remain in the cache after completion of the program execution. This question can be answered by our cache modeling in KLEESpectre .
Thus, to test the effectiveness of the cache model in KLEESpectre , we add a loop at lines 11-15. The loop continuously brings in data to the different cache sets after the leakage of sensitive data at line 9. The memory accesses in the loop may evict the sensitive data introduced into the cache by BCB vulnerability (line 9) after N iterations. Each iteration brings a memory block to a different cache line, for example, the loop introduces a memory block for each cache set by the first 256 iterations and the entire cache is filled up after performing total 512 iterations for a 2-way (256 sets) cache. We run this litmus test program with different value of N from 1 to 512 to evaluate the effectiveness of KLEESpectre . Specifically, we aim to detect the eviction of the sensitive data from the cache for different values of N and different cache associativities (i.e. 2, 4 and 8).
The outcome of our findings is shown in Figure 3. The red solid line denotes that KLEESpectre can detect the sensitive cache state, which means the sensitive data is still in the cache after N memory accesses in the test code. In contrast, the green dash line indicates that the sensitive data has been evicted from the cache by the additional code (Leakage free). We can see from Figure 3 that the sensitive data is no longer present in the cache after 260, 288 and 452 memory accesses for cache associativity 2, 4 and 8, respectively.
The result in Figure 3 proves the effectiveness of KLEESpectre cache modeling. As an example, consider the code at line 9 in Listing 3. The data read by array1[idx] is one byte represented as . Thus, has a value between 0 and 255. The address of the memory access performed by array2 is captured via . As the least significant six bits of are used for the byte offset in the cache block (64 byte cache block), only two bits of are used for the two least significant bits of the cache set index. Thus, the address can map to one of four selected contiguous cache sets depending on the value of for any cache associativity. Thus to completely evict from the cache for arbitrary values of , we need access to 8, 16 and 32 corresponding caches lines for 2, 4 and 8-way associate caches, respectively.
As shown in Figure 3, for 2-way set associative caches, the leakage is undetectable after 260 memory accesses from the loop at lines 11-13. In a 2-way set-associative cache, can potentially map to four contiguous cache sets depending on the value of . Thus, if we want to guarantee the eviction of from the cache, then we need to fill up these contiguous four cache sets that may map to. In our experiment, was mapped to the first cache set. As a result, 260 memory accesses can completely fill up the first four sets of a 2-way cache. Specifically, the first 256 iterations of the loop (lines 11-13) access memory blocks mapping to all cache sets (256 cache sets for 2-ways cache) and the rest four iterations introduce the second memory blocks for first four cache sets. This guarantees the removal of from the cache for any value of . To the best of our knowledge, none of the existing tools such as oo7 (Wang et al., 2018) and SPECTECTOR(Guarnieri et al., 2018) can accurately verify the cache-side channel freedom against BCB attack like KLEESpectre .
|encoder||LibTomCrypt||encode binary data to ASCII string||134||14|
|salsa||Linux-tegra||Salsa20 stream cipher||279||20|
|str2key||openssl||Key preparation for DES||385||12|
Benchmark selection. To evaluate KLEESpectre on real programs, we select ten cryptography related programs from well known projects: libTomCrypt, hpn-ssh, openssl and Linux-tegra. Table 1 outlines some salient features of the subject benchmarks. All the benchmarks potentially process or contain sensitive data. All of these benchmarks were also used in a recent work (Wu and Wang, 2019) to perform the analysis of speculative execution via abstract interpretation. In Table 1, column LoC denotes the lines of code; the collected programs have 134 (encoder) to 1,838 (AES) lines of code. The column #Branch denotes the number of branches in each program ranging from 9 (seed) to 71 (chacha20). For all the benchmarks, we use the internal function klee_make_symbolic() of KLEE to set the input of the programs (e.g., the plaintext and the key in cryptography programs) as symbolic variables. All benchmarks are compiled by Clang-6.0 with ”-O1” optimization.
Experimental results. We run KLEE, KLEESpectre with SEW=50, and KLEESpectre with SEW=100 (SEW is the size of the speculative window in terms of the number of micro-instructions) on the benchmarks listed in Table 1 to compare the performance and the effectiveness of KLEESpectre to detect BCB gadgets. The results are shown in Table 2. The column Explored paths denotes the number of explored normal execution paths and the column Explored speculative path indicates the explored speculative execution paths by KLEESpectre .
In each category of KLEE, KLEESpectre 50 and KLEESpectre 100, column Analysis time provides the analysis time of the tool. We conduct our experimental evaluation on Intel Xeon Gold 6126 (15) running at 2.6GHz with 192GB memory. Intel Xeon Gold 6126 is equipped with 12 cores (24 threads) and 19.25MB shared last-level cache (LLC). The machine is running a Ubuntu 16.04 server with Linux kernel 4.4.
Both KLEE, KLEESpectre 50, KLEESpectre 100 complete the analysis within 69 seconds. More specifically, for most benchmarks, KLEESpectre 50 and KLEESpectre 100 have longer analysis time than KLEE; but the analysis time of KLEESpectre is still acceptable. For example, KLEE explores three paths of chacha20 in 0.50s, KLEESpectre 50 explores all three normal paths along with 12,392 speculative paths in 2s. Besides, KLEESpectre 100 always explores more speculative paths than KLEESpectre 50 because KLEESpectre 100 executes more instructions along any speculative path. Moreover, if KLEESpectre encounters branch instructions along the speculative path, then it generates new speculative states (nested speculative execution), resulting in managing larger number of symbolic states as compared to KLEE. Finally, the speculative execution might be terminated upon receiving an exception or the program exit event. The column Avg. #inst in Table 2 shows the average number of the instructions executed along the speculative path, which is close to the SEW value in most benchmarks (e.g., 47.49 and 95.25 for KLEESpectre 50 and KLEESpectre 100, respectively, while analyzing chacha20).
|Program||KLEE||KLEESpectre 50||KLEESpectre 100|
As for the detection result of BCB Gadgets, the detected number of vulnerable instructions are listed in columns VB, UC_VB, RS and LS. VB represents the number of vulnerable branches. The mis-prediction of such branches may result in the secret data to be loaded in the cache. The term UC_VB means that the vulnerable branch can directly be trained via the user controlled input. RS is the abbreviation of Read Secret. Specifically, RS means that the secret can be loaded after executing the respective instruction. LS is an abbreviation of Leak secret wherein an instruction can leak the secret loaded by RS instruction to the cache state. The columns VB, UC_VB, RS and LS in Table 2 are reported as the unique code locations and if one vulnerable code location appears in several speculative execution paths, the code location is only reported once.
We detect VB and RS in all the benchmarks. For example, KLEESpectre 50 found eight vulnerable branches in chacha20 but none of of them is user-controlled. Only the benchmark str2key contains a Leak secret (LS), which means that the secret can potentially be loaded to the cache and observed by the attacker.
Listing 4 shows a potential Spectre variant 1 vulnerability in the str2key function DES_set_odd_parity(). The loop iteratively reads the data pointed by *key and uses the data to index array odd_parity. A mis-prediction of the for loop condition may cause a speculative execution of a few more loop iterations than normal execution. This may lead sensitive data beyond the end of *key (i.e. beyond the size DES_KEY_SZ) to be loaded into the cache. The sensitive data can impact the cache state when it is used to access array odd_parity. Thus, the cache state can potentially be observed by the attacker through probing array odd_parity. The exact amount of the leakage depends on the number of iterations that can be speculatively executed. However, in its current state, KLEESpectre does not compute an exact quantification of the leakage.
We also compare the KLEESpectre result with oo7 (Wang et al., 2018) and show that oo7 can only detect data leakage in encoder and ocb3. This is because oo7 only identifies the user controlled branches as vulnerable branches. However, KLEESpectre assumes all branches can be mis-trained by the attacker; for example, the victim process and the attacker process may be scheduled to the same core and the attacker can directly mis-train the branch prediction (Canella et al., 2018; Evtyushkin et al., 2018).
|Program||KLEESpectre 100||KLEESpectre 100 with cache modeling|
The cache modeling of KLEESpectre can accurately check whether the leaked sensitive data can be observed by the attacker through cache side-channel attack. Our cache model is not invoked until some sensitive data leakage is identified along the speculative path. As we do not find any data leakage in our benchmarks, in this experiment, we insert several vulnerable functions to the benchmarks and check whether KLEESpectre can detect them. More specifically, we randomly choose three Spectre v1 variant functions suggested by Kocher (Kocher, 2018), then insert them to the start, middle and the end of each benchmark listed in Table 1. Then, we run KLEESpectre with cache modeling enabled. Each experiment is conducted over three runs for three different cache associativities: 2, 4 and 8.
Table 3 shows the test result of comparing KLEESpectre 100 and KLEESpectre 100 with cache model enabled (KLEESpectre 100_Cache). All inserted vulnerable code that leaks the sensitive data in the speculative execution path have been detected by KLEESpectre 100 (str2key contains one original leakage). More importantly, we observe that the number of vulnerable code fragments reduces when we enable the cache modeling. For example, three data leakage scenarios were identified in ocb3 without cache modeling; but only two of them were identified when cache modeling is enabled. The remaining two data leakages were identified as false positives. This means that the sensitive data loaded into the cache were subsequently evicted by other memory accesses. Moreover, we observe that the presence of information leakage might depend on the cache configuration, asserting further importance to the cache modeling embodied by KLEESpectre . For example, three data leakage scenarios were detected by KLEESpectre for 2-ways cache in aes; but only two leakage scenarios were detected when using the 4-ways or 8-ways cache configurations for the same aes benchmark.
The precision of KLEESpectre comes with the cost of solving the symbolic cache model. Thus both the analysis time and the solver time increase (as compared to the cache modeling being disabled). The time to solve the symbolic cache model depends on the number of memory accesses and the percentage of symbolic addresses. As observed from Table 3 that the percentage of symbolic addresses is relatively low (the maximum is 27.72% for encoder); thus the solver can finish within an acceptable time. Finally, except for hash and des, we did not observe a noticeable difference in analysis time with increased cache associativity. This means that our symbolic cache model scales well with respect to varying cache configurations.
Path explosion. Path explosion is a major challenge in the symbolic execution. KLEESpectre is based on symbolic execution, which does not scale to large programs while exploring all feasible program paths. In particular, KLEESpectre forks more paths than the classical symbolic execution for performing the speculative execution. However, only limited number of instructions are executed on the speculative paths in KLEESpectre , which is bounded by the Speculative Execution Windows (SEW). Thus, as observed in our evaluation, KLEESpectre has similar complexity with the classical symbolic execution. Moreover, the existing methods to alleviate the path explosion can also be used by KLEESpectre , for example, state merging (Hansen et al., 2009; Kuznetsov et al., 2012). Specifically, the speculative states can be merged similarly as the classical states when the control flows of a program merges. Besides, the symbolic execution can be guided by the low-cost static analysis in such a fashion that a static analysis can be performed to roughly locate the vulnerable code and prune the redundant paths during the construction of symbolic execution tree.
Precise modeling of program behavior. The program behavior running on the hardware may not be the same as it is in the symbolic execution. This is because KLEESpectre uses bitcode that may not replicate exactly the same behavior as the final binary code due to the compiler optimization. For example, the program may have more memory accesses when running on the hardware than it is during the symbolic execution due to the register spilling. However, KLEESpectre is designed as an over-approximation method that it captures all necessary memory accesses and detects all potentially secret leakage. This results in the absence of false negatives. In other words, KLEESpectre guarantees that all leakage in the real execution can be detected. However, KLEESpectre can generate false positives. For instance, the leakage detected by KLEESpectre may not be exploitable in the real hardware.
Spectre-style attack mitigation. Several approaches have been proposed to mitigate Spectre vulnerabilities (Carruth, 2018; Oleksenko et al., 2018; community, 2018; Wang et al., 2018; Guarnieri et al., 2018).
Speculative Load hardening (Carruth, 2018) (SLH) is a mitigation technique for Spectre variant 1, adopted by the LLVM compiler. SLH identifies the potentially vulnerable code fragments where memory accesses depend on the conditional branches and then inserts hardening instruction sequence to nullify the pointers that may leak the data. SLH hardens the RS stage of the vulnerable code, the secret data cannot be loaded to the cache during speculative execution after nullifying the crucial pointers. As SLH repairs the program at every conditional branch and the hardening instructions slow down execution, it introduces 36% performance overhead. Oleksenko et al. (Oleksenko et al., 2018) present mitigation of Spectre variant 1 attack by delaying the execution of the vulnerable instructions via introduction of artificial data dependencies instead of serialization instructions to stop speculative execution altogether. These methods lack accurate analysis and hence overestimate vulnerable code fragments leading to a significant performance overhead of the repaired programs.
Microsoft Visual C/C++ compiler (community, 2018) enables mitigation of Spectre Variant 1 through a compiler option that inserts ”lfence” serializing instruction at potentially vulnerable code. However, this technique successfully mitigates only 2 out of 15 Spectre litmus tests (Kocher et al., 2018).
oo7 (Wang et al., 2018) is the first work proposed to mitigate Spectre-style attacks via modeling speculative execution in static analysis. oo7 works on binary and leverages taint analysis to track the vulnerable branches and memory operations that lead to Spectre-style vulnerabilities. oo7 can effectively detect and fix Spectre-style attacks, but may still produce false positives due to conservative static analysis.
SPECTECTOR (Guarnieri et al., 2018) presents a principled approach using speculative non-interference in symbolic execution to discover data leakage. However, Spectector only finds whether some secret data has been speculatively accessed; it does not check the possibility of follow-up cache side-channel attack, which is what we achieved via our cache modeling.
Side-channel attack identification via cache modeling. Casym (Brotzman et al., 2019) presents a cache-aware symbolic execution to identify and fix cache side-channel vulnerabilities. Casym provides two cache models: the infinite cache model of caches with infinite size and associativity, and the age model that tracks the distance of a memory access from its most recent access. However, the description of the cache models in Casym are sketchy and hence prevents reproducibility. Besides, Casym does not consider speculative execution in its models. CACHEFIX (Chattopadhyay and Roychoudhury, 2018) is another cache side-channel verification tool that can detect and fix the attack via symbolic execution. It also targets cache timing attacks and does not consider speculative execution paths.
Abstract interpretation is a static analysis approach that has been effectively adopted for cache hit/miss modeling in Worst-Case Execution Time (WCET) estimation. Wu et al.(Wu and Wang, 2019) introduce abstract interpretation to side-channel attack detection by extending it to cover speculative execution. This approach targets timing based side-channel attack but does not handle Spectre attack. A similar approach is embodied CacheAudit (Doychev et al., 2015), however, the CacheAudit approach does not consider speculative execution semantics.
In summary, the previous works either do not model speculative execution or lack a precise cache model. KLEESpectre is the first work to integrate speculative symbolic execution with cache modeling.
We have presented a new software testing tool named as KLEESpectre to expose the micro-architectural features to the software testing. The micro-architectural features such as speculative execution and caches are generally ignored by traditional software testing. This hides the vulnerabilities caused by invisible micro-architectural behaviours when a program runs on the hardware. KLEESpectre makes these behaviours visible in the software testing via modeling the speculative execution and caches within the traditional symbolic execution. The experiment shows that KLEESpectre can effectively detect the BCB vulnerabilities and the cache model can make such detection more accurate. KLEESpectre is only a step forward to extend the foundation of software testing to systematically discover vulnerabilities dependent on micro-architectural features. Our tool also provides an open platform to extend our methodologies as more Spectre style attacks are being discovered. For reproducibility and further research in the area, our tool and the benchmarks are publicly available:
This research is supported by the National Research Foundation, Prime Minister’s Office, Singapore under its National Cybersecurity R&D Program (Award No. NRF2014NCR-NCR001-21) and administered by the National Cybersecurity R&D Directorate.
A decision procedure for bit-vectors and arrays. In International Conference on Computer Aided Verification, pp. 519–531. Cited by: §5.