CVE-2017-5753, commonly known as Spectre, is a side-channel attack which exploits the speculative execution processes performed by modern CPU’s. 

So what do these terms mean?

Side-channel attacks

A side-channel attack is an attack based on information gained from the actual implementation of a computer system, rather than weaknesses in an implemented algorithm.
Information can be gathered from timing information, power consumption and EM leakage.
An obscure example of this is the work of Mordechai Guri, the head of R&D at the Ben-Gurion University of the Negev, Israel. Guri has developed a technique for using RAM sticks to act as a wireless receiver by manipulating the electrical current inside the RAM producing electromagnetic waves in the 2.4GHz Wi-Fi range (source), thus side-channelling information.

Speculative execution

All modern microprocessors use speculative execution to increase performance at conditional branch instructions due to the relatively large amount of time for RAM access.
The execution path is predicted based on the previous history of similar branch instructions.
To increase performance, instructions after the branch may be executed before determining whether that branch will need to be executed. If the prediction is correct, then an increase in performance is observed. 

How does CVE-2017-5753 work?

Putting it simply, it comes down the following function:

Vulnerable victim code

The ‘if’ conditional checks whether the size of array1 is less than the value of ‘x’ that is given to it.

As this information isn’t readily available to the CPU (not currently stored in the CPU cache) the second line of code is run speculatively while the data is retrieved from memory. 

The second line of code first reads the data in array1[x]. As this is larger than the size of array1, the data read from out-of-bounds memory.
This data is then multiplied by 512 and the resulting value is used to read that location in array2. 

When the size of array1 is received from memory and now knows that the if statement is false, the CPU reverts its state to before the speculative execution. This is how processors are supposed to function. 

However, the result from the speculatively executed line of code is stored in the CPUs cache memory. 

If a second function was to iterate through each value of ‘x’ and read “array2[x * 512]” the value that returns fast is the value that was stored in the cache memory and is also the value that was read out of bounds. 

Using this it is possible to read memory locations containing secret information including encryption keys and account credentials. 

Analysis of the source code

The source code for the Spectre exploit will be analysed with each function explained in detail. 

The source code for the Spectre exploit is available here.

The source code can be compiled using GCC with the following command:
‘gcc -std=c99 spectre.c -o spectre’

Imports

The first section of code contains the imports. 

The imports stdio.h, stdint.h and string.h are fairly standard imports and are used in a large amount of C programs.  

The imports intrin.h for the Visual Studio compiler and x86intrin.h for other compilers are used to include the files for the ’rdtscp’ and ’clflush’ functions. The rdtscp function is used to read time stamp counter data and clflush is used to flush the cache lines. 

The line ‘pragma optimize(“gt, on”)’ is specified for the Visual Studio compiler which allows optimizations on a function-by-function basis. The g and t flags are for global optimizations and fast sequences of machine code. 

Imports list

Shared memory space

The variable ‘array1’ which is located at line 16 in the source code represents the shared memory space that is accessible by both the attacker and the victim functions.  This array is surrounded by two (2) unused arrays at lines 15 and 34 with an allocated size of 64 bytes each. These arrays are used to ensure different cache lines are hit.  On many processors, including the Intel i7 used in this test, the L1 cache has 64 bytes per line.

The variable ‘array2’ is defined with ‘256 * 512 bytes’ set as its array size. 

Creating arrays

Victim code

Within the victim code section at line 37, a char* named ’secret’ is declared and assigned our secret.

This has been set to ’YOU DONT HAVE TO BE PERFECT TO BE AWESOME.’ for this test, but can be changed to any string. 

The original spectre proof of concept had this variable set to ‘The Magic Words are Squeamish Ossifrage.’

This secret is only known to the victim function and the attacker function will attempt to recover using the spectre exploit, which includes the speculative execution of the victim function to read within the secret variable and the side-channel attack of the character read.

Important note: Within the proof of concept, the victim and attacker processes share the same process and therefore the same memory. This is to make the proof of concept simpler to run but still demonstrates the exploit in regards to retrieving the secret using side-channel attacks.

Following the secret, an unsigned int is declared, ’uint8 t temp = 0;’. As the comments states ’Used so the compiler won’t optimize out victim function()’, this is presumably to stop the compiler from removing victim function during the optimisation process. 

Following this is the ’victim_function’ function. 

Victim function

