Nathan Jones, Mindstorms Engineering, LLC
Worksheet for "Introduction to Hardware (In)Security with the ChipWhisperer-Nano"Introduction to ChipWhispererExercise 0: Get the necessary softwareUsing CW-NanoUsing simulated dataExercise 1: Program the CW-NanoExercise 2: Get a power traceUsing CW-NanoUsing pre-captured dataExercise 3: Plot a power traceSimple Power Analysis (SPA)Exercise 4: Differentiate MCU operationsDifferential Power Analysis (DPA)Exercise 5: Identify the effect of a single bit in an MCU's power traceExercise 6: Use Hamming weight (HW) to sort power tracesExercise 7: Compare two key guessesExercise 8: Find a mystery byteExercise 9: Use DPA to recover an AES keyClosing thoughts
This worksheet provides three different tracks depending on what hardware and software you have:
⚡️: Complete any exercise marked with this symbol if you have the ChipWhisperer-Nano device
💻: Complete any exercise marked with this symbol if you only have the ChipWhisperer Python library
📝: Complete any exercise marked with this symbol if you have neither (can be completed with just a pen and paper, though a spreadsheet application may make the math go faster)
Workshop track: ⚡️ / 💻
If you don't want to mess around with any hardware or software, then you can skip this exercise! Don't worry, you can still follow along with any exercises marked with a 📝.
Follow the instructions here to install the ChipWhisperer software for your operating system or for a virtual machine. This ensures that you have the necessary files and software to program the STM32 target on the CW-Nano.
It also installs the ChipWhisperer Python library and gives you access to a number of high quality Jupyter notebooks to continue your hardware hacking learning journey long after this workshop ends.
You'll now be able to follow along with any exercises marked with a ⚡️.
If you don't have the CW-Nano or have any trouble connecting to the CW-Nano you do have, then simply clone the chipwhisperer Git repository. Then, create a Python virtual environment and use requirements.txt to install the libraries we'll be using today (see below). You'll be able to follow along with any exercises marked with a 💻 or 📝.
xxxxxxxxxx$ git clone https://github.com/newaetech/chipwhisperer$ cd chipwhisperer$ python3 -m venv cwnano$ source cwnano/bin/activate$ git submodule update --init jupyter$ python -m pip install -e .$ python -m pip install -r jupyter/requirements.txtYou'll now be able to follow along with any exercises marked with a 💻.
I've installed the necessary software (or am opting to complete just the pen and paper exercises)
Workshop track: ⚡️
Open firmware/mcu/simpleserial-base/simpleserial-base.c in your code editor of choice.
In get_pt(), in between trigger_high() and trigger_low(), place the following code. Then save the file.
xxxxxxxxxxvolatile long int A = 0xE6A9;for(volatile int i = 0; i < 20; i++) A *= 2;Navigate to the folder eoc_files and start a Python session.
xxxxxxxxxx(cwnano) $ cd PATH/TO/eoc_files(cwnano) $ python3>>>Connect to the CW-Nano using the chipwhisperer Python library.
xxxxxxxxxx>>> import chipwhisperer as cw>>> scope = cw.scope()>>> target = cw.target(scope, cw.targets.SimpleSerial)>>> scope.default_setup()Compile the simpleserial-base program either from another terminal window
xxxxxxxxxx(cwnano) $ cd firmware/mcu/simpleserial-base(cwnano) $ make PLATFORM=CWNANO CRYPTO_TARGET=NONEor using the helper file after setting the value of FIRMWARE_ROOT in intro_to_hw_security.py.
xxxxxxxxxx>>> import intro_to_hw_insecurity as helper>>> helper.build("simpleserial-base")Flash the program also using the chipwhisperer Python library
xxxxxxxxxx>>> cw.program_target(scope, cw.programmers.STM32FProgrammer, "/PATH/TO/firmware/mcu/simpleserial-base/simpleserial-base-CWNANO.hex")or using the helper file.
xxxxxxxxxx>>> helper.flash(scope, "simpleserial-base")I programmed the on-board STM32 and saw
Verified flash OKin the Python REPL.
Workshop track: ⚡️ / 💻
Use get_traces from the helper file.
xxxxxxxxxx>>> mul2_loop, = helper.get_traces(scope, target, N=1)⚠️NOTE: If you see (ChipWhisperer Target WARNING|File SimpleSerial.py:454) Unexpected start to command: then this just means that the target was erroneously in the middle of processing a message when we started capturing our trace, which we can fix by resetting the target and then recapturing the trace.
xxxxxxxxxx>>> helper.reset(scope)>>> mul2_loop, = helper.get_traces(scope, target, N=1)Inspect the result.
xxxxxxxxxx>>> mul2_loop.wavearray([ 0.20703125, 0.30078125, 0.0546875 , ..., -0.0703125 , -0.15234375, 0.07421875])You can also do this manually.
Create a bytearray to send to the target.
xxxxxxxxxx>>> ktp = cw.ktp.Basic()>>> msg = ktp.next_text()>>> msgCWbytearray(b'64 1b c1 3c 36 57 e7 2d 0c 85 d5 59 16 56 0a ae')or
xxxxxxxxxx>>> msg = cw.util.CWByteArray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])Then run capture_trace, which arms the scope, sends our message, and stores the power trace (when triggered) along with the reply from the target.
xxxxxxxxxx>>> mul2_loop = cw.capture_trace(scope, target, msg)If you don't have the CW-Nano, import the pre-captured data.
Start by navigating to the folder eoc_files and starting a Python session.
xxxxxxxxxx(cwnano) $ cd PATH/TO/eoc_files(cwnano) $ python3>>>Then import the libraries and the data.
xxxxxxxxxx>>> import chipwhisperer as cw>>> import intro_to_hw_insecurity as helper>>> proj = cw.import_project("intro_to_hw_insecurity_data.zip")>>> mul2_loop = proj.traces[0]I've procured a power trace.
Workshop track: ⚡️ / 💻
Create a simple plot of the power trace using plot(trace, file) from the helper file. E.g.
xxxxxxxxxxhelper.plot(mul2_loop.wave, "mul2_loop.html")The helper file also provides the function plot_with_hover(trace, file), which adds a hovering textbox with x and y data for the nearest data point underneath the current mouse position.
These functions use holoviews to plot our traces, which you could use directly if you wanted (example below).
ximport holoviews as hvfrom bokeh.plotting import show, output_file
hv.extension('bokeh')output_file("mul2_loop.html")curve = hv.Curve(mul2_loop[0].wave)show(hv.render(curve))You can also use something like matplotlib to plot the power traces (example below), though holoviews handles large data sets better and has more natural controls for panning and zooming.
xxxxxxxxxx>>> import matplotlib.pyplot as plt>>> plt.plot(mul2_loop.wave)
# Optional: Annotate and customize the plot>>> plt.title("Sample power trace of CW-Nano")>>> plt.xlabel("Sample number")>>> plt.ylabel("Power")>>> plt.grid(True)
# Optional: Save the plot>>> plt.savefig('mul2_loop.png', bbox_inches='tight', dpi=300)
>>> plt.show()I've created a plot of my power trace.
Workshop track: ⚡️ / 💻 / 📝
Open up the plot you made in the last exercise (mul2_loop.html) and the 5 files named ex4_trace#.html (e.g. ex4_trace0.html, ex4_trace1.html, etc)
Match the power traces to the five code snippets below.
Empty
xxxxxxxxxx// No code between trigger_high() and trigger_low()Multiplication by 2
xxxxxxxxxxvolatile long int A = 0xE6A9;A *= 2;// 19 more timesDivision by 3
xxxxxxxxxxvolatile long int A = 0xE6A9;A /= 3;// 19 more timesTable look-up (16 times)
xxxxxxxxxxstatic const uint8_t sbox[256] = {0x63,0x7c,0x77 /* ... */};uint8_t key[16] = {0x1a, 0x4f, 0x1c, /* ... */};for(size_t idx = 0; idx < 16; idx++) pt[idx] = sbox[ pt[idx]^key[idx] ];Multiple or divide (16 times)
xxxxxxxxxxuint16_t key = 0x1a4f;for(size_t idx = 0; idx < 16; idx++){ if(key & (1<<idx)) pt[idx] = pt[idx]*2; else pt[idx] = pt[idx]/3;}Remember!
The scope starts collecting data immediately before the start of each math operation, but it will collect data well past when it finishes, continuing on with the rest of the application that's waiting for more incoming serial data. Look for the beginning of a similar, repeated feature at the end of every power trace which identifies the serial loop and, therefore, where each operation ends.
For each power trace, try to identify repeating features within the region where you think the operation is occurring. How many different features do you see? How many of each feature are there? These features correspond to specific operations happening inside the MCU. Each code snippet also has a unique set of operations. Which snippet of code seems to best match a trace with a given number of features and feature types?
A few notes about the microarchitecture of the STM32F0:
The STM32F042 on the CW-Nano does have a hardware multiplier (that completes in a single clock-cycle), but it does not have a hardware divider.
Multiplications/divisions by 2 are probably implemented with simple left-/right-shifts.
Conditional branches are very power-hungry (requiring a pipeline flush if the branch was mis-predicted).
Check Compiler Explorer to see what assembly instructions are actually being emitted for each piece of code!
Record your answers below.
- Empty corresponds to:
- Multiplication by 2 corresponds to:
- Division by 3 corresponds to:
- Table look-up corresponds to:
- Multiply or divide corresponds to:
A *= 2 vs A *= 3. Based on what you see, predict whether those two traces would look identical or different on the CW-Nano — and why. Then modify the firmware to use A *= 3 and check your prediction.
I think the traces will look
identical
different
What was the result?
A *= 2 and A *= 3 using SPA." Justify your answer.
True
False
LSL for ×2 and MUL for ×3, which are different instructions with different power profiles, even if both complete in one cycle.Workshop track: ⚡️ / 💻 / 📝
Get 2500 traces of the target performing the "table look-up" code from Exercise 4
For the CW-Nano (⚡️):
Program the target with the "table look-up" code.
Replace the "mul2_loop" code from Exercise 1 in with the table look-up code from Exercise 4. The complete definitions for the variables sbox and key are found in step 2 below (underneath the table of data).
Build and flash the program.
xxxxxxxxxx>>> helper.build("simpleserial-base")>>> helper.flash(scope, "simpleserial-base")Collect 2500 traces using the helper file
xxxxxxxxxx>>> traces = helper.get_traces(scope, target, N=2500)Software only (💻): Load the sample data
xxxxxxxxxx>>> traces = proj.traces[1:2501]Pen and paper (📝): Use the data below, which is a subset taken directly from the sampled data above. You can also find this data in ex5.csv.
| Trace # | In[0] | x = 22 | x = 23 | x = 24 | x = 25 | x = 26 |
|---|---|---|---|---|---|---|
| 83 | 0x20 | -0.003906 | -0.203125 | 0.261719 | -0.015625 | 0.183594 |
| 100 | 0xA5 | 0.039062 | -0.195312 | 0.277344 | -0.019531 | 0.183594 |
| 1472 | 0x13 | -0.003906 | -0.210938 | 0.277344 | -0.015625 | 0.1875 |
| 1515 | 0x71 | 0.039062 | -0.140625 | 0.433594 | 0.003906 | 0.246094 |
| 1611 | 0x67 | 0.054688 | -0.132812 | 0.425781 | 0.003906 | 0.214844 |
| 1710 | 0x71 | 0.035156 | -0.140625 | 0.421875 | -0.003906 | 0.214844 |
Create a variable in your Python REPL that matches the table and key from the code (below).
xxxxxxxxxx>>> sbox = [ # 0 1 2 3 4 5 6 7 8 9 a b c d e f 0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, # 0 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, # 1 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, # 2 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, # 3 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, # 4 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, # 5 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, # 6 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, # 7 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, # 8 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, # 9 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, # a 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, # b 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, # c 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, # d 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, # e 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16 # f]>>> key = [0x1a, 0x4f, 0x1c, 0x6f, 0x05, 0x99, 0x80, 0x71, 0xcd, 0x87, 0xbb, 0xee, 0xde, 0x95, 0x5e, 0x19]Create two lists of traces, one with all of the traces where the LSB of the first output byte was 0 (sbox[ textin[0] ^ key[0] ] & 1 == 0) and the other where the LSB of the first output byte was 1 (sbox[ textin[0] ^ key[0] ] & 1 == 1).
xxxxxxxxxx>>> ones = []>>> zeros = []>>> for t in traces:... if sbox[ t.textin[0] ^ key[0] ] & 1 == 1:... ones.append(t.wave)... else:... zeros.append(t.wave)Alternatively, you can use the textout part of each trace.
xxxxxxxxxx>>> ones = []>>> zeros = []>>> for t in traces:... if t.textout[0] & 1 == 1:... ones.append(t.wave)... else:... zeros.append(t.wave)Take the per-sample average of each group (i.e. avg_ones[0] is the average of sample 0 of every trace in the ones group), subtract one average from the other, and take the per-sample absolute value.
xxxxxxxxxx>>> import numpy as np>>> avg_ones = np.mean(ones, axis=0)>>> avg_zeros = np.mean(zeros, axis=0)>>> diff = np.abs(avg_ones - avg_zeros)You can also do steps 3 and 4 in a single line.
xxxxxxxxxx>>> import numpy as np>>> diff = np.abs(np.mean([t.wave for t in traces if sbox[t.textin[0]^key[0]]&1 == 1], axis=0) - np.mean([t.wave for t in traces if sbox[t.textin[0]^key[0]]&1 == 0], axis=0))Plot diff.
xxxxxxxxxx>>> helper.plot_with_hover(diff, "diff_bit0.html")Zoom in on the portion of the graph where the MCU is performing the table look-up (Review the techniques from Exercise 4 if you aren't sure how to locate that section of the graph!).
What's the magnitude of the largest peak?
At what sample number is it?
How much larger than the next largest peak is it?
I've plotted
diff.
There is a peak in the graph at sample # of magnitude . It's times larger than the next largest peak during the table look-up operation.
In your own words, explain the significance of the peak you are seeing.
sbox[textin[0]^key[0]]&1==1) test another byte, such as the second (sbox[textin[1]^key[1]]&1==1) or third (sbox[textin[2]^key[2]]&1==1). Does the peak change in size or position at all? Can you infer or confirm anything from this about how long each loop iteration takes or whether any of the bytes are better targets for DoM than any others?Workshop track: ⚡️ / 💻 / 📝
Using the traces from the last exercise, sort them based on whether the Hamming weight (the counts of 1s) of the first output byte was >4 or <=4.
xxxxxxxxxx>>> HW_gt4 = [t.wave for t in traces if sbox[t.textin[0]^key[0]].bit_count() > 4]>>> HW_le4 = [t.wave for t in traces if sbox[t.textin[0]^key[0]].bit_count() <= 4]Take their per-sample average and find their difference, then plot it.
xxxxxxxxxx>>> diff = np.abs(np.mean(HW_gt4, axis=0) - np.mean(HW_le4, axis=0))>>> helper.plot_with_hover(diff, "diff_HW.html")Considering the resulting graph:
What's the largest peak in the graph?
What's the magnitude of that peak?
How much larger than the next largest peak is it?
How do these values compare to those from the last exercise?
I've plotted
diff.
There is a peak in the graph at sample # of magnitude . It's times larger than the next largest peak during the table look-up operation. It's times larger than the peak from the last exercise.
In your own words, explain the difference between sorting based on LSB and by Hamming weight.
Workshop track: ⚡️ / 💻 / 📝
Assume you had an incorrect value for key[0] (pick one at random) and then sort the traces based on the LSB or HW of the output of the table look-up using that value.
As in Exercise 5, take the per-sample average of these two groups, subtract one from the other, and plot the result.
Compare the peak you found in Exercise 6 with the largest peak you can find in this graph.
I've plotted
diffbased on an incorrect key.
The largest peak in this graph has a magnitude of and is times larger than the next largest peak. The magnitude is times smaller than the peak from Exercise 6 and its ratio with the next largest peak is also times smaller than in Exercise 6.
key[0]. Record the peak magnitude for each. How much do the peaks vary between wrong guesses? What does this suggest about how confident you can be that any single runner-up is actually wrong?Workshop track: ⚡️ / 💻 / 📝
Get 2500 traces of the target performing AES encryption.
For the CW-Nano (⚡️):
Program the target with the "simpleserial-aes" firmware.
xxxxxxxxxx>>> helper.build("simpleserial-aes", crypto="TINYAES128C")>>> helper.flash(scope, "simpleserial-aes")Collect 2500 traces using the helper file
xxxxxxxxxx>>> traces = helper.get_traces(scope, target, N=2500)Software only (💻): Load the sample data
xxxxxxxxxx>>> traces = proj.traces[2501:5001]Pen and paper (📝): Use the data below, which is a subset taken directly from the sampled data above, and read "Hint for pen and paper solvers". You can also find this data in ex8.csv.
| Trace # | In[0] | x = 484 | x = 485 | x = 486 | x = 487 | x = 488 | x = 489 | x = 490 |
|---|---|---|---|---|---|---|---|---|
| 27 | 0xDF | -0.08984375 | 0.027344 | 0.242188 | 0.058594 | 0.011719 | 0.167969 | 0.05078125 |
| 898 | 0x57 | -0.13671875 | -0.03125 | 0.179688 | -0.011719 | -0.058594 | 0.121094 | 0.04296875 |
| 1048 | 0x1B | -0.15625 | -0.039062 | 0.167969 | -0.015625 | -0.074219 | 0.117188 | 0.03515625 |
| 1072 | 0x59 | -0.13671875 | -0.035156 | 0.183594 | -0.011719 | -0.054688 | 0.109375 | 0.02734375 |
| 1089 | 0x56 | -0.1015625 | 0.035156 | 0.269531 | 0.101562 | 0.042969 | 0.179688 | 0.05859375 |
| 1564 | 0x4A | -0.12109375 | 0.011719 | 0.230469 | 0.070312 | 0.019531 | 0.152344 | 0.05078125 |
| 1791 | 0x79 | -0.14453125 | -0.023438 | 0.175781 | -0.019531 | -0.066406 | 0.132812 | 0.046875 |
| 2398 | 0x27 | -0.13671875 | -0.03125 | 0.214844 | 0.066406 | 0.019531 | 0.152344 | 0.05078125 |
Conduct DoM for each of the 256 possible values of key[0]. In pseudocode:
xxxxxxxxxxFOR EACH value of key_guess, from 0 -> 255SORT traces based on LSB or HW of sbox[in[0]^key_guess]PLOT the DoM of the two sets of sorted tracesSTORE the peak value from the DoM graph during the operationThe BEST key guess is the one with the greatest peak value
Which key guess was associated with the largest peak?
The key guess with the highest peak was for key value .
key[0] and only the first byte of the S-box output. If AES used a simple identity function (no S-box; e.g. out[i] = in[i] ^ key[i]) instead of the actual S-box substitution, would the DoM attack still work? Would it be stronger, weaker, or about the same?out[i] depends only on bit j of in[i] and bit j of key[i], with no cross-bit mixing. This means the DoM signal from sorting by bit j contains information about only one bit of the key at a time. You'd recover the key one bit per sort rather than one byte per sort. The S-box's nonlinearity is what lets a single S-box output byte expose the entire byte of key: changing any one bit of the key input changes roughly half the output bits, so the correlation between HW(sbox[in ⊕ key]) and power is much stronger than the correlation between HW(in ⊕ key) and power.Workshop track: ⚡️ / 💻
Repeat Exercise 8 for all 16 bytes of the AES key.
The AES key is .
What are three things you want to remember about today's workshop?