Partial key recovery on STM32F4


Yesterday I’ve updated my installation (develop branch) and I’ve tested it by running a Hardware Crypto Attack, but I recovered the key partially.

Things were improved with changing number of samples from 3000 to 5000. Recovered 10-12 bytes, but only 4 were with big difference in correlation.
Using diff probe didn’t help at all. Recovered 6-10 bytes, but only couple with bigger correlation.

Any ideas?

PS I tried to run this notebook on v5.1.3, but because of previous bugs I never got a key.


This sort of thing happening isn’t super uncommon for hardware crypto since the actual encryption is a smaller part of spikes in the power trace. To recover the rest of the key, you have a few options:

  1. You can increase the number of traces you capture. This has the downside of making the analysis take longer
  2. This partial break actually lets you narrow down the attack to a single point, as you now know exactly where the encryption is occurring. This should make you more likely to find the key and will decrease the time the analysis takes
  3. Increasing the gain might help the signal to noise ratio a bit (just make sure the signal doesn’t start clipping where the encryption is occurring).

Let me know if that’s enough for you to recover the rest of the key,


Thanks Alex!

How do you find a place when encryption happens on the trace? Do you have a proper method other than trial-and-error?

Can I retrieve from analyzer a sample point when encryption happens?

What is the source of lack of correlation? Is it a byte value or positition? What happens if I change the key?

Hi 3,

Assuming you’re using ChipWhisperer Analyzer, simply use the correlation vs. time plot from a byte you’ve successfully broken:

import chipwhisperer.analyzer as cwa
import numpy as np
#... do attack
plot_data = cwa.analyzer_plots(results)
# get correlation vs time data for successful byte
output_v_time = plot_data.output_vs_time(successful_byte)[1]
encryption_location = np.argmax(output_v_time)

should work.


1 Like

Thanks Alex, that’s very useful. I’ve made a small change to print all locations for each byte.
The results are very interesting to me (I’m not windowing at all):
Uploading: locations2.png…

I see that not all bytes are focused in a same sample, so I windowed it to [1080, 1090] and got:

The results are very similar and if I select only one point [1082, 1083], I’m still not able to get the full key.

I’m taking 10k traces.

I cannot edit above post and it doesn’t contain images, so here they are:

First image

Second image

Hi 3,

One thing I completely forgot to think of was was to make sure you’re using the correct key for the analyzer plots (last_round_state_diff gets the last round key, not the first round).

Try the following code:

from chipwhisperer.analyzer.attacks.models.aes.key_schedule import key_schedule_rounds
last_round_key = bytearray(key_schedule_rounds(list(key), 0, 10))
results.known_key = last_round_key
plot_data = cwa.analyzer_plots(results)

import numpy as np
#... do attack
plot_data = cwa.analyzer_plots(results)
# get correlation vs time data for successful byte
output_v_time = plot_data.output_vs_time(successful_byte)[1]
encryption_location = np.argmax(output_v_time)


Ah, it’s fine Alex, because this tutorial does:

def process_key(key):
    recv_key = key_schedule_rounds(key, 0, 10)
    return recv_key

attack.process_known_key = process_key

When attack finishes do I have last round key and I have to roll it back? Or it’s already rolled for me?

Last round state diff will give you the last round key, so you’ll need to roll it back by flipping the arguments in the key_schedule_rounds call:

first_round_key = key_schedule_rounds(last_round_key, 10, 0)

It expects a list, so convert the last round key to one if it isn’t already. I do have an STM32F4 in the office that I can test the attack on to make sure there’s nothing weird with the tutorial, thought it might not be until next week that I can get to it.


Hi 3,

Sorry about getting to this so late. I’ve run the attack and confirmed that it fails to recover the 0th, 4th, 8th, and 12th bytes of the key. With ~30k traces, the rest of the bytes of the key are pretty distinct, but there’s nothing in those 4 bytes. The obvious culprit is the shift rows operation since it has no effect on those bytes, but it’s hard to say for sure.

Anyways, so if you plot the correlations of the broken bytes, you should see two peaks, one earlier one with lower correlation and one later one with higher correlation. The leakage for bytes 0, 4, 8, and 12 are in this earlier peak.


Hi Alex,

Thanks for your investigation. I also did some testing, and here are my conclusions:

Attack result for 3k traces and point_range = [1000, 1500]

print([ np.argmax(plot_data.output_vs_time(i)[1]) for i in range(16) ])

[133, 319, 484, 82, 396, 84, 82, 82, 82, 141, 82, 300, 316, 327, 179, 290]

but (added np.abs, before np.argmax)

print([ np.argmax(np.abs(plot_data.output_vs_time(i)[1])) for i in range(16) ])

[393, 77, 77, 82, 77, 78, 77, 77, 77, 77, 77, 77, 77, 77, 78, 301]

With that in mind, same 3k traces, I tested point_range = [1077, 1078]

Attack results:

The green-red chart shows a huge spike downwards at sample 1077. So there is a correlation, but it’s negative and attack doesn’t follow it.

I don’t understand why, but a workaround is to force attack to only one point.

Hi 3,

We’d actually expect the right correlation to be negative here, since we’re measuring a voltage drop across the shunt resistor (increased current here is decreased voltage, so everything’s inverted), so you definitely want the absolute value of the correlation in general.

You can also just run results.key_guess() to get the best key guess.


Are you saying that PGE table just prints it without absolute, but the key_guess would use absolute value?

The PGE table does print the absolute value of the correlation, sorry for the confusion. I just mentions since you mentioned taking the absolute value of the plot data.


Ah, okay, np :slight_smile:

Do you know why attack picks only positive correlation?

So you’re seeing the correct key only come out of the positive correlation part? That’s super interesting and not at all expected! We’re talking about making an “unsolved oddities” folder in the Jupyter directory, so we might have to throw that in there as well :slight_smile:


I used the PGE table to measure how many bytes were cracked.

It seems the PGE table presents positive correlation only.
But If I focus the attack to only one point, the PGE table presents much much better results.

So my conclusion was the attack picks only positive correlation and I asked this:

Oh, that makes sense. What the table is actually showing you is the absolute value of the correlation (or at least it should be). The software AES attack wouldn’t work very well if this wasn’t the case, since the highest correlation for that is negative.