
Configure your Pi Zero 2W to be a BadUSB
Uma plataforma de ataque BadUSB / HID programável construída em um Raspberry Pi Zero 2 W. O Pi se enumera como um dispositivo USB composto (teclado mais um volume de armazenamento em massa opcional somente leitura) e executa payloads no estilo Ducky-Script contra o host no qual está conectado. Projetado para engajamentos autorizados de red team, testes de penetração e CTFs.
⚠️ Aviso legal. Use apenas em sistemas que você possui ou para os quais possui permissão explícita por escrito para testar. Acesso não autorizado é ilegal na maioria das jurisdições, e fingir que não sabia não vai ajudar.
configured do UDC — sem disparos espúrios de payload na inicialização.LAYOUT.IF / ELSE, WHILE, HOLD / RELEASE, INJECT_MOD, geradores RANDOM_*, STRING_BLOCK / STRINGLN_BLOCK, DEFINE.ExecStop que desmonta o gadget de forma limpa. Sem estado residual do configfs, a reinicialização funciona sem reiniciar./dev/hidg0 — sem nós de dispositivo com permissão de escrita mundial.+----------------+ Cabo USB +-------------+
| Raspberry Pi | ===================>>> | PC Host |
| Zero 2 W | (Pi emula HID + | (alvo) |
| (este dispositivo)| unidade opcional)| |
+----------------+ +-------------+
|
| /home/pi/pi-badusb/
|
+-- badusb.service ----> monitor_and_run.py
|
| em `state == configured`:
v
run_payload.py
|
v
/dev/hidg0 (gadget HID USB)
badusb.service executa gadget_setup.sh,
que usa configfs/libcomposite para compor um gadget USB com uma
função de teclado HID e (opcionalmente) um LUN de armazenamento em massa apoiado
por um arquivo de imagem plana em /var/badusb/storage.img.monitor_and_run.py. Ele verifica
/sys/class/udc/<udc>/state e aguarda o estado configured —
o estado USB-spec que significa que um host enumerou o gadget com sucesso.
(Nós não usamos a existência de /dev/hidg0 como sinal:
isso é verdade no momento em que o gadget se liga ao UDC na inicialização,
independentemente de algo estar conectado.)run_payload.py,
que analisa payload.txt e escreve relatórios HID em /dev/hidg0.| Componente | Nota |
|---|---|
| Raspberry Pi Zero 2 W | Testado na revisão de hardware de 2026-05. Pi Zero W mais antigo com o driver dwc_otg BCM2835 também funciona em princípio, mas este README assume o 2 W com dwc2. |
| Cabo micro-USB para USB-A | Ou um adaptador com formato de "pen drive" que permite ao Pi se conectar diretamente a uma porta do host. |
| Opcional: carregador separado | Se você quiser alimentar o Pi a partir de uma fonte não host para que o listener possa inicializar antes de ser conectado a um alvo (caso contrário, a porta do host fornece a energia). |
O Pi Zero 2 W tem duas portas micro-USB:
PWR IN — apenas energia, não expõe linhas de dados USB ao bloco OTG dwc2.USB — a porta de dados OTG; é aqui que você conecta ao alvo.Clone ou copie o repositório para o Pi, então execute o 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
Após a reinicialização, habilite e inicie o serviço:
sudo systemctl enable --now badusb.service
journalctl -u badusb -f
install.sh é idempotente — execute-o novamente sempre que alterar os arquivos
do projeto. Ele:
/boot/firmware (Bookworm+) vs /boot (mais antigo).dtoverlay=dwc2,dr_mode=otg esteja ativo sob um bloco [all]
em config.txt. Os padrões do Raspberry Pi Imager colocam esta linha
dentro de um filtro [cm5] que não se aplica ao Pi Zero 2 W; o
instalador anexa uma substituição marcada com sentinela para que execuções
repetidas não a dupliquem.modules-load=dwc2 esteja em cmdline.txt.g_ether ainda estiver presente em cmdline.txt (ele rouba o
UDC do libcomposite e quebra o modo gadget)./etc/systemd/system/badusb.service./etc/udev/rules.d/99-badusb-hidg.rules
para que seja gravável pelo grupo .# Iniciar / parar / reiniciar
sudo systemctl start badusb
sudo systemctl stop badusb
sudo systemctl restart badusb # seguro fazer enquanto conectado
# Acompanhar ao vivo
journalctl -u badusb -f
# Desabilitar início automático na inicialização
sudo systemctl disable badusb
# Ajustar temporizações (cria um drop-in de substituição)
sudo systemctl edit badusb
# (cole um bloco [Service] com Environment="BADUSB_REARM_COOLDOWN_S=8" etc.)
sudo systemctl restart badusb
O serviço depende de sys-kernel-config.mount e da presença de um UDC,
portanto não pode disparar payloads antes do gadget estar realmente pronto.
Editar o payload não requer reinicialização — payload.txt é lido
novamente a cada conexão.
payload.txt fica no diretório de instalação. A referência completa de comandos
está em payload_commands.md.
REM Abrir diálogo Executar e digitar uma saudação via Bloco de Notas
LAYOUT US
GUI r
DELAY 1500
STRING notepad
ENTER
DELAY 2500
STRINGLN Olá do Pi Zero 2 W
VAR $USER="alice"
VAR $COUNT=0
WHILE $COUNT < 3
STRINGLN Olá $USER (iteração $COUNT)
VAR $COUNT = $COUNT + 1
END_WHILE
IF $USER == "alice"
STRINGLN correspondido
ELSE
STRINGLN não correspondido
END_IF
Expressões matemáticas em VAR passam por um avaliador seguro que percorre a ast —
sem nomes, sem chamadas, sem acesso a atributos, apenas literais numéricos e
+ - * / // % **.
HOLD SHIFT
STRINGLN esta linha está em maiúsculas
RELEASE SHIFT
REM Segurar Ctrl+Shift (0x01 + 0x02) e pressionar A
INJECT_MOD 0x03
STRING a
REM Liberar todos os modificadores
INJECT_MOD 0x00
RANDOM_LETTER 12 # 12 letras aleatórias maiúsculas/minúsculas
RANDOM_NUMBER 6 # 6 dígitos aleatórios
RANDOM_SPECIAL 4 # 4 símbolos ASCII aleatórios
LAYOUT UK # alternar para mapeamentos ISO do Reino Unido para STRING/STRINGLN subsequentes
STRING @ " # ~ £ \ | # digita corretamente em um host com locale UK
Coloque outro arquivo em keymaps/ (ao lado de us.py e uk.py) e
a diretiva LAYOUT <nome> o capturará via importlib.
gadget_setup.sh)Estes podem ser substituídos por invocação, definindo-os no ambiente
ao executar gadget_setup.sh manualmente, ou globalmente através das
diretivas Environment= da unidade.
monitor_and_run.py)Substitua com:
sudo systemctl edit badusb
# No editor:
# [Service]
# Environment="BADUSB_REARM_COOLDOWN_S=8"
# Environment="BADUSB_MIN_INTER_FIRE_S=20"
sudo systemctl restart badusb
/dev/hidg0A implementação anterior testava a existência + capacidade de escrita de /dev/hidg0
como sinal de "host conectado". Esse nó de dispositivo é criado no momento em que o gadget
se liga ao UDC na inicialização — muito antes de qualquer host realmente o ter
enumerado. Assim, os payloads eram disparados imediatamente após a energização,
independentemente de onde o Pi estava conectado.
O sinal confiável é /sys/class/udc/<udc>/state, que reporta o estado do
dispositivo conforme a especificação USB. Apenas configured significa que o host
emitiu SetConfiguration(1) — o dispositivo agora está apto a enviar relatórios HID.
Detectar desconexão física no Pi Zero 2 W é impossível via software: a placa não conecta o sense VBUS ao bloco OTG dwc2 do SoC. Após uma desconexão física:
/sys/class/udc/<udc>/state permanece em configured.current_speed permanece em high-speed.GOTGCTL permanece em 0x000d0000 (bit BSesVld definido).Então, em vez de esperar por um sinal que nunca virá, o listener
ativamente causa a desconexão: após cada payload, ele escreve "" no
arquivo UDC do configfs (que o kernel interpreta como desligamento),
dorme BADUSB_REARM_COOLDOWN_S, então escreve o nome do UDC de volta
para religar. A próxima conexão do host produz uma transição configured
limpa que o listener pode detectar.
Se o operador deixar o Pi conectado após um payload, o religamento faz com que o host reenumerie o gadget. Para evitar um loop de disparo descontrolado, duas salvaguardas entram em ação:
BADUSB_MIN_INTER_FIRE_S — se uma transição configured ocorrer
dentro desta janela do disparo anterior, suprima-o e desligue
novamente. O gadget cicla silenciosamente em segundo plano.BADUSB_MAX_FIRES_PER_MINUTE — limite máximo com janela deslizante. Acima do
limite, o listener pausa por BADUSB_RATELIMIT_PAUSE_S e registra um
aviso.rm -rf no configfsOs arquivos de atributo gerenciados pelo kernel do configfs (bcdUSB, idVendor,
webusb/*, os_desc/*, …) não podem ser removidos por rm(2) — o kernel
retorna EPERM. Eles são liberados apenas quando seu diretório pai
é removido com rmdir. Tanto gadget_setup.sh quanto teardown_gadget.sh
percorrem a árvore configfs na ordem canônica do libcomposite — apenas rmdir,
nunca rm em arquivos de atributo — e o kernel limpa o resto
automaticamente.
os.write e não file.writeEscrever uma string vazia via open(path, "w").write("") não
invoca write(2) com zero bytes — o TextIOWrapper do CPython o elimina.
Para o desligamento via configfs (que o kernel interpreta a partir de uma
escrita de comprimento zero após remover a nova linha), usamos os.write(fd, b"\n") diretamente para que a chamada de sistema seja sempre emitida com pelo menos um byte.
O parser Ducky possui um conjunto de testes pytest com 34 testes que são executados contra
um MockHIDEngine (um substituto em memória para o escritor HID real), portanto
não precisa de Pi nem de hardware USB.
cd /home/pi/pi-badusb
python3 -m pytest tests/
A cobertura inclui:
safe_eval_math aceitando aritmética, rejeitando nomes / chamadas /
acesso a atributos / constantes de string.evaluate_condition para comparações numéricas e de strings com distinção entre maiúsculas/minúsculas.VAR com =, +=, -=, *=, /=.IF / ELSE / END_IF tomando o ramo correto.WHILE / END_WHILE contagens de iteração para < e <=.cat /sys/class/udc/*/state — deve atingir configured quando o
host enumerar. Se permanecer em not attached, o host não está
se comunicando: tente um cabo diferente (alguns são apenas para carga) ou uma
porta de host diferente.lsmod | grep dwc2 — deve estar carregado. Se apenas dwc_otg estiver lá,
dtoverlay=dwc2,dr_mode=otg não está sendo aplicado; execute install.sh
novamente e reinicie.journalctl -u badusb -f enquanto conecta — deve mostrar
Host attached. Running payload. dentro de ~2s da enumeração do host.Este era um bug real que agora foi corrigido. Se acontecer, você reverteu
para uma versão anterior ao os.write. Certifique-se de que monitor_and_run.py
corresponde ao branch main atual (procure por os.write(fd, payload)).
Operation not permitted"A versão anterior do gadget_setup.sh usava rm -rf no configfs. A versão
atual usa teardown_gadget() — se você vir esses erros, tem uma
cópia antiga. Reimplante a partir do main.
/dev/hidg0"A regra udev precisa de uma conexão a quente para ser aplicada, ou sudo udevadm trigger
e um novo login para que o usuário pi pegue o grupo plugdev.
g_ether durante a instalação"Remova g_ether de cmdline.txt; ele reivindica o UDC antes que
libcomposite possa se ligar.
O tamanho do armazenamento em massa é definido por BACKING_SIZE_MB e só entra em vigor na
primeira execução quando a imagem de apoio é criada. 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 isso que servem BADUSB_MIN_INTER_FIRE_S e BADUSB_MAX_FIRES_PER_MINUTE.
Defina-os mais altos via systemctl edit badusb. Com os
padrões, um Pi deixado conectado dispara novamente a cada ~15s no primeiro
minuto, então pausa por 60s, depois retoma.
.
├── README.md Este arquivo
├── LICENSE MIT
├── install.sh Instalador idempotente (config do firmware, systemd, udev, plugdev)
├── gadget_setup.sh Compõe o gadget USB via configfs/libcomposite
├── teardown_gadget.sh Desmontagem canônica do configfs (configurado como ExecStop)
├── reload_gadget.sh Helper manual de desligar/religar UDC
├── autorun.sh Wrapper de execução manual legado (systemd é preferido)
├── monitor_and_run.py Listener: aguarda conexão do host, executa payload, força religamento
├── run_payload.py Interpretador estilo Ducky-Script
├── payload.txt Seu payload — edite à vontade; relido a cada conexão
├── payload_commands.md Referência completa de comandos
├── etc/
│ ├── badusb.service Unidade systemd
│ └── 99-badusb-hidg.rules Regra udev para propriedade de /dev/hidg0
├── keymaps/
│ ├── __init__.py Carregador dinâmico de layout
│ ├── us.py US ANSI (padrão)
│ └── uk.py UK ISO
└── tests/
├── __init__.py
├── conftest.py Configuração de caminho do pytest
└── test_parser.py 34 testes do parser contra um MockHIDEngine
libcomposite,
o driver do kernel dwc2.MIT — veja LICENSE.
configured| Requisito | Por quê |
|---|
| Raspberry Pi OS (Debian Bookworm ou Trixie, 64-bit Lite recomendado) | O script de instalação escreve em /boot/firmware/... no Bookworm+ e volta para /boot/... em imagens mais antigas. |
| Python 3 | Vem com Pi OS. |
mkfs.vfat | Para formatar a imagem de apoio do armazenamento em massa na primeira execução. Desative se desabilitar o armazenamento em massa. |
| Acesso root para configuração | Mexe com systemd, udev e /boot/firmware/config.txt. |
/dev/hidg0plugdevpi ao grupo plugdev./var/badusb/ para a imagem de apoio do armazenamento em massa.| Variável | Padrão | Significado |
|---|
ENABLE_MASS_STORAGE | 1 | 0 para um gadget apenas HID. |
BACKING_FILE | /var/badusb/storage.img | Imagem plana exposta ao host. |
BACKING_SIZE_MB | 64 | Criada na primeira execução se ausente. |
BACKING_LABEL | BADUSB | Rótulo do volume FAT. |
MASS_STORAGE_RO | 1 | Somente leitura por padrão. |
| Variável de ambiente | Padrão | Significado |
|---|
BADUSB_POST_PAYLOAD_FLUSH_S | 0.5 | Dormir após o payload antes de desvincular para que as escritas HID sejam drenadas. |
BADUSB_REARM_COOLDOWN_S | 5 | Quanto tempo o gadget fica invisível para o host entre desvincular e religar. |
BADUSB_MIN_INTER_FIRE_S | 10 | Segundos mínimos entre dois disparos de payload; abaixo disso, o disparo é suprimido e o gadget se desvincula novamente. |
BADUSB_MAX_FIRES_PER_MINUTE | 6 | Limite máximo; acima disso, pausa por BADUSB_RATELIMIT_PAUSE_S. |
BADUSB_RATELIMIT_PAUSE_S | 60 | Duração da pausa após acionamento do limite de taxa. |
RANDOM_* correção de comprimento; RANDOM_<desconhecido> sem operação + aviso.INJECT_MOD persistência do byte modificador através de teclas subsequentes.HOLD SHIFT capitalizando cada caractere em STRINGLN abc.LAYOUT US vs LAYOUT UK produzindo relatórios diferentes para @ e
"; layout desconhecido volta para o anterior.STRING_BLOCK unindo linhas; STRINGLN_BLOCK respeitando recuo mínimo.