Cet article fait suite à Premiers pas avec le ChipWhisperer-Husky, qui couvre l’installation de l’environnement et la configuration du matériel.
L’objectif ici est de capturer une trace de consommation en suivant le lab Lab 2_1A - Instruction Power Differences des notebooks NewAE, puis de l’interpréter pour en extraire des informations sur ce que la cible est en train d’exécuter.
Le firmware analysé est simpleserial-aes-lab2, fourni par NewAE dans le dépôt
newaetech/chipwhisperer-jupyter. Ce programme implémente un algorithme volontairement opaque : l’exercice consiste à déduire son comportement uniquement à partir de sa courbe de consommation, sans accès au code source ni au binaire.
Avant de brancher quoi que ce soit, il faut comprendre ce qu’on mesure et pourquoi c’est utile.
Un microcontrôleur ne consomme pas un courant constant : sa consommation dépend de ce qu’il est en train de faire. À chaque instant, ce sont les opérations exécutées par les unités de calcul qui déterminent la quantité de courant appelée.
Cette sensibilité vient de la technologie utilisée. Les circuits modernes sont construits en logique CMOS, dans laquelle un circuit au repos (dont les états ne changent pas) ne consomme presque rien. L’essentiel de l’énergie est dépensé au moment des commutations, lorsque les transistors basculent d’un état à l’autre. Ce n’est donc pas l’état du circuit qui coûte de l’énergie, mais le changement d’état. Concrètement, chaque fois qu’un bit passe de 0 à 1 ou de 1 à 0, il faut charger ou décharger de minuscules capacités à l’intérieur du circuit. La consommation à un instant donné reflète ainsi directement le nombre de bits qui basculent à cet instant.
C’est cette dépendance entre les données traitées et la consommation mesurable de l’extérieur qui rend l’analyse de canal auxiliaire possible.
Le montage physique est minimal. La carte cible SAM4S s’insère sur la baseboard CW313, qui se connecte au ChipWhisperer-Husky via le connecteur 20 broches. Le Husky est ensuite relié au PC par USB.
Ce seul câble nappe suffit : le Husky alimente la cible, génère son horloge, mesure sa consommation via la résistance de shunt intégrée et assure la communication série avec elle.

