PsycoStea/Pi-Zero-2W-Bad-USB
GitHub: PsycoStea/Pi-Zero-2W-Bad-USB
该项目将树莓派 Zero 2 W 配置为可编程的 BadUSB 设备,自动检测主机接入并执行兼容 Ducky-Script 的键盘注入攻击载荷。
Stars: 35 | Forks: 1
# Raspberry Pi Zero 2 W BadUSB HID 工具包
一个基于 Raspberry Pi Zero 2 W 构建的可编程 BadUSB / HID 攻击平台。
该 Pi 会枚举为一个 USB 复合设备(键盘外加一个可选的
只读大容量存储卷),并对其所插入的主机
执行兼容 Ducky-Script 的载荷。
专为授权的红队演练、渗透测试和 CTF 设计。
## 目录
1. [功能](#features)
2. [工作原理](#how-it-works)
3. [硬件](#hardware)
4. [安装](#install)
5. [日常操作](#daily-operation)
6. [编写载荷](#writing-payloads)
7. [配置参考](#configuration-reference)
8. [架构说明](#architecture-notes)
9. [测试](#tests)
10. [故障排除](#troubleshooting)
11. [仓库结构](#repository-layout)
12. [致谢](#credits)
## 功能
- **可编程的按键载荷**,采用兼容 Ducky-Script 的方言。
- **可靠的主机接入检测**,通过 UDC `configured` 状态实现 — 开机时
不会误触发载荷。
- **在 Pi Zero 2 W 硬件上切实有效的“攻击间重置”**。
拔出、重新插入、再次触发 — 无需重新上电。
- **可配置的安全防护**,防止设备一直插在主机上时出现
失控循环:支持设置每次触发的最小间隔以及每分钟触发次数的
速率限制,两者均可通过环境变量覆盖。
- **可选的复合大容量存储 gadget**,由扁平映像文件支持
(默认只读 — 暴露实时 SD 卡是以前那种
难以调试的文件系统损坏雷区)。
- **支持美国和英国键盘布局**,可通过 `LAYOUT` 为每个载荷单独选择。
- 支持变量、`IF` / `ELSE`、`WHILE`、`HOLD` / `RELEASE`、`INJECT_MOD`、
`RANDOM_*` 生成器、`STRING_BLOCK` / `STRINGLN_BLOCK`、`DEFINE`。
- **systemd 集成**,包含 `ExecStop` 指令,可干净地卸载 gadget。
不会留下残留的 configfs 状态,重启服务无需重新启动系统。
- **udev 管理的权限**控制 `/dev/hidg0` — 没有全局可写的
设备节点。
- **34 项测试的 pytest 套件**,用于 Ducky 解析器,可在
任何未连接 Pi 的主机上运行。
- 每次 payload 运行后的 **ACT LED 状态指示灯**。
## 工作原理
```
+----------------+ 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)
```
1. 开机时,`badusb.service` systemd 单元会运行 `gadget_setup.sh`,
该脚本使用 configfs/`libcomposite` 组建一个带有
HID 键盘功能以及(可选)一个由位于 `/var/badusb/storage.img` 的
扁平映像文件支持的大容量存储 LUN 的 USB gadget。
2. 随后该单元运行 `monitor_and_run.py`。它会轮询
`/sys/class/udc//state` 并等待 `configured` 状态 — 这是
USB 规范中的一种状态,表示主机已成功枚举
该 gadget。(我们 **不** 使用 `/dev/hidg0` 的存在作为信号:
因为只要 gadget 在开机时绑定到 UDC,该节点就会存在,
无论它是否实际插入了任何主机。)
3. 当主机连接时,监听器会执行 `run_payload.py`,
该脚本会解析 `payload.txt` 并将 HID 报告写入 `/dev/hidg0`。
4. 当 payload 执行完毕后,监听器会**主动从 UDC 解绑**
该 gadget(Pi Zero 2 W 无法通过软件检测物理
断开 — 参见 [架构说明](#architecture-notes)),
休眠一段冷却时间,然后重新绑定。之后设备将等待下一次
`configured` 状态转换。
## 硬件
| 组件 | 备注 |
|-----------|------|
| Raspberry Pi Zero 2 W | 在 2026-05 硬件版本上测试过。带有 BCM2835 dwc_otg 驱动程序的旧版 Pi Zero W 原则上也可行,但本 README 假定使用带有 dwc2 的 2 W。 |
| micro-USB 转 USB-A 数据线 | 或者使用“U盘”外形规格的适配器,让 Pi 可以直接插入主机端口。 |
| 可选:独立的充电器 | 如果您希望从非主机电源为 Pi 供电,以便监听器能在插入目标之前启动(否则将由主机端口提供电源)。 |
Pi Zero 2 W 有两个 micro-USB 端口:
- **`PWR IN`** — 仅供电,不向 dwc2 OTG 模块暴露 USB 数据线。
- **`USB`** — OTG 数据端口;这是您用来连接目标主机的端口。
## 软件
| 需求 | 原因 |
|-------------|-----|
| 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`。 |
## 安装
将仓库克隆或复制到 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` (旧版)。
- 确保 `dtoverlay=dwc2,dr_mode=otg` 在 `config.txt` 中的 `[all]`
块下处于活动状态。Raspberry Pi Imager 的默认设置将此行放在了
不适用于 Pi Zero 2 W 的 `[cm5]` 过滤器内;安装
程序会附加一个带有标记的覆盖项,因此重新运行时不会
重复添加。
- 确保 `cmdline.txt` 中包含 `modules-load=dwc2`。
- 如果 `cmdline.txt` 中仍然存在 `g_ether` 则发出警告(它会从
`libcomposite` 窃取 UDC 并破坏 gadget 模式)。
- 将 systemd 单元安装到 `/etc/systemd/system/badusb.service`。
- 将 udev 规则安装到 `/etc/udev/rules.d/99-badusb-hidg.rules`
,以便 `/dev/hidg0` 可由 `plugdev` 组写入。
- 将 `pi` 用户添加到 `plugdev` 组。
- 创建 `/var/badusb/` 目录用于大容量存储后端映像。
## 日常操作
```
# 启动 / 停止 / 重启
sudo systemctl start badusb
sudo systemctl stop badusb
sudo systemctl restart badusb # safe to do while plugged in
# 实时查看
journalctl -u badusb -f
# 禁用开机自启动
sudo systemctl disable badusb
# 调整时间(创建一个 override drop-in)
sudo systemctl edit badusb
# (粘贴一个带有 Environment="BADUSB_REARM_COOLDOWN_S=8" 等内容的 [Service] 块)
sudo systemctl restart badusb
```
该服务依赖于 `sys-kernel-config.mount` 以及 UDC 的存在,
因此它无法在 gadget 真正准备好之前触发载荷。
**编辑载荷无需重启** — `payload.txt` 会在每次
插入时重新读取。
## 编写载荷
`payload.txt` 位于安装目录中。完整的命令
参考请见 [`payload_commands.md`](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
```
`VAR` 中的数学表达式会经过一个基于 `ast` 遍历器的安全计算器 —
不支持名称、函数调用或属性访问,仅支持数字字面量以及
`+ - * / // % **`。
### 按住修饰键
```
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
```
将另一个文件放入 `keymaps/` 目录中(与 `us.py` 和 `uk.py` 同级),
`LAYOUT ` 指令将通过 `importlib` 自动加载它。
## 配置参考
### 大容量存储 gadget (`gadget_setup.sh` 顶部)
| 变量 | 默认值 | 含义 |
|----------|---------|---------|
| `ENABLE_MASS_STORAGE` | `1` | 设为 `0` 表示仅启用 HID gadget。 |
| `BACKING_FILE` | `/var/badusb/storage.img` | 向主机暴露的扁平映像。 |
| `BACKING_SIZE_MB` | `64` | 如果不存在,则在首次运行时创建。 |
| `BACKING_LABEL` | `BADUSB` | FAT 卷标。 |
| `MASS_STORAGE_RO` | `1` | 默认只读。 |
在手动运行 `gadget_setup.sh` 时,可以通过在环境中设置它们来针对特定调用进行覆盖;或者通过单元的 `Environment=` 指令进行全局覆盖。
### 监听器可调参数 (`monitor_and_run.py`)
| 环境变量 | 默认值 | 含义 |
|---------|---------|---------|
| `BADUSB_POST_PAYLOAD_FLUSH_S` | `0.5` | 在解绑前休眠,以便 HID 写入操作排空。 |
| `BADUSB_REARM_COOLDOWN_S` | `5` | 在解绑和重新绑定之间,gadget 对主机保持不可见的时间长度。 |
| `BADUSB_MIN_INTER_FIRE_S` | `10` | 两次 payload 触发之间的最小秒数;低于此值,将抑制触发并且 gadget 重新解绑。 |
| `BADUSB_MAX_FIRES_PER_MINUTE` | `6` | 硬性上限;超过此值,将暂停 `BADUSB_RATELIMIT_PAUSE_S`。 |
| `BADUSB_RATELIMIT_PAUSE_S` | `60` | 触发速率限制后的暂停持续时间。 |
覆盖方式:
```
sudo systemctl edit badusb
# 在编辑器中:
# [Service]
# Environment="BADUSB_REARM_COOLDOWN_S=8"
# Environment="BADUSB_MIN_INTER_FIRE_S=20"
sudo systemctl restart badusb
```
## 架构说明
### 为什么监听器使用 UDC `state` 而不是 `/dev/hidg0`
之前的实现测试了 `/dev/hidg0` 的存在和可写性
作为“主机已连接”的信号。该设备节点在开机时 gadget 绑定到 UDC 的那一刻就创建好了 — 远早于任何主机实际
枚举它。因此,无论 Pi 插在哪里,payload 都会在开机后立即触发。
可靠的信号是 `/sys/class/udc//state`,它报告的是
USB 规范的设备状态。只有 `configured` 状态才意味着主机已发出
`SetConfiguration(1)` — 此时设备才有资格发送 HID 报告。
### 为什么我们在每次 payload 后强制解绑
在 Pi Zero 2 W 上检测物理断开**无法通过软件实现**:
该板卡没有将 VBUS 检测连接到 SoC 的 dwc2 OTG
模块。物理拔出后:
- `/sys/class/udc//state` 保持为 `configured`。
- `current_speed` 保持为 `high-speed`。
- dwc2 `GOTGCTL` 寄存器保持为 `0x000d0000` (设置了 BSesVld 位)。
- 不会触发任何 udev 事件。
因此,监听器不再等待永远不会到来的信号,而是
*主动引发*断开:每次 payload 执行完后,它会向 gadget 的 `UDC` configfs 文件写入 `""`(内核将其解释为解绑),休眠 `BADUSB_REARM_COOLDOWN_S` 后,再写回 UDC 名称以重新绑定。下一次主机插入将产生一个干净的 `configured` 转换,监听器即可检测到。
如果操作员在 payload 执行完后将 Pi 一直插着,重新绑定会导致主机重新枚举该 gadget。为了防止出现失控的触发循环,将启动两项安全防护措施:
1. `BADUSB_MIN_INTER_FIRE_S` — 如果在距上一次触发的此时间窗口内发生 `configured` 转换,则将其抑制并再次解绑。gadget 会在后台静默循环。
2. `BADUSB_MAX_FIRES_PER_MINUTE` — 滑动窗口硬性上限。超过该上限时,监听器会暂停 `BADUSB_RATELIMIT_PAUSE_S` 并记录一条警告。
### 为什么我们从不在 configfs 上使用 `rm -rf`
configfs 中由内核管理的属性文件(`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_*` 长度正确性;`RANDOM_` 无操作 + 警告。
- `INJECT_MOD` 修饰键字节在后续按键中的持久性。
- `HOLD SHIFT` 将 `STRINGLN abc` 中的每个字符大写。
- `LAYOUT US` 与 `LAYOUT UK` 针对产生不同的报告;未知布局回退到前一个布局。
- `STRING_BLOCK` 连接行;`STRINGLN_BLOCK` 遵循最小缩进。
## 故障排除
### “插入时 Payload 从不触发”
1. `cat /sys/class/udc/*/state` — 当主机枚举时必须达到 `configured` 状态。如果它停留在 `not attached`,说明主机没有通信:尝试更换数据线(有些仅支持充电)或
尝试其他主机端口。
2. `lsmod | grep dwc2` — 必须加载。如果只有 `dwc_otg`,
说明 `dtoverlay=dwc2,dr_mode=otg` 未生效;重新运行 `install.sh`
并重启。
3. 插入时运行 `journalctl -u badusb -f` — 应该在主机枚举后约 2 秒内显示 `Host attached. Running payload.`。
### “Payload 陷入死循环,LED 闪烁,即使已拔出”
这是一个真实存在过的 Bug,现已修复。如果再次发生,说明您回退到了 `os.write` 之前的构建版本。请确保 `monitor_and_run.py` 与当前 main 分支匹配(搜索 `os.write(fd, payload)`)。
### “服务无法重启 — `Operation not permitted`”
修复前的 `gadget_setup.sh` 在 configfs 上使用了 `rm -rf`。当前版本使用 `teardown_gadget()` — 如果您看到这些错误,说明您使用的是旧副本。请从 main 分支重新部署。
### “`/dev/hidg0` 权限拒绝”
udev 规则需要热插拔才能应用,或者执行 `sudo udevadm trigger` 并重新登录,以便 `pi` 用户获取 `plugdev` 组。
### “安装过程中出现 `g_ether` 警告”
从 `cmdline.txt` 中移除 `g_ether`;它会在 `libcomposite` 绑定之前抢占 UDC。
### “主机显示有 U 盘,但大小不是我期望的”
大容量存储大小由 `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
```
### “我想让 Pi 一直插着而不对主机进行垃圾信息轰炸”
这就是 `BADUSB_MIN_INTER_FIRE_S` 和 `BADUSB_MAX_FIRES_PER_MINUTE`
的作用。通过 `systemctl edit badusb` 将它们调高。使用默认值时,一直插着的 Pi 在第一分钟内会每约 15 秒重新触发一次,然后暂停 60 秒,接着恢复。
## 仓库结构
```
.
├── 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
```
## 致谢
- 最初的项目脚本和灵感:**Psycostea**。
- USB gadget 研究:USB Rubber Ducky、Hak5、`libcomposite` 文档、
dwc2 内核驱动程序。
- 2026-05 准确性校对:重写了主机接入检测,确保大容量
存储安全,修复了多个解析器 Bug,添加了键盘映射抽象,
将启动迁移到 systemd,添加了单元测试框架,并
解决了 Pi Zero 2 W 在硬件级别上无法检测
物理断开的问题。
## 许可证
MIT — 详见 [`LICENSE`](LICENSE)。
标签:BadUSB, Ducky Script, HID攻击, Raspberry Pi, 内存执行, 逆向工具