I’m currently running an experiment on a target board: SAM4L (ATSAM4LC2AA), building this firmware(simpleserial-aes). When I use the software (SW) implementation of AES, it works just fine. However, when I switch to the hardware (HW) implementation “HWAES” with the following setting: AESA->AESA_MODE = AESA_MODE_ENCRYPT | (AESA_MODE_CTYPE(0x00));, I am unable to guess the keys as expected, despite not using any countermeasures.
Should I change something? I’ve looked into the documentation but couldn’t find anything relevant.
I would recommend to run the attack in several steps.
Let’s first collect correct power traces.
For the first attempt, you can run the script
import chipwhisperer as cw
import time
from tqdm import tqdm
scope = cw.scope()
target = cw.target(scope, cw.targets.SimpleSerial2)
time.sleep(0.05)
scope.default_setup()
time.sleep(0.05)
scope.io.nrst = 'low'
time.sleep(0.05)
scope.io.nrst = 'high_z'
scope.gain.db = 38
scope.adc.samples = 2000
scope.adc.offset = 0
scope.adc.basic_mode = "rising_edge"
scope.clock.reset_adc()
assert (scope.clock.adc_locked), "ADC failed to lock"
project = cw.create_project("traces/SAM4L_HW_AES.cwp", overwrite=True)
ktp = cw.ktp.Basic()
N = 10
for i in tqdm(range(N)):
key, text = ktp.next()
trace = cw.capture_trace(scope, target, text, key)
if trace is None:
continue
project.traces.append(trace)
print(scope.adc.trig_count)
project.save()
scope.dis()
target.dis()
Please, run above script and share the output, then we will fix the “scope.adc.samples” and “N” values.
…and, BTW, I would recommend to focus on the CPA attacks (for HW AES) rather than DPA
Perfect!
So, the final script to collect the power traces will be:
import chipwhisperer as cw
import time
from tqdm import tqdm
scope = cw.scope()
target = cw.target(scope, cw.targets.SimpleSerial2)
target.baud = 38400
time.sleep(0.05)
scope.default_setup()
time.sleep(0.05)
scope.io.nrst = 'low'
time.sleep(0.05)
scope.io.nrst = 'high_z'
scope.gain.db = 38
scope.adc.samples = 200
scope.adc.offset = 0
scope.adc.basic_mode = "rising_edge"
scope.clock.reset_adc()
assert (scope.clock.adc_locked), "ADC failed to lock"
project = cw.create_project("traces/SAM4L_HW_AES.cwp", overwrite=True)
ktp = cw.ktp.Basic()
N = 20000
for i in tqdm(range(N)):
key, text = ktp.next()
trace = cw.capture_trace(scope, target, text, key)
if trace is None:
continue
project.traces.append(trace)
print(scope.adc.trig_count)
project.save()
scope.dis()
target.dis()
Above script will collect 20000 traces and stores them in the project “traces/SAM4L_HW_AES.cwp” which will be available “offline” for further analyzing.
Now, please run the script again (it can take up to 30 - 40 min) and if it is possible, share the folder “traces” with me. The folder “traces” will store the project with captured traces in numpy format. The good option is to share via github repo or you can share just via file upload resources.
…couple of words regarding HW AES countermeasures. The home page of the target board claims
void aes_init(void)
{
periclk_aesa_init();
SCIF->SCIF_GCCTRL[AESA_GCLK_NUM].SCIF_GCCTRL = SCIF_GCCTRL_OSCSEL(GENCLK_SRC_CLK_CPU) | SCIF_GCCTRL_CEN;
/* AES Enable */
AESA->AESA_CTRL = AESA_CTRL_ENABLE | AESA_CTRL_NEWMSG; /* Enable, auto-accept new messages */
//Use with debugger to check PARAMETER register value
//volatile uint32_t param = AESA->AESA_PARAMETER;
/* AES Mode */
//AESA->AESA_MODE = AESA_MODE_ENCRYPT | (AESA_MODE_CTYPE(0x0F)); /* Encrypt Mode, with all countermeasures */
AESA->AESA_MODE = AESA_MODE_ENCRYPT; /* Encrypt Mode, without countermeasures */
/* Setup random seed for countermeasures to work */
AESA->AESA_DRNGSEED = 0xDEADBEEF; //A very random number
}
so, to disable all countermeasures you have to use
AESA->AESA_MODE = AESA_MODE_ENCRYPT;
you already use AESA->AESA_MODE = AESA_MODE_ENCRYPT | (AESA_MODE_CTYPE(0x00)); which also does not activate the countermeasures.
When you share the collected traces, I will check whether syncronization between traces is fine and then share with you the next script which will run HW AES attack against the collected traces.
@Tate Thanks for sharing the traces. It looks like the traces are truncated due to high “scope.gain.db” value when the magnitude of the traces being collected goes out of the dynamic range of the ADC.
So, you have to decrease the “scope.gain.db” value in the script. You can try to use “scope.gain.db = 30” first.
Just collect 10 traces and estimate the pictures. You shouldn’t see truncated spikes below 0.5.
The temporal script to adjust parameters can be
import chipwhisperer as cw
import time
from tqdm import tqdm
scope = cw.scope()
target = cw.target(scope, cw.targets.SimpleSerial2)
target.baud = 38400
time.sleep(0.05)
scope.default_setup()
time.sleep(0.05)
scope.io.nrst = 'low'
time.sleep(0.05)
scope.io.nrst = 'high_z'
scope.gain.db = 30
scope.adc.samples = 200
scope.adc.offset = 0
scope.adc.basic_mode = "rising_edge"
scope.clock.reset_adc()
assert (scope.clock.adc_locked), "ADC failed to lock"
project = cw.create_project("traces/SAM4L_HW_AES.cwp", overwrite=True)
ktp = cw.ktp.Basic()
N = 10
for i in tqdm(range(N)):
key, text = ktp.next()
trace = cw.capture_trace(scope, target, text, key)
if trace is None:
continue
project.traces.append(trace)
print(scope.adc.trig_count)
project.save()
scope.dis()
target.dis()
You can estimate the result on your side by yourself using the script
import chipwhisperer as cw
from bokeh.plotting import figure, show
from bokeh.io import output_notebook
from bokeh.palettes import brewer
project = cw.open_project("traces/SAM4L_HW_AES.cwp")
p = figure(sizing_mode='scale_width', plot_height=300, x_range=(0, 200))
xrange = range(0, len(project.waves[0]))
for i in range(10):
p.line(xrange, project.waves[i], line_color="red")
show(p)
Just put it in the same folder where you run the script to collect the power traces. The script will create a picture and open it in the default web browser. Probably, you will need to install missed python packages using the pip.
@Tate BTW, I was able to recover the key even with these traces.
python cpa_attack.py
2024-07-18 10:24:10,885 - lascar.session - INFO - Session Session: 20000 traces, 18 engines, batch_size=100, leakage_shape=(50,)
INFO:lascar.session:Session Session: 20000 traces, 18 engines, batch_size=100, leakage_shape=(50,)
Session |100%|#########################################################################################################|20000 trc/20000 | (18 engines, batch_size=100, leakage_shape=(50,)) |Time: 0:00:18
Best Guess is D0 (Corr = 0.07410606624966237)
Best Guess is 14 (Corr = 0.08130798855923012)
Best Guess is F9 (Corr = 0.06168717961341643)
Best Guess is A8 (Corr = 0.05319156115029012)
Best Guess is C9 (Corr = 0.09187811671745756)
Best Guess is EE (Corr = 0.05657068856313248)
Best Guess is 25 (Corr = 0.053741836991501514)
Best Guess is 89 (Corr = 0.06432084611061244)
Best Guess is E1 (Corr = 0.05593180783083206)
Best Guess is 3F (Corr = 0.059734288065038346)
Best Guess is 0C (Corr = 0.07579080270439846)
Best Guess is C8 (Corr = 0.07701108994023992)
Best Guess is B6 (Corr = 0.06195052329587045)
Best Guess is 63 (Corr = 0.07070176616045554)
Best Guess is 0C (Corr = 0.07384373527375233)
Best Guess is A6 (Corr = 0.0606030319990759)
But let’s make perfect power traces before…
This will help to fix issues by yourself in future.
import chipwhisperer.common.api.lascar as cw_lascar
from lascar import *
import chipwhisperer.analyzer as cwa
import chipwhisperer as cw
project = cw.open_project("traces/SAM4L_HW_AES.cwp")
cw_container = cw_lascar.CWContainer(project, project.textouts, 70, 120)
guess_range = range(256)
leakage = cw_lascar.lastround_HD_gen
cpa_engines = [CpaEngine("cpa_%02d" % i, leakage(i), guess_range) for i in range(16)]
session = Session(cw_container, engines=cpa_engines).run(batch_size=100)
for i in range(16):
results = cpa_engines[i].finalize()
guess = abs(results).max(1).argsort()[-1]
print("Best Guess is {:02X} (Corr = {})".format(guess, abs(results).max()))
Put this script at the same folder where you collected the power traces.
The main idea here is using the “lastround_HD_gen” leakage model. Try to learn why this leakage model works against the AES HW implementation.
Homework to you:
Find, where exactly required leakage happens. The script already has a hint to you.
Find, what minimal number of traces are required to recover the last round key.
Sorry for the confusion, I meant how to test it(as we did with DPA for SW Implementation) against the known key
“known_key = [0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c]”.
I’m trying that using the base module"Ctype= 0x00, using CRYPTO_TARGET = ‘HWAES’
".