
Richte deinen Pi Zero 2W als BadUSB ein
Eine programmierbare BadUSB-/HID-Angriffsplattform auf Basis eines Raspberry Pi Zero 2 W. Der Pi präsentiert sich als USB-Verbundgerät (Composite Device) (Tastatur plus optionales, schreibgeschütztes Massenspeicher-Volume) und führt Ducky-Script-artige Payloads gegen den Host aus, an den er angeschlossen ist. Entwickelt für autorisierte Red-Team-Einsätze, Penetrationstests und CTFs.
⚠️ Rechtlicher Hinweis. Verwenden Sie das Gerät nur an Systemen, die Ihnen gehören oder für die Sie eine ausdrückliche schriftliche Testgenehmigung besitzen. Unbefugter Zugriff ist in den meisten Rechtsordnungen illegal, und so zu tun, als hätte man es nicht gewusst, hilft nicht weiter.
configured — keine fehlerhaften
Payload-Auslösungen beim Booten.LAYOUT wählbar.IF / ELSE, WHILE, HOLD / RELEASE, INJECT_MOD, RANDOM_*-Generatoren,
STRING_BLOCK / STRINGLN_BLOCK, DEFINE.ExecStop, das das Gadget sauber herunterfährt. Kein übrig
gebliebener configfs-Zustand; ein Neustart des Dienstes funktioniert ohne Reboot./dev/hidg0 — keine weltweit beschreibbaren
Geräteknoten.+----------------+ USB cable +-------------+
| Raspberry Pi | ===================>>> | Host PC |
| Zero 2 W | (Pi emulates HID + | (target) |
| (this device) | optional drive) | |
+----------------+ +-------------+
|
| /home/pi/pi-badusb/
|
+-- badusb.service ----> monitor_and_run.py
|
| on `state == configured`:
v
run_payload.py
|
v
/dev/hidg0 (USB HID gadget)
badusb.service gadget_setup.sh aus, das über
configfs/libcomposite ein USB-Gadget mit einer HID-Tastaturfunktion und (optional) einer
Massenspeicher-LUN zusammensetzt, die von einer flachen Image-Datei unter
/var/badusb/storage.img gespeist wird.monitor_and_run.py aus. Sie pollt /sys/class/udc/<udc>/state und
wartet auf configured — den USB-Spezifikationszustand, der bedeutet, dass ein Host das
Gadget erfolgreich enummeriert hat. (Wir verwenden nicht die Existenz von /dev/hidg0
als Signal: die ist bereits in dem Moment gegeben, in dem das Gadget beim Booten an die UDC
gebunden wird, unabhängig davon, ob etwas eingesteckt ist.)run_payload.py aus, das payload.txt
parst und HID-Reports an /dev/hidg0 schreibt.| Komponente | Hinweis |
|---|---|
| Raspberry Pi Zero 2 W | Getestet mit der Hardware-Revision von 2026-05. Der ältere Pi Zero W mit dem BCM2835-dwc_otg-Treiber funktioniert prinzipiell ebenfalls, aber dieses README setzt den 2 W mit dwc2 voraus. |
| micro-USB-auf-USB-A-Kabel | Oder ein Adapter im „USB-Stick“-Formfaktor, mit dem der Pi direkt in einen Host-Port gesteckt werden kann. |
| Optional: separates Netzteil | Wenn Sie den Pi aus einer Nicht-Host-Quelle mit Strom versorgen möchten, damit der Listener booten kann, bevor er in ein Zielgerät eingesteckt wird (andernfalls liefert der Host-Port den Strom). |
Der Pi Zero 2 W hat zwei micro-USB-Ports:
PWR IN — nur Strom, legt keine USB-Datenleitungen an den dwc2-OTG-Block.USB — der OTG-Datenport; hier stecken Sie das Gerät in das Zielgerät.Klonen oder kopieren Sie das Repository auf den Pi und führen Sie dann den Installer aus:
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
Nach dem Neustart den Dienst aktivieren und starten:
sudo systemctl enable --now badusb.service
journalctl -u badusb -f
install.sh ist idempotent — führen Sie es erneut aus, wenn Sie Projektdateien ändern.
Es:
/boot/firmware (Bookworm+) gegenüber /boot (älter).dtoverlay=dwc2,dr_mode=otg unter einem [all]-Block in config.txt
aktiv ist. Die Standardeinstellungen des Raspberry Pi Imagers setzen diese Zeile in einen
[cm5]-Filter, der auf dem Pi Zero 2 W nicht greift; der Installer fügt einen mit Sentinel
markierten Override hinzu, damit erneute Ausführungen ihn nicht duplizieren.modules-load=dwc2 in cmdline.txt steht.g_ether noch in cmdline.txt vorhanden ist (es entzieht libcomposite die
UDC und bricht den Gadget-Modus)./etc/systemd/system/badusb.service.# Start / stop / restart
sudo systemctl start badusb
sudo systemctl stop badusb
sudo systemctl restart badusb # safe to do while plugged in
# Watch live
journalctl -u badusb -f
# Disable autostart on boot
sudo systemctl disable badusb
# Tune timings (creates an override drop-in)
sudo systemctl edit badusb
# (paste an [Service] block with Environment="BADUSB_REARM_COOLDOWN_S=8" etc)
sudo systemctl restart badusb
Der Dienst hängt von sys-kernel-config.mount und dem Vorhandensein einer UDC ab, sodass er
keine Payloads auslösen kann, bevor das Gadget tatsächlich bereit ist.
Das Bearbeiten des Payloads erfordert keinen Neustart — payload.txt wird bei jedem
Einstecken neu gelesen.
payload.txt liegt im Installationsverzeichnis. Die vollständige Befehlsreferenz finden Sie
in payload_commands.md.
REM Open Run dialog and type a greeting via Notepad
LAYOUT US
GUI r
DELAY 1500
STRING notepad
ENTER
DELAY 2500
STRINGLN Hello from the Pi Zero 2 W
VAR $USER="alice"
VAR $COUNT=0
WHILE $COUNT < 3
STRINGLN Hello $USER (iteration $COUNT)
VAR $COUNT = $COUNT + 1
END_WHILE
IF $USER == "alice"
STRINGLN matched
ELSE
STRINGLN missed
END_IF
Mathematische Ausdrücke in VAR durchlaufen einen sicheren ast-Walker-Evaluator — keine
Namen, keine Aufrufe, kein Attributzugriff, nur numerische Literale und + - * / // % **.
HOLD SHIFT
STRINGLN this line is in capitals
RELEASE SHIFT
REM Hold Ctrl+Shift (0x01 + 0x02) and tap A
INJECT_MOD 0x03
STRING a
REM Release all modifiers
INJECT_MOD 0x00
RANDOM_LETTER 12 # 12 random mixed-case letters
RANDOM_NUMBER 6 # 6 random digits
RANDOM_SPECIAL 4 # 4 random ASCII symbols
LAYOUT UK # switch to UK ISO mappings for subsequent STRING/STRINGLN
STRING @ " # ~ £ \ | # types correctly on a UK-locale host
Legen Sie eine weitere Datei in keymaps/ ab (neben us.py und uk.py), und die Direktive
LAYOUT <name> wird sie über importlib übernehmen.
gadget_setup.sh)Diese Werte können pro Aufruf überschrieben werden, indem Sie sie beim manuellen Ausführen von
gadget_setup.sh in der Umgebung setzen, oder global über die Environment=-Direktiven der
Unit.
monitor_and_run.py)Überschreiben mit:
sudo systemctl edit badusb
# In the editor:
# [Service]
# Environment="BADUSB_REARM_COOLDOWN_S=8"
# Environment="BADUSB_MIN_INTER_FIRE_S=20"
sudo systemctl restart badusb
state verwendet, nicht /dev/hidg0Die vorherige Implementierung nutzte die Existenz und Beschreibbarkeit von /dev/hidg0 als
Signal für „Host angeschlossen“. Dieser Geräteknoten wird in dem Moment erstellt, in dem das
Gadget beim Booten an die UDC gebunden wird — lange bevor ein Host es tatsächlich enummeriert
hat. Dadurch wurden Payloads sofort beim Einschalten ausgelöst, unabhängig davon, wo der Pi
eingesteckt war.
Das zuverlässige Signal ist /sys/class/udc/<udc>/state, das den Gerätezustand gemäß
USB-Spezifikation meldet. Nur configured bedeutet, dass der Host SetConfiguration(1)
ausgeführt hat — das Gerät ist nun berechtigt, HID-Reports zu senden.
Ein physisches Trennen beim Pi Zero 2 W zu erkennen ist aus der Software heraus unmöglich: Das Board verbindet keine VBUS-Sense-Leitung mit dem dwc2-OTG-Block des SoC. Nach einem physischen Abziehen:
/sys/class/udc/<udc>/state bleibt bei configured.current_speed bleibt bei high-speed.GOTGCTL-Register bleibt bei 0x000d0000 (BSesVld-Bit gesetzt).Statt also auf ein Signal zu warten, das niemals kommen wird, verursacht der Listener das
Trennen aktiv: Nach jedem Payload schreibt er "" in die UDC-configfs-Datei des
Gadgets (was der Kernel als Unbind interpretiert), wartet BADUSB_REARM_COOLDOWN_S und
schreibt dann den UDC-Namen zurück, um neu zu binden. Das nächste Einstecken des Hosts erzeugt
einen sauberen configured-Übergang, den der Listener erkennen kann.
Wenn der Anwender den Pi nach einem Payload eingesteckt lässt, führt das Rebind dazu, dass der Host das Gadget neu enummeriert. Um eine außer Kontrolle geratene Auslöseschleife zu verhindern, greifen zwei Schutzmechanismen:
BADUSB_MIN_INTER_FIRE_S — wenn ein configured-Übergang innerhalb dieses Fensters nach
der vorherigen Auslösung passiert, wird er unterdrückt und erneut entbunden. Das Gadget
durchläuft den Zyklus still im Hintergrund.BADUSB_MAX_FIRES_PER_MINUTE — harte Obergrenze mit gleitendem Fenster. Über der
Obergrenze pausiert der Listener für BADUSB_RATELIMIT_PAUSE_S und protokolliert eine
Warnung.rm -rf verwendenDie vom Kernel verwalteten Attributdateien von configfs (bcdUSB, idVendor, webusb/*,
os_desc/*, …) können nicht mit rm(2) entfernt werden — der Kernel gibt EPERM zurück. Sie
werden nur freigegeben, wenn ihr übergeordnetes Verzeichnis per rmdir entfernt wird. Sowohl
gadget_setup.sh als auch teardown_gadget.sh durchlaufen den configfs-Baum in kanonischer
libcomposite-Reihenfolge — nur rmdir, niemals rm auf Attributdateien — und der Kernel
räumt den Rest automatisch auf.
os.write statt file.write verwendenDas Schreiben eines leeren Strings über open(path, "w").write("") ruft write(2) nicht
mit null Bytes auf — CPythons TextIOWrapper verschluckt den Aufruf. Für das configfs-Unbind
(das der Kernel aus einem Schreibvorgang ableitet, der nach dem Entfernen von Zeilenumbrüchen
eine Länge von null aufweist) verwenden wir direkt os.write(fd, b"\n"), sodass der Syscall
immer mit mindestens einem Byte ausgeführt wird.
Der Ducky-Parser verfügt über eine pytest-Suite mit 34 Tests, die gegen eine MockHIDEngine
läuft (ein In-Memory-Ersatz für den echten HID-Writer). Sie benötigt also keinen Pi und keine
USB-Hardware.
cd /home/pi/pi-badusb
python3 -m pytest tests/
Die Abdeckung umfasst:
safe_eval_math, das Arithmetik akzeptiert und Namen / Aufrufe / Attributzugriffe /
String-Konstanten ablehnt.evaluate_condition für numerische und case-sensitive String-Vergleiche.VAR mit =, +=, -=, *=, /=.IF / ELSE / END_IF, das den korrekten Zweig nimmt.WHILE / END_WHILE-Iterationszahlen für < und <=.cat /sys/class/udc/*/state — muss configured erreichen, wenn der Host enummeriert. Wenn
es bei not attached bleibt, kommuniziert der Host nicht: Probieren Sie ein anderes Kabel
(manche sind reine Ladekabel) oder einen anderen Host-Port.lsmod | grep dwc2 — muss geladen sein. Wenn nur dwc_otg vorhanden ist, greift
dtoverlay=dwc2,dr_mode=otg nicht; führen Sie install.sh erneut aus und starten Sie neu.journalctl -u badusb -f während des Einsteckens — sollte innerhalb von ~2 s nach der
Host-Enumeration Host attached. Running payload. anzeigen.Das war ein echter Bug, der inzwischen behoben ist. Wenn es passiert, haben Sie auf einen Stand
vor os.write zurückgesetzt. Stellen Sie sicher, dass monitor_and_run.py dem aktuellen
main-Branch entspricht (suchen Sie nach os.write(fd, payload)).
Operation not permitted“Das gadget_setup.sh vor der Korrektur verwendete rm -rf auf configfs. Die aktuelle Version
verwendet teardown_gadget() — wenn Sie diese Fehler sehen, haben Sie eine alte Kopie. Stellen
Sie den aktuellen Stand von main bereit.
/dev/hidg0“Die udev-Regel benötigt ein Hotplug-Ereignis, um wirksam zu werden, oder sudo udevadm trigger
und eine erneute Anmeldung, damit der Benutzer pi die Gruppenmitgliedschaft in plugdev
übernimmt.
g_ether-Warnung während der Installation“Entfernen Sie g_ether aus cmdline.txt; es beansprucht die UDC, bevor libcomposite binden
kann.
Die Massenspeichergröße wird über BACKING_SIZE_MB festgelegt und wirkt nur beim ersten Lauf,
wenn das Backing-Image erstellt wird. Zum Ändern der Größe:
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
Genau dafür gibt es BADUSB_MIN_INTER_FIRE_S und BADUSB_MAX_FIRES_PER_MINUTE. Erhöhen Sie
sie über systemctl edit badusb. Mit den Standardwerten feuert ein eingesteckt gelassener Pi
in der ersten Minute etwa alle ~15 s erneut, pausiert dann 60 s und setzt danach fort.
.
├── README.md This file
├── LICENSE MIT
├── install.sh Idempotent installer (firmware config, systemd, udev, plugdev)
├── gadget_setup.sh Composes the USB gadget via configfs/libcomposite
├── teardown_gadget.sh Canonical configfs teardown (wired as ExecStop)
├── reload_gadget.sh Manual UDC unbind/rebind helper
├── autorun.sh Legacy manual-launch wrapper (systemd is preferred)
├── monitor_and_run.py Listener: waits for host attach, runs payload, forces re-arm
├── run_payload.py Ducky-Script-style interpreter
├── payload.txt Your payload — edit freely; re-read on each plug-in
├── payload_commands.md Full command reference
├── etc/
│ ├── badusb.service systemd unit
│ └── 99-badusb-hidg.rules udev rule for /dev/hidg0 ownership
├── keymaps/
│ ├── __init__.py Dynamic layout loader
│ ├── us.py US ANSI (default)
│ └── uk.py UK ISO
└── tests/
├── __init__.py
├── conftest.py pytest path setup
└── test_parser.py 34 parser tests against a MockHIDEngine
libcomposite-Dokumentation und der
dwc2-Kerneltreiber.MIT — siehe LICENSE.
configured| Anforderung | Grund |
|---|
| Raspberry Pi OS (Debian Bookworm oder Trixie, 64-Bit-Lite empfohlen) | Das Installationsskript schreibt auf Bookworm+ nach /boot/firmware/... und fällt bei älteren Images auf /boot/... zurück. |
| Python 3 | Ist in Pi OS enthalten. |
mkfs.vfat | Zum Formatieren des Massenspeicher-Backing-Images beim ersten Lauf. Überspringbar, wenn Sie Massenspeicher deaktivieren. |
| Root-Zugriff für die Einrichtung | Modifiziert systemd, udev und /boot/firmware/config.txt. |
/etc/udev/rules.d/99-badusb-hidg.rules/dev/hidg0plugdevpi zur Gruppe plugdev hinzu./var/badusb/ für das Massenspeicher-Backing-Image.| Variable | Standard | Bedeutung |
|---|
ENABLE_MASS_STORAGE | 1 | 0 für ein reines HID-Gadget. |
BACKING_FILE | /var/badusb/storage.img | Flache Image-Datei, die dem Host präsentiert wird. |
BACKING_SIZE_MB | 64 | Wird beim ersten Lauf erstellt, falls sie fehlt. |
BACKING_LABEL | BADUSB | FAT-Volume-Label. |
MASS_STORAGE_RO | 1 | Standardmäßig schreibgeschützt. |
| Umgebungsvariable | Standard | Bedeutung |
|---|
BADUSB_POST_PAYLOAD_FLUSH_S | 0.5 | Schlafen nach dem Payload vor dem Unbind, damit HID-Schreibvorgänge abfließen können. |
BADUSB_REARM_COOLDOWN_S | 5 | Wie lange das Gadget für den Host zwischen Unbind und Rebind unsichtbar bleibt. |
BADUSB_MIN_INTER_FIRE_S | 10 | Mindestsekunden zwischen zwei Payload-Auslösungen; darunter wird die Auslösung unterdrückt und das Gadget erneut entbunden. |
BADUSB_MAX_FIRES_PER_MINUTE | 6 | Harte Obergrenze; darüber Pause für BADUSB_RATELIMIT_PAUSE_S. |
BADUSB_RATELIMIT_PAUSE_S | 60 | Pausendauer nach Auslösen des Rate-Limits. |
RANDOM_*; RANDOM_<unknown> als No-op + Warnung.INJECT_MOD-Modifikator-Bytes über nachfolgende Tastenanschläge hinweg.HOLD SHIFT, das jedes Zeichen in STRINGLN abc großschreibt.LAYOUT US vs. LAYOUT UK, die für @ und " unterschiedliche Reports erzeugen; ein
unbekanntes Layout fällt auf das vorherige zurück.STRING_BLOCK, das Zeilen verbindet; STRINGLN_BLOCK, das die Mindesteinrückung
berücksichtigt.