Jason Snell

@syne

Exploiting Spectre with Deep learning

January 9th 2018

Google’s Project Zero dropped a bombshell on the security community last week detailing CPU bugs affecting Intel, AMD, and ARM architectures going back as far as Pentium Pros. Yup, the chip with a mountain of cache and immortalized by Weird Al.

There are actually a few variants of the bugs, collectively named Meltdown and Spectre, detailed in the official bug report. Interestingly, the last line of the bug summary states:

All PoCs are written against specific processors and will likely require at least some adjustments before they can run in other environments, e.g. because of hardcoded timing tresholds[sic].

Indeed, hardcoding values is never a good idea as it often leads to inflexible code. This post explores how we might learn these thresholds with the help of deep learning. But first, let’s review how Spectre works.

Spectre — or how to take advantage of CPU optimizations

If you’re looking for the full explanation of this clever exploit, you can find the paper here. In summary, Spectre takes advantage of two very popular CPU optimizations: branch prediction and speculative execution. When a CPU executes a bit of code that includes a branch attached to a memory value, since retrieving this value from RAM may take some time, the CPU may elect to execute past the branch. In higher level programming languages, branches are usually the result of conditional statements such as if … then …. When the value in RAM finally arrives, the CPU throws away the executed code if it happened to be wrong in its prediction.

This speculative execution has the side effect of copying RAM the CPU reads into cache and if this memory belongs to another process or, worse, the kernel, it will be cached before modern security systems realize the memory access is forbidden. Thus, malicious code can trick the CPU into caching privileged memory and then read out the cached value via a side channel timing attack. This sort of attack exploits timing differences in how long a CPU takes to fetch values from RAM (slow) vs on-die CPU cache (fast). As the bug summary mentions, every CPU will exhibit different timing behavior. This is where deep learning comes in.

Deep Learning CPU Cache Timings

To read the byte value of a privileged memory address, our proof of concept code attempts to read every possible 8 bit value at a given memory address — all 256 of them. We can’t directly read the value because the kernel won’t let us, but we can observe how long each read took. If a read is quick, chances are we hit the right value. Unfortunately, this isn’t always true as some values are always a bit faster to read (for example, 0 and 1) so assuming the fastest can lead to misread values. Let’s see how we can use a deep learning model to improve read accuracy.

The full proof of concept code can be found here and I’ll include only the most relevant bits from here on. To get started, we’ll build our training data using a string of possible byte values and their observed cache timings:

data_x = np.zeros([n_samples, 256])
data_y = np.zeros([n_samples, len(unique_chars)])
for i in range(n_iterations):
for j, val in enumerate(secret_chars):
data_x[i * n_chars + j, :] = deep_spectre.train(j)
data_y[i * n_chars + j, char_to_idx_map[val]] = 1

data_x contains our training data and data_y contains our target data. Each row of data_x represents an observation of all 256 cache read timings for a single byte value. Each row of data_y contains the byte value, one hot encoded. Neural networks are typically sensitive to large values so we’ll normalize (0 mean, unit variance) the timings using Scikit Learn’s handy scaler. We’ll also split the training and target data into training and validation sets. The validation set contains data the model never sees while training and provides a reasonable metric of how well the model will perform in the wild:

scaler = StandardScaler()
data_x = scaler.fit_transform(data_x)
x_train, x_test, y_train, y_test = train_test_split(data_x, data_y)

Now for the exciting part — let’s build a simple deep model. We’ll use the Keras API to wrap TensorFlow and build a four layer densely connected neural network. If you’re curious about neural architecture, I’d encourage you to check out the other layer types Keras has to offer.

model = Sequential()
model.add(Dense(200, input_shape = (256,), activation = ‘relu’))
model.add(Dense(150, activation = ‘relu’))
model.add(Dense(100, activation = ‘relu’))
model.add(Dense(len(unique_chars), activation = ‘softmax’))
model.compile(loss = ‘categorical_crossentropy’,
optimizer = ‘adam’,
metrics = [‘accuracy’])
model.fit(x_train, y_train, 
batch_size = 32,
epochs = 10,
validation_data = (x_test, y_test))

The call to model.fit() will start the training process. If all goes well, our model should achieve above 90% accuracy. Running this on my machine yields:

Train on 48000 samples, validate on 16000 samples
Epoch 1/10
48000/48000 [==============================] — 4s 83us/step — loss: 2.9168 — acc: 0.3363 — val_loss: 0.7985 — val_acc: 0.8276
Epoch 2/10
48000/48000 [==============================] — 4s 73us/step — loss: 0.4543 — acc: 0.9007 — val_loss: 0.3505 — val_acc: 0.9204
Epoch 3/10
48000/48000 [==============================] — 4s 75us/step — loss: 0.2802 — acc: 0.9367 — val_loss: 0.2825 — val_acc: 0.9335
Epoch 4/10
48000/48000 [==============================] — 3s 73us/step — loss: 0.2516 — acc: 0.9441 — val_loss: 0.2948 — val_acc: 0.9293
Epoch 5/10
48000/48000 [==============================] — 4s 73us/step — loss: 0.2368 — acc: 0.9451 — val_loss: 0.2640 — val_acc: 0.9361
Epoch 6/10
48000/48000 [==============================] — 4s 73us/step — loss: 0.2320 — acc: 0.9460 — val_loss: 0.2765 — val_acc: 0.9360
Epoch 7/10
48000/48000 [==============================] — 3s 73us/step — loss: 0.2405 — acc: 0.9458 — val_loss: 0.2588 — val_acc: 0.9376
Epoch 8/10
48000/48000 [==============================] — 4s 74us/step — loss: 0.2324 — acc: 0.9468 — val_loss: 0.2502 — val_acc: 0.9403
Epoch 9/10
48000/48000 [==============================] — 4s 73us/step — loss: 0.2269 — acc: 0.9474 — val_loss: 0.2452 — val_acc: 0.9408
Epoch 10/10
48000/48000 [==============================] — 3s 72us/step — loss: 0.2277 — acc: 0.9467 — val_loss: 0.2663 — val_acc: 0.9392

Now that we have a trained model, we can use it to predict unknown byte values given only cache timings. For example, let’s read a 40 byte secret string:

secret_len = 40
x_message = np.zeros([secret_len, 256])
for i in range(secret_len):
x_message[i, :] = deep_spectre.read(i)

After passing x_message to model.predict(), we simply take the output neuron with the highest probability via np.argmax() and map it back to a byte value:

y_pred = model.predict(scaler.transform(x_message))
pred_chars = np.argmax(y_pred, axis=1)
message = ‘’.join(list(map(lambda x: chr(idx_to_char_map[x]), pred_chars)))
print(“The secret message is:”, message)

If everything went according to plan, we should see:

The secret message is: The Magic Words are Squeamish Ossifrage.

Packaging these deep learning frameworks with an exploit would make for a prohibitively large payload. However, it may be possible to employ TensorFlow Lite to reduce the size of the model. It’s certainly not difficult to imagine a new breed of neural exploits armed with deep learning models to overcome idiosyncrasies found across CPU architectures and software versions.

Of course, this remains purely speculative ;)

More Related Stories