
Configura tu Pi Zero 2W para que sea un BadUSB
Una plataforma de ataque BadUSB / HID programable construida sobre una Raspberry Pi Zero 2 W. La Pi se enumera como un dispositivo compuesto USB (teclado más un volumen de almacenamiento masivo de solo lectura opcional) y ejecuta cargas útiles al estilo Ducky-Script contra el host al que está conectada. Diseñada para pruebas de penetración autorizadas, ejercicios de red-team y CTFs.
⚠️ Aviso legal. Úselo solo en sistemas que posea o para los que tenga permiso explícito por escrito. El acceso no autorizado es ilegal en la mayoría de las jurisdicciones, y fingir que no lo sabía no ayudará.
configured del UDC — sin disparos de carga útil espurios al arrancar.LAYOUT.IF / ELSE, WHILE, HOLD / RELEASE, INJECT_MOD, generadores RANDOM_*, STRING_BLOCK / STRINGLN_BLOCK, DEFINE.ExecStop que desmonta el gadget limpiamente. Sin estado residual de configfs, el reinicio funciona sin necesidad de reiniciar el sistema./dev/hidg0 — sin nodos de dispositivo con escritura mundial.+----------------+ Cable USB +-------------+
| Raspberry Pi | ===================>>> | PC Host |
| Zero 2 W | (Pi emula HID + | (objetivo) |
| (este dispositivo)| unidad opcional) | |
+----------------+ +-------------+
|
| /home/pi/pi-badusb/
|
+-- badusb.service ----> monitor_and_run.py
|
| on `state == configured`:
v
run_payload.py
|
v
/dev/hidg0 (gadget HID USB)
badusb.service ejecuta gadget_setup.sh, que utiliza configfs/libcomposite para componer un gadget USB con una función de teclado HID y (opcionalmente) un LUN de almacenamiento masivo respaldado por un archivo de imagen plana en /var/badusb/storage.img.monitor_and_run.py. Sondea /sys/class/udc/<udc>/state y espera a configured — el estado de la especificación USB que significa que un host ha enumerado exitosamente el gadget. (NO usamos la existencia de /dev/hidg0 como señal: eso es cierto desde el momento en que el gadget se vincula al UDC en el arranque, independientemente de si hay algo conectado.)run_payload.py, que analiza payload.txt y escribe informes HID en /dev/hidg0.| Componente | Nota |
|---|---|
| Raspberry Pi Zero 2 W | Probada en la revisión de hardware de 2026-05. La Pi Zero W más antigua con el controlador dwc_otg BCM2835 también funciona en principio, pero este README asume 2 W con dwc2. |
| Cable micro-USB a USB-A | O un adaptador en formato "stick USB" que permita enchufar la Pi directamente a un puerto host. |
| Opcional: cargador separado | Si desea alimentar la Pi desde una fuente que no sea el host para que el escuchador pueda arrancar antes de conectarse a un objetivo (de lo contrario, el puerto host suministra la energía). |
La Pi Zero 2 W tiene dos puertos micro-USB:
PWR IN — solo alimentación, no expone líneas de datos USB al bloque dwc2 OTG.USB — el puerto de datos OTG; aquí es donde se conecta al objetivo.Clone o copie el repositorio en la Pi, luego ejecute el instalador:
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
Después del reinicio, habilite e inicie el servicio:
sudo systemctl enable --now badusb.service
journalctl -u badusb -f
install.sh es idempotente — vuelva a ejecutarlo cada vez que cambie archivos del proyecto. Él:
/boot/firmware (Bookworm+) vs /boot (más antiguo).dtoverlay=dwc2,dr_mode=otg esté activo bajo un bloque [all] en config.txt. Los valores predeterminados de Raspberry Pi Imager colocan esta línea dentro de un filtro [cm5] que no aplica en Pi Zero 2 W; el instalador agrega una anulación marcada para que las re-ejecuciones no la dupliquen.modules-load=dwc2 esté en cmdline.txt.g_ether sigue presente en cmdline.txt (roba el UDC de libcomposite y rompe el modo gadget)./etc/systemd/system/badusb.service./etc/udev/rules.d/99-badusb-hidg.rules para que sea escribible por el grupo .# Iniciar / detener / reiniciar
sudo systemctl start badusb
sudo systemctl stop badusb
sudo systemctl restart badusb # seguro hacerlo mientras está conectado
# Ver en vivo
journalctl -u badusb -f
# Deshabilitar inicio automático al arrancar
sudo systemctl disable badusb
# Ajustar tiempos (crea un drop-in de anulación)
sudo systemctl edit badusb
# (pegue un bloque [Service] con Environment="BADUSB_REARM_COOLDOWN_S=8" etc.)
sudo systemctl restart badusb
El servicio depende de sys-kernel-config.mount y la presencia de un UDC, por lo que no puede disparar cargas útiles antes de que el gadget esté realmente listo.
Editar la carga útil no requiere reinicio — payload.txt se lee fresco en cada conexión.
payload.txt reside en el directorio de instalación. La referencia completa de comandos está en payload_commands.md.
REM Abrir el diálogo Ejecutar y escribir un saludo via Notepad
LAYOUT US
GUI r
DELAY 1500
STRING notepad
ENTER
DELAY 2500
STRINGLN Hola desde la Pi Zero 2 W
VAR $USER="alice"
VAR $COUNT=0
WHILE $COUNT < 3
STRINGLN Hola $USER (iteración $COUNT)
VAR $COUNT = $COUNT + 1
END_WHILE
IF $USER == "alice"
STRINGLN coincidencia
ELSE
STRINGLN fallo
END_IF
Las expresiones matemáticas en VAR pasan por un evaluador seguro que recorre ast — sin nombres, sin llamadas, sin acceso a atributos, solo literales numéricos y + - * / // % **.
HOLD SHIFT
STRINGLN esta línea está en mayúsculas
RELEASE SHIFT
REM Mantener Ctrl+Shift (0x01 + 0x02) y pulsar A
INJECT_MOD 0x03
STRING a
REM Liberar todos los modificadores
INJECT_MOD 0x00
RANDOM_LETTER 12 # 12 letras aleatorias mixtas mayúsculas/minúsculas
RANDOM_NUMBER 6 # 6 dígitos aleatorios
RANDOM_SPECIAL 4 # 4 símbolos ASCII aleatorios
LAYOUT UK # cambiar a mapeos UK ISO para posteriores STRING/STRINGLN
STRING @ " # ~ £ \ | # escribe correctamente en un host con configuración regional UK
Coloque otro archivo en keymaps/ (junto a us.py y uk.py) y la directiva LAYOUT <nombre> lo cargará mediante importlib.
gadget_setup.sh)Estas se pueden anular por invocación configurándolas en el entorno al ejecutar gadget_setup.sh manualmente, o globalmente mediante las directivas Environment= de la unidad.
monitor_and_run.py)Anular con:
sudo systemctl edit badusb
# En el editor:
# [Service]
# Environment="BADUSB_REARM_COOLDOWN_S=8"
# Environment="BADUSB_MIN_INTER_FIRE_S=20"
sudo systemctl restart badusb
/dev/hidg0La implementación anterior probaba la existencia + escritura de /dev/hidg0 como señal de "host conectado". Ese nodo de dispositivo se crea en el momento en que el gadget se vincula al UDC en el arranque — mucho antes de que algún host lo haya enumerado realmente. Así que las cargas útiles se disparaban inmediatamente al encender, independientemente de dónde estuviera conectada la Pi.
La señal confiable es /sys/class/udc/<udc>/state, que informa el estado del dispositivo según la especificación USB. Solo configured significa que el host ha emitido SetConfiguration(1) — el dispositivo ahora es elegible para enviar informes HID.
Detectar la desconexión física en la Pi Zero 2 W es imposible desde el software: la placa no conecta VBUS sense al bloque dwc2 OTG del SoC. Después de una desconexión física:
/sys/class/udc/<udc>/state permanece en configured.current_speed permanece en high-speed.GOTGCTL permanece en 0x000d0000 (bit BSesVld establecido).Entonces, en lugar de esperar una señal que nunca llegará, el escuchador causa activamente la desconexión: después de cada carga útil, escribe "" en el archivo configfs UDC del gadget (que el kernel interpreta como desvinculación), duerme BADUSB_REARM_COOLDOWN_S, luego escribe el nombre del UDC nuevamente para volver a vincular. La siguiente conexión del host produce una transición limpia a configured que el escuchador puede detectar.
Si el operador deja la Pi conectada después de una carga útil, la re-vinculación hace que el host re-enumere el gadget. Para evitar un bucle de disparo descontrolado, entran en juego dos salvaguardas:
BADUSB_MIN_INTER_FIRE_S — si ocurre una transición a configured dentro de esta ventana desde el disparo anterior, suprímelo y vuelve a desvincular. El gadget cicla silenciosamente en segundo plano.BADUSB_MAX_FIRES_PER_MINUTE — límite duro de ventana deslizante. Por encima del límite, el escuchador pausa durante BADUSB_RATELIMIT_PAUSE_S y registra una advertencia.rm -rf en configfsLos archivos de atributos administrados por el kernel de configfs (bcdUSB, idVendor, webusb/*, os_desc/*, …) no se pueden eliminar con rm(2) — el kernel devuelve EPERM. Se liberan solo cuando su directorio padre es eliminado con rmdir. Tanto gadget_setup.sh como teardown_gadget.sh recorren el árbol configfs en el orden canónico de libcomposite — solo rmdir, nunca rm en archivos de atributos — y el kernel limpia el resto automáticamente.
os.write no file.writeEscribir una cadena vacía mediante open(path, "w").write("") no invoca write(2) con cero bytes — el TextIOWrapper de CPython lo omite. Para la desvinculación de configfs (que el kernel interpreta a partir de una escritura de longitud cero después de eliminar el salto de línea), usamos os.write(fd, b"\n") directamente para que la llamada al sistema siempre se emita con al menos un byte.
El parser Ducky tiene una suite de pruebas pytest de 34 pruebas que se ejecuta contra un MockHIDEngine (un reemplazo en memoria para el escritor HID real), por lo que no necesita Pi ni hardware USB.
cd /home/pi/pi-badusb
python3 -m pytest tests/
La cobertura incluye:
safe_eval_math aceptando aritmética, rechazando nombres / llamadas / acceso a atributos / constantes de cadena.evaluate_condition para comparaciones numéricas y de cadenas sensibles a mayúsculas/minúsculas.VAR con =, +=, -=, *=, /=.IF / ELSE / END_IF tomando la rama correcta.WHILE / END_WHILE contando iteraciones para < y <=.cat /sys/class/udc/*/state — debe alcanzar configured cuando el host enumera. Si permanece en not attached, el host no está hablando: pruebe con un cable diferente (algunos son solo de carga) o un puerto host diferente.lsmod | grep dwc2 — debe estar cargado. Si solo está dwc_otg, dtoverlay=dwc2,dr_mode=otg no se está aplicando; vuelva a ejecutar install.sh y reinicie.journalctl -u badusb -f mientras conecta — debe mostrar Host attached. Running payload. dentro de ~2s de la enumeración del host.Este fue un error real que ahora está corregido. Si ocurre, ha revertido a una compilación anterior a os.write. Asegúrese de que monitor_and_run.py coincida con la rama main actual (busque os.write(fd, payload)).
Operation not permitted"Antes de la corrección, gadget_setup.sh usaba rm -rf en configfs. La versión actual usa teardown_gadget() — si ve esos errores, tiene una copia antigua. Vuelva a implementar desde main.
/dev/hidg0"La regla udev necesita un hot-plug para aplicarse, o sudo udevadm trigger y un nuevo inicio de sesión para que el usuario pi recoja el grupo plugdev.
g_ether durante la instalación"Elimine g_ether de cmdline.txt; reclama el UDC antes de que libcomposite pueda vincularse.
El tamaño del almacenamiento masivo se establece con BACKING_SIZE_MB y solo tiene efecto en la primera ejecución cuando se crea la imagen de respaldo. Para redimensionar:
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
Para eso están BADUSB_MIN_INTER_FIRE_S y BADUSB_MAX_FIRES_PER_MINUTE. Ajústelos más altos mediante systemctl edit badusb. Con los valores predeterminados, una Pi dejada conectada se vuelve a disparar cada ~15s durante el primer minuto, luego pausa 60s, luego reanuda.
.
├── README.md Este archivo
├── LICENSE MIT
├── install.sh Instalador idempotente (configuración de firmware, systemd, udev, plugdev)
├── gadget_setup.sh Compone el gadget USB mediante configfs/libcomposite
├── teardown_gadget.sh Desmontaje canónico de configfs (configurado como ExecStop)
├── reload_gadget.sh Ayudante manual de desvinculación/re-vinculación del UDC
├── autorun.sh Envoltorio de lanzamiento manual heredado (se prefiere systemd)
├── monitor_and_run.py Escuchador: espera conexión del host, ejecuta carga útil, fuerza rearme
├── run_payload.py Intérprete de estilo Ducky-Script
├── payload.txt Su carga útil — edite libremente; se relee en cada conexión
├── payload_commands.md Referencia completa de comandos
├── etc/
│ ├── badusb.service Unidad systemd
│ └── 99-badusb-hidg.rules Regla udev para la propiedad de /dev/hidg0
├── keymaps/
│ ├── __init__.py Cargador dinámico de distribuciones
│ ├── us.py US ANSI (predeterminado)
│ └── uk.py UK ISO
└── tests/
├── __init__.py
├── conftest.py Configuración de ruta de pytest
└── test_parser.py 34 pruebas del parser contra MockHIDEngine
libcomposite, el controlador del kernel dwc2.MIT — consulte LICENSE.
configured| Requisito | Por qué |
|---|
| Raspberry Pi OS (Debian Bookworm o Trixie, 64-bit Lite recomendado) | El script de instalación escribe en /boot/firmware/... en Bookworm+ y retrocede a /boot/... en imágenes más antiguas. |
| Python 3 | Viene con Pi OS. |
mkfs.vfat | Para formatear la imagen de respaldo de almacenamiento masivo en la primera ejecución. Deshabilitar si no se usa almacenamiento masivo. |
| Acceso root para la configuración | Toca systemd, udev y /boot/firmware/config.txt. |
/dev/hidg0plugdevpi a plugdev./var/badusb/ para la imagen de respaldo de almacenamiento masivo.| Variable | Valor por defecto | Significado |
|---|
ENABLE_MASS_STORAGE | 1 | 0 para un gadget solo HID. |
BACKING_FILE | /var/badusb/storage.img | Imagen plana expuesta al host. |
BACKING_SIZE_MB | 64 | Creada en la primera ejecución si falta. |
BACKING_LABEL | BADUSB | Etiqueta del volumen FAT. |
MASS_STORAGE_RO | 1 | Solo lectura por defecto. |
| Variable de entorno | Valor por defecto | Significado |
|---|
BADUSB_POST_PAYLOAD_FLUSH_S | 0.5 | Dormir después de la carga útil antes de desvincular para que las escrituras HID drenen. |
BADUSB_REARM_COOLDOWN_S | 5 | Cuánto tiempo permanece el gadget oscuro para el host entre desvincular y volver a vincular. |
BADUSB_MIN_INTER_FIRE_S | 10 | Segundos mínimos entre dos disparos de carga útil; por debajo, el disparo se suprime y el gadget se desvincula nuevamente. |
BADUSB_MAX_FIRES_PER_MINUTE | 6 | Límite duro; por encima, pausa durante BADUSB_RATELIMIT_PAUSE_S. |
BADUSB_RATELIMIT_PAUSE_S | 60 | Duración de la pausa después de activar el límite de tasa. |
RANDOM_* corrección de longitud; RANDOM_<desconocido> sin operación + advertencia.INJECT_MOD persistencia del byte modificador en pulsaciones de tecla posteriores.HOLD SHIFT poniendo en mayúsculas cada carácter en STRINGLN abc.LAYOUT US vs LAYOUT UK produciendo diferentes informes para @ y "; distribución desconocida retrocede a la anterior.STRING_BLOCK uniendo líneas; STRINGLN_BLOCK respetando la sangría mínima.