
Configure your Pi Zero 2W to be a BadUSB
Программируемая платформа для атак BadUSB / HID, построенная на Raspberry Pi Zero 2 W. Pi эмулирует составное USB-устройство (клавиатуру плюс опциональный доступный только для чтения том массового хранения) и выполняет пейлоады в стиле Ducky-Script на хосте, к которому она подключена. Предназначена для авторизованных заданий red-команд, тестирования на проникновение и CTF.
⚠️ Юридическое уведомление. Используйте только на системах, которыми вы владеете или на которые имеете явное письменное разрешение на тестирование. Несанкционированный доступ незаконен в большинстве юрисдикций, и притворство, что вы не знали, не поможет.
configured — никаких
ложных запусков пейлоада при загрузке.LAYOUT.IF / ELSE, WHILE, HOLD / RELEASE, INJECT_MOD,
генераторы RANDOM_*, STRING_BLOCK / STRINGLN_BLOCK, DEFINE.ExecStop, который корректно отключает гаджет.
Никаких остаточных состояний configfs, перезапуск работает без перезагрузки./dev/hidg0 — никаких узлов устройств,
доступных на запись всем.+----------------+ USB cable +-------------+
| Raspberry Pi | ===================>>> | Host PC |
| Zero 2 W | (Pi эмулирует HID + | (целевой) |
| (это устройство)| опциональный диск) | |
+----------------+ +-------------+
|
| /home/pi/pi-badusb/
|
+-- badusb.service ----> monitor_and_run.py
|
| при `state == configured`:
v
run_payload.py
|
v
/dev/hidg0 (USB HID гаджет)
badusb.service запускает gadget_setup.sh,
который с помощью configfs/libcomposite собирает USB-гаджет с
функцией HID-клавиатуры и (опционально) LUN массового хранения на основе
плоского образа файла в /var/badusb/storage.img.monitor_and_run.py. Он опрашивает
/sys/class/udc/<udc>/state и ждёт configured — состояние USB-спецификации,
означающее, что хост успешно перечислил гаджет. (Мы не используем существование
/dev/hidg0 как сигнал: оно истинно в момент привязки гаджета к UDC при загрузке,
независимо от того, что-либо подключено или нет.)run_payload.py,
который разбирает payload.txt и записывает HID-отчёты в /dev/hidg0.| Компонент | Примечание |
|---|---|
| Raspberry Pi Zero 2 W | Протестировано на ревизии оборудования 2026-05. Более старые Pi Zero W с драйвером BCM2835 dwc_otg также работают в принципе, но этот README предполагает 2 W с dwc2. |
| Кабель micro-USB → USB-A | Или переходник в форм-факторе «USB-флешки», позволяющий подключить Pi напрямую к порту хоста. |
| Опционально: отдельное зарядное устройство | Если вы хотите питать Pi от источника, отличного от хоста, чтобы слушатель мог загрузиться до подключения к целевому устройству (в противном случае питание подаётся от порта хоста). |
Pi Zero 2 W имеет два micro-USB порта:
PWR IN — только питание, не выставляет линии данных USB на блок OTG dwc2.USB — порт данных OTG; сюда подключаетесь к целевому устройству.Клонируйте или скопируйте репозиторий на Pi, затем запустите установщик:
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
После перезагрузки включите и запустите сервис:
sudo systemctl enable --now badusb.service
journalctl -u badusb -f
install.sh идемпотентен — запускайте его повторно при изменении файлов проекта.
Он:
/boot/firmware (Bookworm+) vs /boot (старые версии).dtoverlay=dwc2,dr_mode=otg активен под блоком [all]
в config.txt. По умолчанию Raspberry Pi Imager помещает эту строку
внутрь фильтра [cm5], который не применяется на Pi Zero 2 W; установщик
добавляет переопределение с меткой, чтобы повторные запуски не дублировали её.modules-load=dwc2 присутствует в cmdline.txt.g_ether всё ещё присутствует в cmdline.txt (он перехватывает
UDC у libcomposite и ломает режим гаджета)./etc/systemd/system/badusb.service./etc/udev/rules.d/99-badusb-hidg.rules,
чтобы был доступен для записи группе .# Запуск / остановка / перезапуск
sudo systemctl start badusb
sudo systemctl stop badusb
sudo systemctl restart badusb # безопасно, даже если подключено
# Просмотр лога в реальном времени
journalctl -u badusb -f
# Отключение автозапуска при загрузке
sudo systemctl disable badusb
# Настройка таймингов (создаёт переопределение drop-in)
sudo systemctl edit badusb
# (вставьте блок [Service] с Environment="BADUSB_REARM_COOLDOWN_S=8" и т.д.)
sudo systemctl restart badusb
Сервис зависит от sys-kernel-config.mount и наличия UDC,
поэтому он не сможет запустить пейлоады, пока гаджет не будет действительно готов.
Редактирование пейлоада не требует перезапуска — payload.txt читается заново
при каждом подключении.
payload.txt находится в каталоге установки. Полная справка по командам —
в payload_commands.md.
REM Открыть диалог Run и напечатать приветствие через 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
Математические выражения в VAR проходят через безопасный вычислитель обхода ast —
никаких имён, вызовов, доступа к атрибутам, только числовые литералы и
+ - * / // % **.
HOLD SHIFT
STRINGLN эта строка ЗАГЛАВНЫМИ
RELEASE SHIFT
REM Удерживать Ctrl+Shift (0x01 + 0x02) и нажать A
INJECT_MOD 0x03
STRING a
REM Отпустить все модификаторы
INJECT_MOD 0x00
RANDOM_LETTER 12 # 12 случайных букв в разном регистре
RANDOM_NUMBER 6 # 6 случайных цифр
RANDOM_SPECIAL 4 # 4 случайных символа ASCII
LAYOUT UK # переключиться на британскую раскладку для последующих STRING/STRINGLN
STRING @ " # ~ £ \ | # правильно печатается на хосте с британской раскладкой
Поместите ещё один файл в keymaps/ (рядом с us.py и uk.py), и
директива LAYOUT <name> подхватит его через importlib.
gadget_setup.sh)Эти значения можно переопределить для каждого вызова, установив их в
окружении при ручном запуске gadget_setup.sh, или глобально через
директивы Environment= юнита.
monitor_and_run.py)Переопределение:
sudo systemctl edit badusb
# В редакторе:
# [Service]
# Environment="BADUSB_REARM_COOLDOWN_S=8"
# Environment="BADUSB_MIN_INTER_FIRE_S=20"
sudo systemctl restart badusb
state, а не /dev/hidg0В предыдущей реализации проверка существования и доступности для записи /dev/hidg0
использовалась как сигнал «хост подключён». Этот узел устройства создаётся в момент
привязки гаджета к UDC при загрузке — задолго до того, как какой-либо хост фактически
его перечислил. Поэтому пейлоады запускались сразу после включения питания независимо
от того, куда был подключён Pi.
Надёжным сигналом является /sys/class/udc/<udc>/state, который сообщает
состояние устройства по USB-спецификации. Только configured означает, что хост
выдал SetConfiguration(1) — теперь устройство может отправлять HID-отчёты.
Обнаружение физического отключения на Pi Zero 2 W невозможно программно: плата не подключает VBUS sense к блоку OTG dwc2 SoC. После физического отключения:
/sys/class/udc/<udc>/state остаётся configured.current_speed остаётся high-speed.GOTGCTL остаётся 0x000d0000 (бит BSesVld установлен).Поэтому вместо ожидания сигнала, который никогда не придёт, слушатель активно вызывает
отключение: после каждого пейлоада он записывает "" в файл UDC configfs гаджета
(что ядро интерпретирует как отвязку), ждёт BADUSB_REARM_COOLDOWN_S, затем записывает
имя UDC обратно для повторной привязки. Следующее подключение хоста порождает чистый
переход в configured, который слушатель может обнаружить.
Если оператор оставляет Pi подключённым после пейлоада, повторная привязка вызывает повторное перечисление гаджета хостом. Чтобы предотвратить бесконечный цикл срабатываний, включены две меры защиты:
BADUSB_MIN_INTER_FIRE_S — если переход в configured происходит
в пределах этого окна после предыдущего срабатывания, подавить его и снова отвязать.
Гаджет циклически молча в фоне.BADUSB_MAX_FIRES_PER_MINUTE — скользящее жёсткое ограничение. При превышении
лимита слушатель приостанавливается на BADUSB_RATELIMIT_PAUSE_S и логирует
предупреждение.rm -rf для configfsФайлы атрибутов, управляемые ядром, в configfs (bcdUSB, idVendor,
webusb/*, os_desc/*, …) не могут быть удалены через rm(2) — ядро
возвращает EPERM. Они освобождаются только при rmdir их родительского
каталога. Как gadget_setup.sh, так и teardown_gadget.sh обходят дерево
configfs в каноническом порядке libcomposite — только rmdir,
никогда rm для файлов атрибутов — и ядро автоматически очищает остальное.
os.write, а не file.writeЗапись пустой строки через open(path, "w").write("") не вызывает
write(2) с нулевым количеством байт — TextIOWrapper CPython пропускает его.
Для отвязки configfs (которую ядро интерпретирует как запись нулевой длины
после обрезания символа новой строки) мы используем непосредственно
os.write(fd, b"\n"), чтобы системный вызов всегда выполнялся хотя бы
с одним байтом.
Парсер Ducky имеет набор из 34 тестов pytest, которые запускаются против
MockHIDEngine (внутренний заменяющий модуль для реального записи HID), поэтому
не требуется ни Pi, ни USB-оборудования.
cd /home/pi/pi-badusb
python3 -m pytest tests/
Покрытие включает:
safe_eval_math – принимает арифметику, отклоняет имена / вызовы /
доступ к атрибутам / строковые константы.evaluate_condition – для числовых и чувствительных к регистру строковых сравнений.VAR с =, +=, -=, *=, /=.IF / ELSE / END_IF – выбор правильной ветки.WHILE / END_WHILE – количество итераций для < и <=.cat /sys/class/udc/*/state — должно достигнуть configured, когда хост
перечисляет устройство. Если остаётся not attached, хост не общается:
попробуйте другой кабель (некоторые только для зарядки) или другой порт хоста.lsmod | grep dwc2 — должен быть загружен. Если только dwc_otg,
dtoverlay=dwc2,dr_mode=otg не применяется; перезапустите install.sh
и перезагрузитесь.journalctl -u badusb -f при подключении — должно показывать
Host attached. Running payload. в течение ~2 секунд после перечисления хоста.Это была реальная ошибка, которая теперь исправлена. Если это происходит,
вы вернулись к версии до os.write. Убедитесь, что monitor_and_run.py соответствует
текущей основной ветке (ищите os.write(fd, payload)).
До исправления gadget_setup.sh использовал rm -rf для configfs. Текущая
версия использует teardown_gadget() — если вы видите эти ошибки, у вас
старая копия. Повторно разверните из основной ветки.
Правило udev требует горячего подключения для применения, или
sudo udevadm trigger и повторного входа в систему, чтобы пользователь pi
приобрёл группу plugdev.
Удалите g_ether из cmdline.txt; он захватывает UDC до того, как
libcomposite сможет привязаться.
Размер массового хранения задаётся BACKING_SIZE_MB и вступает в силу только
при первом запуске, когда создаётся образ. Чтобы изменить размер:
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
Для этого предназначены BADUSB_MIN_INTER_FIRE_S и BADUSB_MAX_FIRES_PER_MINUTE.
Установите их выше через systemctl edit badusb. С настройками по умолчанию
Pi, оставленный подключённым, будет срабатывать каждые ~15 секунд в течение первой
минуты, затем сделает паузу на 60 секунд, затем возобновит.
.
├── README.md Этот файл
├── LICENSE MIT
├── install.sh Идемпотентный установщик (конфигурация прошивки, systemd, udev, plugdev)
├── gadget_setup.sh Собирает USB-гаджет через configfs/libcomposite
├── teardown_gadget.sh Каноническое отключение configfs (используется как ExecStop)
├── reload_gadget.sh Вспомогательный скрипт для ручной отвязки/привязки UDC
├── autorun.sh Устаревшая обёртка для ручного запуска (рекомендуется systemd)
├── monitor_and_run.py Слушатель: ждёт подключения хоста, запускает пейлоад, принудительно перезаряжает
├── run_payload.py Интерпретатор в стиле Ducky-Script
├── payload.txt Ваш пейлоад — редактируйте свободно; перечитывается при каждом подключении
├── payload_commands.md Полная справка по командам
├── etc/
│ ├── badusb.service systemd-юнит
│ └── 99-badusb-hidg.rules правило udev для /dev/hidg0
├── keymaps/
│ ├── __init__.py Динамический загрузчик раскладок
│ ├── us.py US ANSI (по умолчанию)
│ └── uk.py UK ISO
└── tests/
├── __init__.py
├── conftest.py Настройка путей для pytest
└── test_parser.py 34 теста парсера против MockHIDEngine
libcomposite,
драйвер ядра dwc2.MIT — см. LICENSE.
configured| Требование | Зачем |
|---|
| Raspberry Pi OS (Debian Bookworm или Trixie, рекомендуется 64-битная Lite) | Скрипт установки пишет в /boot/firmware/... на Bookworm+ и делает откат к /boot/... на старых образах. |
| Python 3 | Поставляется с Pi OS. |
mkfs.vfat | Для форматирования образа массового хранения при первом запуске. Отключите, если массовое хранение не используется. |
| Доступ root для настройки | Затрагивает systemd, udev и /boot/firmware/config.txt. |
/dev/hidg0plugdevpi в группу plugdev./var/badusb/ для образа массового хранения.| Переменная | По умолчанию | Значение |
|---|
ENABLE_MASS_STORAGE | 1 | 0 для гаджета только HID. |
BACKING_FILE | /var/badusb/storage.img | Плоский образ, доступный хосту. |
BACKING_SIZE_MB | 64 | Создаётся при первом запуске, если отсутствует. |
BACKING_LABEL | BADUSB | Метка тома FAT. |
MASS_STORAGE_RO | 1 | Только чтение по умолчанию. |
| Переменная окружения | По умолчанию | Значение |
|---|
BADUSB_POST_PAYLOAD_FLUSH_S | 0.5 | Задержка после пейлоада перед отвязкой, чтобы HID-записи были отправлены. |
BADUSB_REARM_COOLDOWN_S | 5 | Как долго гаджет остаётся невидимым для хоста между отвязкой и повторной привязкой. |
BADUSB_MIN_INTER_FIRE_S | 10 | Минимальное количество секунд между двумя срабатываниями пейлоада; если меньше, срабатывание подавляется и гаджет отвязывается снова. |
BADUSB_MAX_FIRES_PER_MINUTE | 6 | Жёсткий лимит; при превышении пауза на BADUSB_RATELIMIT_PAUSE_S. |
BADUSB_RATELIMIT_PAUSE_S | 60 | Длительность паузы после превышения лимита. |
RANDOM_* – корректность длины; RANDOM_<неизвестно> – no-op + предупреждение.INJECT_MOD – сохранение байта модификатора между последующими нажатиями.HOLD SHIFT – преобразование каждого символа в STRINGLN abc в заглавный.LAYOUT US vs LAYOUT UK – разные отчёты для @ и "; неизвестная раскладка
возвращается к предыдущей.STRING_BLOCK – объединение строк; STRINGLN_BLOCK – учёт минимального отступа.