
Configure your Pi Zero 2W to be a BadUSB
一个基于 Raspberry Pi Zero 2 W 构建的可编程 BadUSB / HID 攻击平台。 Pi 枚举为 USB 复合设备(键盘加一个可选的只读大容量存储卷),并在插入的主机上执行 Ducky-Script 风格的载荷。专为授权的红队演练、渗透测试和 CTF 设计。
⚠️ 法律声明。 仅在您拥有或已获得明确书面许可测试的系统上使用。未经授权的访问在大多数司法管辖区是非法的,声称不知道也无济于事。
configured 状态实现——不会在启动时错误触发载荷。LAYOUT 分别为每个载荷选择。IF / ELSE、WHILE、HOLD / RELEASE、INJECT_MOD、RANDOM_* 生成器、STRING_BLOCK / STRINGLN_BLOCK、DEFINE。ExecStop 干净地拆除小工具。不会遗留 configfs 状态,无需重启即可重新启动。/dev/hidg0——没有全局可写的设备节点。+----------------+ USB 数据线 +-------------+
| Raspberry Pi | ===================>>> | 主机 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 systemd 单元运行 gadget_setup.sh,它使用 configfs/libcomposite 组合一个 USB 小工具,包含 HID 键盘功能和(可选)由 /var/badusb/storage.img 平面镜像文件支持的大容量存储 LUN。monitor_and_run.py。它轮询 /sys/class/udc/<udc>/state 并等待 configured——这是 USB 规范状态,表示主机已成功枚举小工具。(我们不使用 /dev/hidg0 的存在作为信号:因为小工具在启动时绑定到 UDC 时该节点就已存在,无论是否插入任何设备。)run_payload.py,解析 payload.txt 并将 HID 报告写入 /dev/hidg0。configured 状态转换。| 组件 | 说明 |
|---|---|
| Raspberry Pi Zero 2 W | 已在 2026-05 硬件版本上测试。较旧的 Pi Zero W 使用 BCM2835 dwc_otg 驱动程序理论上也可行,但本文档假设使用带 dwc2 的 2 W。 |
| micro-USB 转 USB-A 数据线 | 或“U 盘”形式适配器,使 Pi 可直接插入主机端口。 |
| 可选:独立充电器 | 如果希望从非主机源为 Pi 供电,以便监听器在插入目标之前启动(否则主机端口提供电源)。 |
Pi Zero 2 W 有两个 micro-USB 端口:
PWR IN — 仅供电,不向 dwc2 OTG 模块暴露 USB 数据线。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+)与 /boot(旧版)。config.txt 的 [all] 块下启用 dtoverlay=dwc2,dr_mode=otg。Raspberry Pi Imager 默认将此行放在 [cm5] 过滤器中,该过滤器不适用于 Pi Zero 2 W;安装器会附加一个带标记的覆盖项,以便重新运行时不会重复。cmdline.txt 中包含 modules-load=dwc2。cmdline.txt 中仍然存在 g_ether 则发出警告(它会从 libcomposite 中抢占 UDC 并破坏小工具模式)。/etc/systemd/system/badusb.service。/etc/udev/rules.d/99-badusb-hidg.rules,使 /dev/hidg0 对 组可写。# 启动 / 停止 / 重启
sudo systemctl start badusb
sudo systemctl stop badusb
sudo systemctl restart badusb # 插入时安全操作
# 实时查看日志
journalctl -u badusb -f
# 禁用开机自启
sudo systemctl disable badusb
# 调整时间参数(创建覆盖配置)
sudo systemctl edit badusb
# (粘贴一个包含 Environment="BADUSB_REARM_COOLDOWN_S=8" 等的 [Service] 块)
sudo systemctl restart badusb
该服务依赖于 sys-kernel-config.mount 和 UDC 的存在,因此在小工具实际准备就绪之前无法触发载荷。
编辑载荷无需重启——payload.txt 每次插入时都会重新读取。
payload.txt 位于安装目录中。完整命令参考位于 payload_commands.md。
REM 打开运行对话框并通过记事本输入问候语
LAYOUT US
GUI r
DELAY 1500
STRING notepad
ENTER
DELAY 2500
STRINGLN 来自 Raspberry Pi Zero 2 W 的问候
VAR $USER="alice"
VAR $COUNT=0
WHILE $COUNT < 3
STRINGLN 你好 $USER (第 $COUNT 次迭代)
VAR $COUNT = $COUNT + 1
END_WHILE
IF $USER == "alice"
STRINGLN 匹配
ELSE
STRINGLN 未匹配
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 # 切换为 UK ISO 映射,用于后续的 STRING/STRINGLN
STRING @ " # ~ £ \ | # 在 UK 语言环境的主机上正确输入
将另一个文件放入 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 检测连接到 SoC 的 dwc2 OTG 模块。物理拔出后:
/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 处理 configfsconfigfs 的内核管理属性文件(bcdUSB、idVendor、webusb/*、os_desc/*……)不能通过 rm(2) 删除——内核返回 EPERM。它们仅在其父目录被 rmdir 时释放。gadget_setup.sh 和 teardown_gadget.sh 都按规范的 libcomposite 顺序遍历 configfs 树——仅使用 rmdir,从不使用 rm 处理属性文件——内核会自动清理其余内容。
os.write 而不是 file.write通过 open(path, "w").write("") 写入空字符串不会调用 write(2) 发送零字节——CPython 的 TextIOWrapper 会省略它。对于 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 对 < 和 <= 的迭代次数。RANDOM_* 长度正确性; 无操作并警告。cat /sys/class/udc/*/state——主机枚举时必须达到 configured。如果保持 not attached,则主机未通信:尝试不同的数据线(有些仅充电)或不同的主机端口。lsmod | grep dwc2——必须已加载。如果只有 dwc_otg,说明 dtoverlay=dwc2,dr_mode=otg 未生效;重新运行 install.sh 并重启。journalctl -u badusb -f——应在主机枚举后约 2 秒内显示 Host attached. Running payload.。这是一个现已修复的实际 bug。如果发生,说明您已回退到旧版 os.write 之前的版本。确保 monitor_and_run.py 与当前主分支匹配(搜索 os.write(fd, payload))。
Operation not permitted”修复前的 gadget_setup.sh 对 configfs 使用了 rm -rf。当前版本使用 teardown_gadget()——如果看到这些错误,说明您有旧版本。从主分支重新部署。
/dev/hidg0 权限拒绝”udev 规则需要热插拔才能生效,或者运行 sudo udevadm trigger 并重新登录,以便 pi 用户获得 plugdev 组身份。
g_ether 警告”从 cmdline.txt 中移除 g_ether;它会在 libcomposite 绑定之前占用 UDC。
大容量存储大小由 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 通过 configfs/libcomposite 组合 USB 小工具
├── 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 /dev/hidg0 所有权的 udev 规则
├── keymaps/
│ ├── __init__.py 动态布局加载器
│ ├── us.py 美国 ANSI(默认)
│ └── uk.py 英国 ISO
└── tests/
├── __init__.py
├── conftest.py pytest 路径设置
└── test_parser.py 34 个解析器测试(针对 MockHIDEngine)
libcomposite 文档、dwc2 内核驱动。MIT——参见 LICENSE。
| 要求 | 原因 |
|---|
| Raspberry Pi OS(推荐 Debian Bookworm 或 Trixie,64 位 Lite) | 安装脚本在 Bookworm+ 上写入 /boot/firmware/...,在旧镜像上回退到 /boot/...。 |
| Python 3 | Pi OS 自带。 |
mkfs.vfat | 用于首次运行时格式化大容量存储后备镜像。如果禁用大容量存储则无需。 |
| 安装需要 root 权限 | 涉及 systemd、udev 和 /boot/firmware/config.txt。 |
plugdevpi 用户添加到 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_<unknown>INJECT_MOD 修饰键字节在后续按键中的持久性。HOLD SHIFT 将 STRINGLN abc 中的每个字符大写。LAYOUT US 与 LAYOUT UK 对 @ 和 " 产生不同报告;未知布局回退到前一个布局。STRING_BLOCK 连接行;STRINGLN_BLOCK 遵守最小缩进。