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, 内存执行, 逆向工具