This function is explained in the Spectre white paper, page 5, at (https://spectreattack.com/spectre.pdf).

While the value of ’x’ is larger than array1 size, a vulnerable processor could do the following:

  1. Read ‘array1 size’. This results in a cache miss and slow memory retrieval.
  2. During the fetch of ’array1 size’, which is slow due to the cache miss, the branch predictor will incorrectly predict that the conditional will result in true.
  3. ‘array1[x]’ is speculatively read and ‘array2[array1[x] * 512]’ is calculated and read.
  4. During the read of ‘array2[array1[x] * 512]’, the read of ’array1 size’ is completed.
  5. The processor now knows it incorrectly predicted the conditional and reverts to its previous state.

While the processor reverts its state to before ‘array2[array1[x] * 512]’ is executed, this value is stored in the CPU cache and can be accessed in a side-channel attack.

Analysis code

The next line of code is the ’#define CACHE HIT THRESHOLD (80)’. This is simply the time threshold of determining if the CPU cache is hit.

Next, at line 53 in the source code, is the ’readMemoryByte’ function. 

This performs a timing attack by testing the time to access the tested value. The number of cache hits is stored in a table named ‘results’. The two best guesses after the loops have finished are returned by the function to main, which prints the results to the console.

At lines 60 and 61 the ’results’ table is built with 256 values. This is due to a single byte having 256

possible value variations (28 = 256).

The cache attack starts at line 65.

To start from a known, clean state, the entire of the ’array2’ table is flushed using ‘_mm_clflush’.

This table is accessible by both the victim and the attacker. This consists of 256 cache lines, with each line consisting of 512 bits.

Flush array code

The next session of code trains the branch predictor to incorrectly predict the conditional is true.

First, the ’array1 size’ is flushed. Then an empty loop is called as a delay, this is presumably to ensure the cache has finished being flushed. Next, ’x’ is computed and the victim function is called with ’x’ as an argument.

Branch predictor training code

For the branch predictor to be trained to predict incorrectly, ’x’ is generated five times

as a small value, which will train the branch predictor to assume that each value ’x’ will likely be

resolved as true, but during large ’x’ values, the ’victim function’ will incorrectly, speculatively resolve the conditional as true. 

Therefore, on each sixth iteration, the branch predictor will speculatively execute the conditional, allowing an out-of-bounds memory read.

By adding ‘printf(“x = %d\n”, x);’ at line 82, the value of ’x’ is printed on each iteration. As shown in the screen capture, the value of ’x’ is set at a low value for 5 iterations to train the branch predictor, followed by a large number to speculatively read out-of-bounds, as expected.

The large value of ‘x’ is calculated to land within the memory location of our ‘secret’ variable. 

 

Printing x values to console

The next section of code is used to measure the time needed to access the byte in cache memory. As the comment suggests, the bytes are checked semi-randomly to ensure the processor does not optimise readings, which would interfere with the timing calculations for the timing attack. 

As ‘array2[array1[x] * 512]’ was speculatively executed in the victim function and the result stored in the cache memory, by iterating through all 256 possible values of ‘x’ for ‘array2[x * 512]’, whichever value is returned fast (indicating a read from cache) was likely the same value read out-of-bounds and is given a +1 score. 

This is repeated 999 times to produce a final score.

Timing code

The rest of the readMemoryByte function selects the two values with the most cache hits and returns them.

Main

The majority of the main function is printing results to the screen and passing values to the analysis function.

The first line of code of interest is ‘size t malicious x = (size t)(secret – (char *)array1);’. This line calculates the offset to the ’secret’ variable from the ’victim function’. To read the secret, then n number of bytes need to be stepped over to read the correct memory location. It is trivial to calculate the offset by using ’offset = (length of secret) – array1’. This is stored as ’malicious x’. The exploit also allows the use of two arguments in the code section starting at line 124 by increasing the number of bytes read, it is possible to read other data in memory. An example of this is reading the string ‘Putting ‘%s’ in memory, address %p’ as shown below.

Compiling and running this exploit on a vulnerable system, the analysis code returns in sequence each byte of the ‘secret’ variable in memory and converts the value to the appropriate Ascii character as shown below.

Spectre exploit output

 

Compiling exploit code

Using GCC

`gcc -std=c99 spectre.c -o spectre`

Using Visual Studio

Create a new empty project and add spectre.c then select Build.

Executing compiled code

Testing

`.\spectre.` with no params.

Read from address

`.\spectre {address} {length}`
with params:
* address – pointer address of victim char *
* length – length of char *

Spectre paper: https://spectreattack.com/spectre.pdf
Spectre exploits info: https://spectreattack.com
CVE-2017-5753: http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=2017-5753
CVE-2017-5715: http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=2017-5715