PI DUCKY CONTROLLER
A security-research tool for my own hardware: script and trigger USB HID payloads on machines I own, without reflashing firmware for every change.
OVERVIEW
USB Rubber Ducky-style tools work by replaying keystrokes from a script the moment a device is plugged in. Most implementations bake the script onto the device itself — change the payload, reflash the firmware. For a research tool I'd be iterating on constantly, that loop was too slow.
So I split the system in two: a Next.js control panel where I build and store scripts, and a Raspberry Pi Pico W that only ever asks "what should I type right now?" — never storing logic of its own. The panel exposes a drag-and-drop DuckyScript builder over a REST API; the Pico polls that API every 0.5 seconds and replays whatever it receives as real USB keystrokes.
The result: I edit a script in the browser, and within half a second the target machine is typing it — no drivers, no software running on the target, no firmware flash.
ROLE & CONSTRAINTS
Solo project, end to end: hardware wiring, firmware (CircuitPython on the Pico), the REST API, the control-panel frontend, and the MongoDB store for saved scripts.
- The target machine must see a standard USB HID keyboard — nothing that looks like a security tool from the OS's point of view.
- No software or drivers can run on the target — the entire "client" is the Pico's firmware.
- Iteration speed on scripts mattered more than raw execution latency.
THE DECISION: POLLING OVER PUSH
This is the same trade every embedded system makes between push and pull: push is faster, pull is simpler to reason about when the client is the least reliable part of the system. Here, the Pico — sitting on a breadboard, USB-powered, no persistent storage — was clearly the least reliable part.
WHAT BROKE
Early on, the Pico validated incoming scripts before replaying them — checked the DuckyScript syntax, rejected malformed commands. That seemed safer. In practice it made debugging miserable: when a script failed, I couldn't tell if the bug was in my builder UI, the API, or the Pico's parser, and the Pico's only debug output is a serial console, not a browser dev tools tab.
The fix was to move all validation to the control panel and make the Pico dumb on purpose: it now replays bytes exactly as it receives them, no interpretation. If something's wrong, it's wrong in the browser, where I can actually see it. The Pico became strictly a replay device — which also happens to be the more honest description of what the hardware should be doing in the first place.
TRY IT
A simplified version of the builder — add blocks, run, watch it "replay" the sequence.
RESULTS & LESSONS
The tool works exactly as scoped: edit a script, plug in the Pico, watch it type. It's the piece of my work furthest from typical web development — most of what I do lives in a browser dev tools tab; this one lives in a serial monitor.
The main lesson carried into later projects: push complexity toward the part of the system that's easiest to inspect and iterate on, not the part that's technically "closer" to the problem. The Pico could have been smarter. Making it dumber made the whole system easier to build and debug.