
Configurez votre Pi Zero 2W en BadUSB
Une plateforme programmable d'attaque BadUSB / HID construite sur un Raspberry Pi Zero 2 W. Le Pi s'énumère en tant que périphérique composite USB (clavier plus un volume de stockage de masse optionnel en lecture seule) et exécute des charges utiles de style Ducky-Script sur l'hôte auquel il est branché. Conçu pour des engagements d'équipe rouge autorisés, des tests de pénétration et des CTF.
⚠️ Avis légal. Utilisez uniquement sur des systèmes dont vous êtes propriétaire ou pour lesquels vous disposez d'une autorisation écrite explicite de test. L'accès non autorisé est illégal dans la plupart des juridictions, et faire semblant de l'ignorer ne vous aidera pas.
configured de l'UDC — pas de déclenchement intempestif de charge utile au démarrage.LAYOUT.IF / ELSE, WHILE, HOLD / RELEASE, INJECT_MOD, générateurs RANDOM_*, STRING_BLOCK / STRINGLN_BLOCK, DEFINE.ExecStop qui démonte proprement le gadget. Pas d'état résiduel de configfs, le redémarrage fonctionne sans redémarrer./dev/hidg0 — pas de nœuds de périphérique accessibles en écriture par tout le monde.+----------------+ Câble USB +-------------+
| Raspberry Pi | ===================>>> | PC hôte |
| Zero 2 W | (Pi émule HID + | (cible) |
| (cet appareil)| optionnel lecteur) | |
+----------------+ +-------------+
|
| /home/pi/pi-badusb/
|
+-- badusb.service ----> monitor_and_run.py
|
| sur `state == configured` :
v
run_payload.py
|
v
/dev/hidg0 (gadget HID USB)
badusb.service exécute gadget_setup.sh, qui utilise configfs/libcomposite pour composer un gadget USB avec une fonction clavier HID et (optionnellement) un LUN de stockage de masse basé sur un fichier image plat à /var/badusb/storage.img.monitor_and_run.py. Il interroge /sys/class/udc/<udc>/state et attend configured — l'état USB-spec qui signifie qu'un hôte a énuméré avec succès le gadget. (Nous n'utilisons pas l'existence de /dev/hidg0 comme signal : celui-ci est vrai dès que le gadget se lie à l'UDC au démarrage, qu'un câble soit branché ou non.)run_payload.py, qui analyse payload.txt et écrit des rapports HID vers /dev/hidg0.| Composant | Remarque |
|---|---|
| Raspberry Pi Zero 2 W | Testé sur révision matérielle 2026-05. Les anciens Pi Zero W avec le pilote dwc_otg BCM2835 fonctionnent également en principe, mais ce README suppose un Zero 2 W avec dwc2. |
| Câble micro-USB vers USB-A | Ou un adaptateur au format "clé USB" qui permet de brancher le Pi directement sur un port hôte. |
| Optionnel : chargeur séparé | Si vous voulez alimenter le Pi depuis une source non-hôte afin que l'écouteur puisse démarrer avant d'être branché sur une cible (sinon le port hôte fournit l'alimentation). |
Le Pi Zero 2 W possède deux ports micro-USB :
PWR IN — alimentation uniquement, n'expose pas les lignes de données USB au bloc dwc2 OTG.USB — le port de données OTG ; c'est là que vous branchez sur la cible.Clonez ou copiez le dépôt dans le Pi, puis exécutez l'installateur :
git clone http://your-gitea/admin/Pi-Zero-2W-Bad-USB.git /home/pi/pi-badusb
cd /home/pi/pi-badusb
sudo ./install.sh
sudo reboot
Après redémarrage, activez et démarrez le service :
sudo systemctl enable --now badusb.service
journalctl -u badusb -f
install.sh est idempotent — réexécutez-le à chaque modification des fichiers du projet. Il :
/boot/firmware (Bookworm+) vs /boot (plus ancien).dtoverlay=dwc2,dr_mode=otg est actif sous un bloc [all] dans config.txt. Les valeurs par défaut de Raspberry Pi Imager placent cette ligne dans un filtre [cm5] qui ne s'applique pas sur Pi Zero 2 W ; l'installateur ajoute une surcouche marquée par un repère pour que les réexécutions ne la dupliquent pas.modules-load=dwc2 est présent dans cmdline.txt.g_ether est toujours présent dans cmdline.txt (il vole l'UDC à libcomposite et casse le mode gadget)./etc/systemd/system/badusb.service./etc/udev/rules.d/99-badusb-hidg.rules pour que soit accessible en écriture par le groupe .# Démarrer / arrêter / redémarrer
sudo systemctl start badusb
sudo systemctl stop badusb
sudo systemctl restart badusb # sûr à faire pendant que le câble est branché
# Voir les logs en direct
journalctl -u badusb -f
# Désactiver le démarrage automatique au boot
sudo systemctl disable badusb
# Régler les temporisations (crée un fichier de remplacement)
sudo systemctl edit badusb
# (collez un bloc [Service] avec Environment="BADUSB_REARM_COOLDOWN_S=8" etc.)
sudo systemctl restart badusb
Le service dépend de sys-kernel-config.mount et de la présence d'un UDC, il ne peut donc pas déclencher de charges utiles avant que le gadget ne soit effectivement prêt.
Modifier la charge utile ne nécessite pas de redémarrage — payload.txt est relu à chaque branchement.
payload.txt se trouve dans le répertoire d'installation. La référence complète des commandes se trouve dans payload_commands.md.
REM Ouvrir la boîte de dialogue Exécuter et taper un message via le Bloc-notes
LAYOUT US
GUI r
DELAY 1500
STRING notepad
ENTER
DELAY 2500
STRINGLN Bonjour depuis le Pi Zero 2 W
VAR $USER="alice"
VAR $COUNT=0
WHILE $COUNT < 3
STRINGLN Bonjour $USER (itération $COUNT)
VAR $COUNT = $COUNT + 1
END_WHILE
IF $USER == "alice"
STRINGLN correspondance
ELSE
STRINGLN pas de correspondance
END_IF
Les expressions mathématiques dans VAR passent par un évaluateur sécurisé (ast walker) — pas de noms, pas d'appels, pas d'accès aux attributs, uniquement des littéraux numériques et + - * / // % **.
HOLD SHIFT
STRINGLN cette ligne est en majuscules
RELEASE SHIFT
REM Maintenir Ctrl+Shift (0x01 + 0x02) et taper A
INJECT_MOD 0x03
STRING a
REM Relâcher tous les modificateurs
INJECT_MOD 0x00
RANDOM_LETTER 12 # 12 lettres aléatoires en minuscules/majuscules
RANDOM_NUMBER 6 # 6 chiffres aléatoires
RANDOM_SPECIAL 4 # 4 symboles ASCII aléatoires
LAYOUT UK # passer aux mappings UK ISO pour les STRING/STRINGLN suivants
STRING @ " # ~ £ \ | # tape correctement sur un hôte en disposition UK
Déposez un autre fichier dans keymaps/ (aux côtés de us.py et uk.py) et la directive LAYOUT <nom> le chargera via importlib.
gadget_setup.sh)Ces valeurs peuvent être remplacées par invocation en les définissant dans l'environnement lors de l'exécution manuelle de gadget_setup.sh, ou globalement via les directives Environment= de l'unité.
monitor_and_run.py)Remplacez avec :
sudo systemctl edit badusb
# Dans l'éditeur :
# [Service]
# Environment="BADUSB_REARM_COOLDOWN_S=8"
# Environment="BADUSB_MIN_INTER_FIRE_S=20"
sudo systemctl restart badusb
state de l'UDC, pas /dev/hidg0L'implémentation précédente testait l'existence + l'accessibilité en écriture de /dev/hidg0 comme signal "hôte attaché". Ce nœud de périphérique est créé dès que le gadget se lie à l'UDC au démarrage — bien avant qu'un hôte ne l'ait réellement énuméré. Ainsi, les charges utiles se déclenchaient immédiatement à la mise sous tension, indépendamment de l'endroit où le Pi était branché.
Le signal fiable est /sys/class/udc/<udc>/state, qui rapporte l'état du périphérique selon la spécification USB. Seul configured signifie que l'hôte a émis SetConfiguration(1) — le périphérique peut alors envoyer des rapports HID.
Détecter une déconnexion physique sur le Pi Zero 2 W est impossible par logiciel : la carte ne connecte pas VBUS sense au bloc OTG dwc2 du SoC. Après un débranchement physique :
/sys/class/udc/<udc>/state reste à configured.current_speed reste à high-speed.GOTGCTL de dwc2 reste à 0x000d0000 (bit BSesVld positionné).Donc au lieu d'attendre un signal qui ne viendra jamais, l'écouteur provoque activement la déconnexion : après chaque charge utile, il écrit "" dans le fichier UDC de configfs du gadget (que le noyau interprète comme un déliement), attend BADUSB_REARM_COOLDOWN_S, puis réécrit le nom de l'UDC pour relier. Le prochain branchement hôte produit une transition propre vers configured que l'écouteur peut détecter.
Si l'opérateur laisse le Pi branché après une charge utile, le reliement force l'hôte à réénumérer le gadget. Pour éviter une boucle de tir incontrôlée, deux protections s'appliquent :
BADUSB_MIN_INTER_FIRE_S — si une transition configured se produit dans cette fenêtre après le tir précédent, elle est supprimée et le gadget est délié à nouveau. Le gadget effectue des cycles silencieux en arrière-plan.BADUSB_MAX_FIRES_PER_MINUTE — limite stricte à fenêtre glissante. Au-dessus du seuil, l'écouteur fait une pause de BADUSB_RATELIMIT_PAUSE_S et enregistre un avertissement.rm -rf sur configfsLes fichiers d'attributs gérés par le noyau de configfs (bcdUSB, idVendor, webusb/*, os_desc/*, …) ne peuvent pas être supprimés par rm(2) — le noyau renvoie EPERM. Ils sont libérés uniquement lorsque leur répertoire parent est supprimé par rmdir. gadget_setup.sh et teardown_gadget.sh parcourent tous deux l'arbre configfs dans l'ordre canonique de libcomposite — uniquement des rmdir, jamais de rm sur des fichiers d'attributs — et le noyau nettoie le reste automatiquement.
os.write et non file.writeÉcrire une chaîne vide via open(path, "w").write("") n'invoque pas write(2) avec zéro octet — le TextIOWrapper de CPython l'élude. Pour le déliement de configfs (que le noyau interprète à partir d'une écriture de longueur nulle après suppression du saut de ligne), nous utilisons directement os.write(fd, b"\n") afin que l'appel système soit toujours émis avec au moins un octet.
L'analyseur Ducky dispose d'une suite pytest de 34 tests qui s'exécute contre un MockHIDEngine (un substitut en mémoire du véritable rédacteur HID), donc elle ne nécessite ni Pi ni matériel USB.
cd /home/pi/pi-badusb
python3 -m pytest tests/
La couverture inclut :
safe_eval_math acceptant l'arithmétique, rejetant les noms / appels / accès aux attributs / constantes de chaînes.evaluate_condition pour les comparaisons numériques et de chaînes sensibles à la casse.VAR avec =, +=, -=, *=, /=.IF / ELSE / END_IF prenant la bonne branche.WHILE / END_WHILE comptage d'itérations pour < et <=.cat /sys/class/udc/*/state — doit atteindre configured lorsque l'hôte énumère. S'il reste à not attached, l'hôte ne communique pas : essayez un autre câble (certains sont charge uniquement) ou un autre port hôte.lsmod | grep dwc2 — doit être chargé. Si seul dwc_otg est présent, dtoverlay=dwc2,dr_mode=otg ne s'applique pas ; réexécutez install.sh et redémarrez.journalctl -u badusb -f pendant le branchement — devrait montrer Host attached. Running payload. dans les ~2s suivant l'énumération de l'hôte.C'était un vrai bug maintenant corrigé. Si cela se produit, vous êtes revenu à une version antérieure à os.write. Assurez-vous que monitor_and_run.py correspond à la branche main actuelle (cherchez os.write(fd, payload)).
Operation not permitted"Avant le correctif, gadget_setup.sh utilisait rm -rf sur configfs. La version actuelle utilise teardown_gadget() — si vous voyez ces erreurs, vous avez une vieille copie. Redéployez depuis main.
/dev/hidg0"La règle udev nécessite un hotplug pour s'appliquer, ou sudo udevadm trigger et une reconnexion pour que l'utilisateur pi récupère le groupe plugdev.
g_ether pendant l'installation"Supprimez g_ether de cmdline.txt ; il réclame l'UDC avant que libcomposite ne puisse se lier.
La taille du stockage de masse est définie par BACKING_SIZE_MB et n'est effective que lors de la première exécution lorsque l'image est créée. Pour redimensionner :
sudo systemctl stop badusb
sudo rm /var/badusb/storage.img
sudo BACKING_SIZE_MB=256 /home/pi/pi-badusb/gadget_setup.sh
sudo systemctl start badusb
C'est à cela que servent BADUSB_MIN_INTER_FIRE_S et BADUSB_MAX_FIRES_PER_MINUTE. Augmentez-les via systemctl edit badusb. Avec les valeurs par défaut, un Pi laissé branché retirera toutes les ~15s la première minute, puis fera une pause de 60s, puis reprendra.
.
├── README.md Ce fichier
├── LICENSE MIT
├── install.sh Installateur idempotent (config firmware, systemd, udev, plugdev)
├── gadget_setup.sh Compose le gadget USB via configfs/libcomposite
├── teardown_gadget.sh Démontage canonique de configfs (utilisé comme ExecStop)
├── reload_gadget.sh Aide manuel pour délier/relier l'UDC
├── autorun.sh Ancien wrapper de lancement manuel (systemd est préféré)
├── monitor_and_run.py Écouteur : attend l'attachement hôte, exécute la charge utile, force le réamorçage
├── run_payload.py Interpréteur de style Ducky-Script
├── payload.txt Votre charge utile — modifiez librement ; relue à chaque branchement
├── payload_commands.md Référence complète des commandes
├── etc/
│ ├── badusb.service Unité systemd
│ └── 99-badusb-hidg.rules Règle udev pour les droits sur /dev/hidg0
├── keymaps/
│ ├── __init__.py Chargeur de dispositions dynamique
│ ├── us.py ANSI US (par défaut)
│ └── uk.py ISO UK
└── tests/
├── __init__.py
├── conftest.py Configuration des chemins pytest
└── test_parser.py 34 tests de l'analyseur contre un MockHIDEngine
libcomposite, le pilote noyau dwc2.MIT — voir LICENSE.
configured| Prérequis | Pourquoi |
|---|
| Raspberry Pi OS (Debian Bookworm ou Trixie, 64-bit Lite recommandé) | Le script d'installation écrit dans /boot/firmware/... sur Bookworm+ et utilise /boot/... sur les images plus anciennes. |
| Python 3 | Fourni avec Pi OS. |
mkfs.vfat | Pour formater l'image de stockage de masse lors de la première exécution. Désactivez si vous désactivez le stockage de masse. |
| Accès root pour l'installation | Touche systemd, udev et /boot/firmware/config.txt. |
/dev/hidg0plugdevpi au groupe plugdev./var/badusb/ pour l'image de stockage de masse.| Variable | Défaut | Signification |
|---|
ENABLE_MASS_STORAGE | 1 | 0 pour un gadget HID uniquement. |
BACKING_FILE | /var/badusb/storage.img | Image plate exposée à l'hôte. |
BACKING_SIZE_MB | 64 | Créée lors de la première exécution si manquante. |
BACKING_LABEL | BADUSB | Nom du volume FAT. |
MASS_STORAGE_RO | 1 | Lecture seule par défaut. |
| Variable d'env | Défaut | Signification |
|---|
BADUSB_POST_PAYLOAD_FLUSH_S | 0.5 | Pause après la charge utile avant de délier pour permettre l'écoulement des écritures HID. |
BADUSB_REARM_COOLDOWN_S | 5 | Durée pendant laquelle le gadget reste invisible pour l'hôte entre le déliement et le reliement. |
BADUSB_MIN_INTER_FIRE_S | 10 | Secondes minimales entre deux tirs de charge utile ; en dessous, le tir est supprimé et le gadget est délié à nouveau. |
BADUSB_MAX_FIRES_PER_MINUTE | 6 | Limite stricte ; au-delà, pause de BADUSB_RATELIMIT_PAUSE_S. |
BADUSB_RATELIMIT_PAUSE_S | 60 | Durée de la pause après déclenchement de la limitation de débit. |
RANDOM_* exactitude de longueur ; RANDOM_<inconnu> inopérant + avertissement.INJECT_MOD persistance de l'octet de modificateur à travers les frappes suivantes.HOLD SHIFT mettant chaque caractère en majuscule dans STRINGLN abc.LAYOUT US vs LAYOUT UK produisant des rapports différents pour @ et " ; disposition inconnue revenant à la précédente.STRING_BLOCK joignant les lignes ; STRINGLN_BLOCK respectant l'indentation minimale.