DPA on firmware implementation of AES

Hello there,

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.

image



Thanks.

BTW, what ChipWhisperer you use? Lite or Husky?

Hi,

The leakage model used in that lab tends not to work very well for hardware AES, since some of the assumptions, Hamming weight leakage instead of Hamming distance, and strong data leakage from all operations don’t end up being true for hardware implementations of AES. We’ve got a lab on hardware AES that I recommend checking out instead: chipwhisperer-jupyter/courses/sca201/SOLN_Lab 2_2 - CPA on Hardware AES Implementation.ipynb at master · newaetech/chipwhisperer-jupyter · GitHub

Alex

Hi.
It’s ChipWhisperer Lite.

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

Hi,
This is the output from my run:

It looks like, the script cannot get data from the target board.
Let’s set the UART speed to 38400. Please, copy/paste below script and try again.

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 = 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()

How did you burn the firmware into the SAM4L? By means of external JTAG?

Hi,
this the output:


Yes Sir; using J-link through JTAG:

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.

1 Like

@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.

Hello,
Yeah I could see the truncated trace,


I’ve tried to complete the whole lab I couldn’t get the key right, are you referencing the CPA method?

Thanks.

Use this script to attack

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:

  1. Find, where exactly required leakage happens. The script already has a hint to you.
  2. Find, what minimal number of traces are required to recover the last round key.

Interesting., it should be at the last round
this is my run for N =20000:


I guess I need to find the right N to guess the keys

Sorry, I didn’t get your point. You already guessed the 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’
".

Thanks.

Well. What round key have we been trying to attack?
You have to answer this question first.