Le ChipWhisperer-Husky est l’outil de capture : connecté au PC par USB, il pilote l’ensemble du montage. La CW313 est la baseboard intermédiaire qui accueille les cartes cibles interchangeables et expose le connecteur standardisé vers le Husky. Le SAM4S est la carte cible elle-même, le microcontrôleur sur lequel tourne le firmware à analyser.
Le code suivant est fourni par NewAE, adapté pour le ChipWhisperer Husky. Il ouvre la connexion avec le scope, initialise la cible et définit une fonction reset_target pour la redémarrer entre deux captures.
import chipwhisperer as cw
try:
if not scope.connectStatus:
scope.con()
except NameError:
scope = cw.scope()
try:
target = cw.target(scope)
except IOError:
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()
target = cw.target(scope)
print("INFO: Found ChipWhisperer😍")
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 PLATFORM == "CWHUSKY":
prog = cw.programmers.SAM4SProgrammer
else:
prog = None
import time
time.sleep(0.05)
scope._default_setup() # evite le bug scope.trace=None dans default_setup()
scope.io.cdc_settings = 0
scope.glitch.enabled = False
scope.LA.enabled = False
scope.adc.segments = 1
scope.adc.samples = 32000 # déterminé par itération : plage suffisante pour capturer le signal complet
scope.gain.db = 5
def reset_target(scope):
if PLATFORM == "CW303" or PLATFORM == "CWLITEXMEGA":
scope.io.pdic = 'low'
time.sleep(0.05)
scope.io.pdic = 'high_z' #XMEGA doesn't like pdic driven high
time.sleep(0.05)
else:
scope.io.nrst = 'low'
time.sleep(0.05)
scope.io.nrst = 'high_z'
time.sleep(0.05)La connexion est établie. Avant de flasher la cible, il faut compiler le firmware. Depuis le dossier du code source correspondant à notre carte cible :
make PLATFORM=$PLATFORM CRYPTO_TARGET=NONE -jDepuis le notebook, le firmware compilé est ensuite flashé sur la cible :
cw.program_target(scope, prog, "../../../firmware/mcu/simpleserial-aes/simpleserial-aes-{}.hex".format(PLATFORM))La cible est flashée. Il reste à comprendre comment le ChipWhisperer lui envoie des données pendant une capture.
Le protocole sous-jacent est SimpleSerial, un format texte minimaliste développé par NewAE qui passe par une liaison UART entre le Husky et le microcontrôleur. Il n’est pas nécessaire de l’implémenter soi-même : l’API ChipWhisperer s’en charge entièrement. Un appel à cw.capture_trace(scope, target, text) suffit pour envoyer le plaintext à la cible, déclencher l’enregistrement de la consommation pendant l’exécution, et récupérer le résultat, sans jamais manipuler le protocole directement.
Le firmware est en place, le matériel est connecté : il ne reste plus qu’à déclencher une capture. La fonction suivante envoie un texte à la cible et récupère la trace de consommation correspondante.
def capture_trace(_ignored=None):
ktp = cw.ktp.Basic()
key, text = ktp.next()
return cw.capture_trace(scope, target, text).waveCette fonction capture une trace de consommation en trois étapes :
cw.ktp.Basic() instancie un générateur de paires clé/texte (key-text pair). Il tire une clé aléatoire une seule fois à la création, puis ktp.next() génère un nouveau texte aléatoire à chaque appel en conservant cette même clé. C’est essentiel : pour analyser les traces, il faut de nombreux chiffrements avec des plaintexts différents mais toujours la même clé.cw.capture_trace() : le ChipWhisperer déclenche l’acquisition au bon moment, envoie le texte au SAM4S, et enregistre la consommation de courant pendant que le firmware l’exécute..wave extrait le signal brut : un tableau de mesures de courant échantillonnées dans le temps, une valeur par coup d’horloge. C’est cette courbe qui constitue la trace.Une fois la trace capturée, il ne reste plus qu’à l’afficher :
import matplotlib.pyplot as plt
trace = capture_trace()
if trace is None:
print("No trace captured")
else:
plt.plot(trace)
plt.show()
La trace brute est difficilement lisible : le signal est noyé dans le bruit. Pour l’atténuer, on applique une convolution glissante qui remplace chaque point par la moyenne de lui-même et des x points voisins :
env = np.convolve(np.abs(trace), np.ones(10)/10, mode='same')Avec une fenêtre de 10 points :

Avec une fenêtre de 100 points :

Les 10 motifs se ressemblent mais ne sont pas identiques : chaque round AES opère sur des données différentes, ce qui produit des patterns de commutation distincts et donc des amplitudes légèrement variables d’un round à l’autre. La régularité de la structure confirme qu’il s’agit d’un algorithme itératif, et le nombre de répétitions identifie sans ambiguïté un AES-128 (AES-192 en produirait 12, AES-256 en produirait 14).
Cette observation reste cependant insuffisante pour retrouver la clé : la relation entre les données traitées et l’amplitude du signal est trop complexe pour être exploitée à l’œil nu.
La capture en elle-même mérite une nuance : la carte cible et le firmware NewAE envoient un signal GPIO pour indiquer précisément au Husky quand déclencher l’enregistrement, juste avant l’exécution de l’AES. Ce trigger matériel rend la capture triviale. Dans un contexte opérationnel réel, ce signal n’existe pas : il faut alors trouver soi-même le bon moment dans la trace, souvent noyé dans le bruit et l’activité du reste du firmware.
Pour exploiter ces traces et retrouver la clé, il faut une approche statistique. C’est ce que nous verrons dans un prochain article, avec une attaque par analyse de corrélation de puissance (CPA).