Part 4, Topic 2: CPA on Firmware Implementation of AES¶
SUMMARY: By now, you'll have used a DPA attack to break AES. While this method has its place in side channel attacks, it often requires a large number of traces to break AES and can suffer from additional issues like ghost peaks.
We've also learned in the previous lab that there is a very linear relationship between the hamming weight of the SBox output and the power consumption at that point. Instead of checking average power consumption over many traces to see if a guessed subkey is correct, we can instead check if our guessed subkey also has this linear relationship with the device's power consumption across a set of traces. Like with DPA, we'll need to repeat this measurement at each point in time along the power trace.
To get an objective measurement of how linear this relationship is, we'll be developing some code to calculate the Pearson correlation coefficient.
LEARNING OUTCOMES:
- Developing an algorithm based on a mathematical description
- Verify that correlation can be used to break a single byte of AES
- Extend the single byte attack to the rest of the key
Prerequisites¶
This notebook will build upon previous ones. Make sure you've completed the following tutorials and their prerequisites:
- ☑ Part 3 notebooks (you should be comfortable with running an attack on AES)
- ☑ Power and Hamming Weight Relationship (we'll be using information from this tutorial)
AES Trace Capture¶
Our first step will be to send some plaintext to the target device and observe its power consumption during the encryption. The capture loop will be the same as in the DPA attack. This time, however, we'll only need 50 traces to recover the key, a major improvement over the last attack!
Depending what you are using, you can complete this either by:
- Capturing new traces from a physical device.
- Reading pre-recorded data from a file.
You get to choose your adventure - see the two notebooks with the same name of this, but called (SIMULATED)
or (HARDWARE)
to continue. Inside those notebooks you should get some code to copy into the following section, which will define the capture function.
Be sure you get the "✔️ OK to continue!"
print once you run the next cell, otherwise things will fail later on!
SCOPETYPE = 'CWNANO'
PLATFORM = 'CWNANO'
CRYPTO_TARGET = 'TINYAES128C'
VERSION = 'HARDWARE'
allowable_exceptions = None
SS_VER = 'SS_VER_2_1'
if VERSION == 'HARDWARE':
#!/usr/bin/env python
# coding: utf-8
# # Part 4, Topic 2: CPA on Firmware Implementation of AES (HARDWARE)
# ---
# **THIS IS NOT THE COMPLETE TUTORIAL - see file with `(MAIN)` in the name.**
#
# ---
# First you'll need to select which hardware setup you have. You'll need to select a `SCOPETYPE`, a `PLATFORM`, and a `CRYPTO_TARGET`. `SCOPETYPE` can either be `'OPENADC'` for the CWLite/CW1200 or `'CWNANO'` for the CWNano. `PLATFORM` is the target device, with `'CWLITEARM'`/`'CW308_STM32F3'` being the best supported option, followed by `'CWLITEXMEGA'`/`'CW308_XMEGA'`, then by `'CWNANO'`. `CRYPTO_TARGET` selects the crypto implementation, with `'TINYAES128C'` working on all platforms. An alternative for `'CWLITEXMEGA'` targets is `'AVRCRYPTOLIB'`. For example:
#
# ```python
# SCOPETYPE = 'OPENADC'
# PLATFORM = 'CWLITEARM'
# CRYPTO_TARGET='TINYAES128C'
# SS_VER='SS_VER_2_1'
# ```
# In[ ]:
# The following code will build the firmware for the target.
# In[ ]:
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
import chipwhisperer as cw
try:
if not scope.connectStatus:
scope.con()
except NameError:
scope = cw.scope(hw_location=(5, 6))
try:
if SS_VER == "SS_VER_2_1":
target_type = cw.targets.SimpleSerial2
elif SS_VER == "SS_VER_2_0":
raise OSError("SS_VER_2_0 is deprecated. Use SS_VER_2_1")
else:
target_type = cw.targets.SimpleSerial
except:
SS_VER="SS_VER_1_1"
target_type = cw.targets.SimpleSerial
try:
target = cw.target(scope, target_type)
except:
print("INFO: Caught exception on reconnecting to target - attempting to reconnect to scope first.")
print("INFO: This is a work-around when USB has died without Python knowing. Ignore errors above this line.")
scope = cw.scope(hw_location=(5, 6))
target = cw.target(scope, target_type)
print("INFO: Found ChipWhisperer😍")
# In[ ]:
if "STM" in PLATFORM or PLATFORM == "CWLITEARM" or PLATFORM == "CWNANO":
prog = cw.programmers.STM32FProgrammer
elif PLATFORM == "CW303" or PLATFORM == "CWLITEXMEGA":
prog = cw.programmers.XMEGAProgrammer
elif "neorv32" in PLATFORM.lower():
prog = cw.programmers.NEORV32Programmer
elif PLATFORM == "CW308_SAM4S" or PLATFORM == "CWHUSKY":
prog = cw.programmers.SAM4SProgrammer
else:
prog = None
# In[ ]:
import time
time.sleep(0.05)
scope.default_setup()
def reset_target(scope):
if PLATFORM == "CW303" or PLATFORM == "CWLITEXMEGA":
scope.io.pdic = 'low'
time.sleep(0.1)
scope.io.pdic = 'high_z' #XMEGA doesn't like pdic driven high
time.sleep(0.1) #xmega needs more startup time
elif "neorv32" in PLATFORM.lower():
raise IOError("Default iCE40 neorv32 build does not have external reset - reprogram device to reset")
elif PLATFORM == "CW308_SAM4S" or PLATFORM == "CWHUSKY":
scope.io.nrst = 'low'
time.sleep(0.25)
scope.io.nrst = 'high_z'
time.sleep(0.25)
else:
scope.io.nrst = 'low'
time.sleep(0.05)
scope.io.nrst = 'high_z'
time.sleep(0.05)
# In[ ]:
try:
get_ipython().run_cell_magic('bash', '-s "$PLATFORM" "$CRYPTO_TARGET" "$SS_VER"', 'cd ../../../firmware/mcu/simpleserial-aes\nmake PLATFORM=$1 CRYPTO_TARGET=$2 SS_VER=$3 -j\n &> /tmp/tmp.txt')
except:
x=open("/tmp/tmp.txt").read(); print(x); raise OSError(x)
# In[ ]:
cw.program_target(scope, prog, "../../../firmware/mcu/simpleserial-aes/simpleserial-aes-{}.hex".format(PLATFORM))
# We only need 50 traces this time to break AES!
# In[ ]:
splot = cw.StreamPlot()
splot.plot()
# In[ ]:
from tqdm.notebook import trange
import numpy as np
import time
ktp = cw.ktp.Basic()
trace_array = []
textin_array = []
key, text = ktp.next()
target.set_key(key)
N = 50 #increase nano reliability
if PLATFORM=="CWNANO":
N = 200
for i in trange(N, desc='Capturing traces'):
scope.arm()
target.simpleserial_write('p', text)
ret = scope.capture()
if ret:
print("Target timed out!")
continue
response = target.simpleserial_read('r', 16)
trace_array.append(scope.get_last_trace())
textin_array.append(text)
key, text = ktp.next()
splot.update(scope.get_last_trace())
trace_array = np.array(trace_array)
# We don't need the hardware anymore, so we'll disconnect:
# In[ ]:
scope.dis()
target.dis()
elif VERSION == 'SIMULATED':
#!/usr/bin/env python
# coding: utf-8
# # Part 4, Topic 2: CPA on Firmware Implementation of AES (SIMULATED)
# ---
# **THIS IS NOT THE COMPLETE TUTORIAL - see file with `(MAIN)` in the name.**
#
# ---
# Instead of performing a capture - just copy this data into the referenced code block. It is a copy of the previously recorded data.
# In[ ]:
from cwtraces import sca101_lab_data
import chipwhisperer as cw
data = sca101_lab_data["lab4_2"]()
trace_array = data["trace_array"]
textin_array = data["textin_array"]
key = data["key"]
INFO: Found ChipWhisperer😍 Building for platform CWNANO with CRYPTO_TARGET=TINYAES128C
SS_VER set to SS_VER_2_1
SS_VER set to SS_VER_2_1
Blank crypto options, building for AES128
arm-none-eabi-gcc (15:9-2019-q4-0ubuntu1) 9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
mkdir -p objdir-CWNANO
.
Welcome to another exciting ChipWhisperer target build!!
.
.
Compiling:
Compiling:
-en simpleserial-aes.c ...
-en .././simpleserial/simpleserial.c ...
.
.
Compiling:
Compiling:
-en .././hal//stm32f0_nano/stm32f0_hal_nano.c ...
-en .././hal//stm32f0/stm32f0_hal_lowlevel.c ...
.
.
-e Done!
Compiling:
Compiling:
-en .././crypto/tiny-AES128-C/aes.c ...
-en .././crypto/aes-independant.c ...
.
Assembling: .././hal//stm32f0/stm32f0_startup.S
arm-none-eabi-gcc -c -mcpu=cortex-m0 -I. -x assembler-with-cpp -mthumb -mfloat-abi=soft -ffunction-sections -DF_CPU=7372800 -Wa,-gstabs,-adhlns=objdir-CWNANO/stm32f0_startup.lst -I.././simpleserial/ -I.././hal/ -I.././hal/ -I.././hal//stm32f0 -I.././hal//stm32f0/CMSIS -I.././hal//stm32f0/CMSIS/core -I.././hal//stm32f0/CMSIS/device -I.././hal//stm32f0/Legacy -I.././simpleserial/ -I.././crypto/ -I.././crypto/tiny-AES128-C .././hal//stm32f0/stm32f0_startup.S -o objdir-CWNANO/stm32f0_startup.o
-e Done!
-e Done!
-e Done!
-e Done!
-e Done!
.
LINKING:
-en simpleserial-aes-CWNANO.elf ...
-e Done!
.
.
Creating load file for Flash: simpleserial-aes-CWNANO.hex
arm-none-eabi-objcopy -O ihex -R .eeprom -R .fuse -R .lock -R .signature simpleserial-aes-CWNANO.elf simpleserial-aes-CWNANO.hex
Creating load file for Flash: simpleserial-aes-CWNANO.bin
arm-none-eabi-objcopy -O binary -R .eeprom -R .fuse -R .lock -R .signature simpleserial-aes-CWNANO.elf simpleserial-aes-CWNANO.bin
.
.
Creating load file for EEPROM: simpleserial-aes-CWNANO.eep
arm-none-eabi-objcopy -j .eeprom --set-section-flags=.eeprom="alloc,load" \
--change-section-lma .eeprom=0 --no-change-warnings -O ihex simpleserial-aes-CWNANO.elf simpleserial-aes-CWNANO.eep || exit 0
Creating Extended Listing: simpleserial-aes-CWNANO.lss
arm-none-eabi-objdump -h -S -z simpleserial-aes-CWNANO.elf > simpleserial-aes-CWNANO.lss
.
Creating Symbol Table: simpleserial-aes-CWNANO.sym
arm-none-eabi-nm -n simpleserial-aes-CWNANO.elf > simpleserial-aes-CWNANO.sym
Size after:
text data bss dec hex filename
5564 536 1568 7668 1df4 simpleserial-aes-CWNANO.elf
+--------------------------------------------------------
+ Default target does full rebuild each time.
+ Specify buildtarget == allquick == to avoid full rebuild
+--------------------------------------------------------
+--------------------------------------------------------
+ Built for platform CWNANO Built-in Target (STM32F030) with:
+ CRYPTO_TARGET = TINYAES128C
+ CRYPTO_OPTIONS = AES128C
+--------------------------------------------------------
Detected known STMF32: STM32F04xxx Extended erase (0x44), this can take ten seconds or more Attempting to program 6099 bytes at 0x8000000 STM32F Programming flash...
STM32F Reading flash...
Verified flash OK, 6099 bytes
Again, let's quickly plot a trace to make sure everything looks as expected:
# ###################
# START SOLUTION
# ###################
cw.plot(trace_array[0]) * cw.plot(trace_array[1])
# ###################
# END SOLUTION
# ###################
AES Model and Hamming Weight¶
Like with the previous tutorial, we'll need to be able to easily grab what the sbox output will be for a given plaintext and key, as well as get the hamming weight of numbers between 0 and 255:
# ###################
# Add your code here
# ###################
#raise NotImplementedError("Add your code here, and delete this.")
# ###################
# START SOLUTION
# ###################
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
]
def aes_internal(inputdata, key):
return sbox[inputdata ^ key]
HW = [bin(n).count("1") for n in range(0, 256)]
# ###################
# END SOLUTION
# ###################
Verify that your model is correct:
assert HW[aes_internal(0xA1, 0x79)] == 3
assert HW[aes_internal(0x22, 0xB1)] == 5
print("✔️ OK to continue!")
✔️ OK to continue!
Developing our Correlation Algorithm¶
As we discussed earlier, we'll be testing how good our guess is using a measurement called the Pearson correlation coefficient, which measures the linear correlation between two datasets.
The actual algorithm is as follows for datasets $X$ and $Y$ of length $N$, with means of $\bar{X}$ and $\bar{Y}$, respectively:
$$r = \frac{cov(X, Y)}{\sigma_X \sigma_Y}$$
$cov(X, Y)$ is the covariance of X
and Y
and can be calculated as follows:
$$cov(X, Y) = \sum_{n=1}^{N}[(Y_n - \bar{Y})(X_n - \bar{X})]$$
$\sigma_X$ and $\sigma_Y$ are the standard deviation of the two datasets. This value can be calculated with the following equation:
$$\sigma_X = \sqrt{\sum_{n=1}^{N}(X_n - \bar{X})^2}$$
As you can see, the calulation is actually broken down pretty nicely into some smaller chunks that we can implement with some simple functions. While we could use a library to calculate all this stuff for us, being able to implement a mathematical algorithm in code is a useful skill to develop.
To start, build the following functions:
mean(X)
to calculate the mean of a datasetstd_dev(X, X_bar)
to calculate the standard deviation of a dataset. We'll need to reuse the mean for the covariance, so it makes more sense to calculate it once and pass it in to each functioncov(X, X_bar, Y, Y_bar)
to calculate the covariance of two datasets. Again, we can just pass in the means we calculate for std_dev here.
HINT: You can use np.sum(X, axis=0)
to replace all of the $\sum$ from earlier. The argument axis=0
will sum across columns, allowing us to use a single mean
, std_dev
, and cov
call for the entire power trace
# ###################
# Add your code here
# ###################
#raise NotImplementedError("Add your code here, and delete this.")
# ###################
# START SOLUTION
# ###################
def mean(X):
return np.sum(X, axis=0)/len(X)
def std_dev(X, X_bar):
return np.sqrt(np.sum((X-X_bar)**2, axis=0))
def cov(X, X_bar, Y, Y_bar):
return np.sum((X-X_bar)*(Y-Y_bar), axis=0)
# ###################
# END SOLUTION
# ###################
Let's quickly check to make sure everything's as expected:
import numpy as np
a = np