SPECTRE.VARIANT1

Potential exploit of speculative execution

Spectre is a hardware vulnerability discovered and published by Google in early 2018 (https://googleprojectzero.blogspot.ca/2018/01/reading-privileged-memory-with-side.htm). The vulnerability results from hardware optimizations aiming at improving CPU performance while it's working with the different memory levels. Although spectre is a hardware vulnerability, for it to be exploited, certain code patterns still should be present in the software that is being run by that hardware. The original Google-published paper is available at https://spectreattack.com/spectre.pdf and shows a number of patterns exposing the vulnerability, as well as a sample program allowing to illegally access the secret data leaked via it. There are different variants of the vulnerability; the variant 1 that is excessively discussed in the original paper is of the main interest for static detection since, unlike other variants, it does not seem to be easily mitigated by general approaches and requires specific code patterns detection in the source. The variant 1 vulnerability allows to leak secret data from the victim's machine RAM via an array out-of-boundary reads. Below is the simplified code snippet demonstrating the high level overview of this vulnerability:

Copy
   struct array {
       unsigned long length;
       unsigned char data[];
   };
    
   struct array *createArrayOfSize(unsigned int size);
    
   void vulnerable_pattern(unsigned long offset)
   {
      struct array *arr1 = createArrayOfSize(5);                  /* small array */
      struct array *arr2 = createArrayOfSize(0x500);              /* a large array */
      unsigned char secret_value;
   
      if (offset < arr1->length) {                                /* untrusted data controls the branch */
          secret_value = arr1->data[offset];                      /* out of boundary read in a mispredicted speculative execution */
   
          unsigned char value2 = arr2->data[secret_value * 256];  /* arr2->data[secret_value * 256] will be loaded to the CPU cache */
                                                                  /* resulting in a measurable side effect */
          /* some more code */
      } else {
          /* code for the else branch */
      }
  }
The code is generally secure since 'arr1->data[offset]' read is protected by the bounds check. However, this protection can be bypassed due to the hardware vulnerability. In order for the attack to be successful, a number of conditions need to be satisfied.
  • The secret data resides in an out-of-boundary memory area of array arr1->data
  • This secret data (that the attacker does have legitimate access to) is supposed to have been accessed by the CPU recently such that it actually is cached in one of its registers
  • 'offset' variable is also supposed to be in the CPU cache
  • arr1->length and arr2->data , to the contrary, are supposed to be uncached.
Once the above is satisfied, the attack can proceed as follows:
  1. At line 14, the control needs to compare 'offset' with 'arr1->length' .
  2. 'offset' is a cache hit, yet 'arr1->length' is a cache miss. CPU requests it to be loaded from the next level cache (or DRAM).
  3. It typically takes up to several hundred CPU cycles for the value of 'arr1->length' to appear in one of the CPU registers. Instead of waiting and thus wasting these cycles, the CPU starts executing the branch speculatively. The particular branch to be executed is chosen by the CPU branch predictor based on the past executions of this code. Since the most common legitimate calls of this function involve valid in-bounds values of 'offset' , the most probable branch will be the 'true' one. The branch predictor can also be 'trained' by the attacker to choose the branch they want. For the speculative execution, if, by the time 'arr1->length' is loaded into the CPU register, the branch predictor turned out to mispredict the branch, the CPU would stop executing the rest of the instructions in that branch and just execute the other branch.
  4. During the speculative execution, while the value 'arr1->length' is being loaded into the CPU L1 cache, the control reaches line 15.
  5. The maliciously chosen value 'offset' points to the secret data. That data read is a cache hit, and becomes immediately available to be stored in the 'secret_value' .
  6. 'arr2->data[secret_value * 256]' read at line 17 is a cache miss. It is requested to be loaded from the next level cache (or DRAM).
  7. The value 'arr1->length' finally arrives in the L1 cache. The CPU realizes the branch was mispredicted, its execution is halted, and control starts executing the 'else' branch instead.
  8. Value of 'arr2->data[secret_value * 256]' arrives in the L1 cache.

While no data is still accessible directly to the attacker, as a result of this execution, they leave a side effect in the CPU, i.e., the 'arr2->data[secret_value * 256]' value has been copied into the CPU L1 cache while other elements of 'arr2->data' array are still only available from the next level caches or DRAM (if extra details of hardware memory operations are ignored). To measure this side effect and finish data leaking, the attacker only needs to read the elements of 'arr2->data' one by one (with a step of 256 and yes, it is assumed that they have some convenient ways of doing so) and measure how long each read takes (this is claimed to be something easily achieved; see Timing Attack). All the reads except for the 'arr2->data[secret_value * 256]' will be slow while the read of 'arr2->data[secret_value * 256]' will be fast, and that reveals the value of the 'secret_data'.

The vulnerability can be mitigated by calling a special 'intrinsic' function anywhere in the branch before the access to 'arr2->data[secret_value * 256]' that forces the CPU to wait until all the values requested from the next level caches actually arrive in their registers. Intel compiler provides the function '_mm_lfence()' to achieve this on Intel architectures.

Code example

In the snippet above, Klocwork reports the following defect:
code.c:17:9: [SPECTRE.VARIANT1] Potentially untrusted data 'offset' can be used to read arbitrary data beyond boundary of array 'arr1->data' at line 15 in speculative branch execution.
Fixed code example
Copy
   struct array {
       unsigned long length;
       unsigned char data[];
   };
    
   struct array *createArrayOfSize(unsigned int size);
    
   void vulnerable_pattern(unsigned long offset)
   {
      struct array *arr1 = createArrayOfSize(5);                  /* small array */
      struct array *arr2 = createArrayOfSize(0x500);              /* a large array */
      unsigned char secret_value;
   
      if (offset < arr1->length) {                                /* untrusted data controls the branch */
          _mm_lfence();                                           /* serialization intrinsic that stops speculative execution */
          secret_value = arr1->data[offset];                      /* no vulnerability exists so no defect is reported */
   
          unsigned char value2 = arr2->data[secret_value * 256];  
                                                                  
          /* some more code */
      } else {
          /* code for the else branch */
      }
  }
Currently, there are following fence operations supported:
  • _mm_lfence
More intrinsics can be added via the newly introduced FENCE kb. The FENCE kb syntax is as follows:
function_name - FENCE

The above record will be interpreted by the checker that function 'function_name' will cause the CPU to wait until the cache is synchronized with the RAM state or make the data leakage in speculative executive a non-issue in any other way. Analysis along that path will be stopped.

Security training

Application security training materials provided by Secure Code Warrior.