Ducky Script · Volume 5
Ducky Script Volume 5 — Smart Payloads: Detection, Jitter & Timing Discipline
OS detection, reading the host's keyboard-lock state, jitter, randomization, and the timing discipline that makes a payload reliable instead of lucky
Contents
1. About this volume
Vol 4 gave the payload the machinery to make decisions — IF, WHILE, VAR. This volume gives it something to decide on. A conditional is only as good as the information it branches on, and a 1.0 payload had none. Ducky Script 3.0’s “smart payload” features are the inputs: what OS am I on, what is the host doing, has the operator pressed the button — plus the evasion features (jitter, randomization) that make the payload’s output harder to detect.
This is the volume that turns “a script that types” into “a payload that adapts.” It is also, with Vol 6, the volume that most distinguishes a 3.0 payload from a 1.0 one.
2. The blind-typing problem, restated
Vol 3 §9 established it; this volume solves as much of it as the language can. A keystroke-injection device cannot see the screen — not in 1.0, not in 3.0, not on any of the four devices. What 3.0 added is a small set of indirect signals the payload can read:
What a payload can and cannot perceive
════════════════════════════════════════════════════════
CANNOT see: the screen, window focus, whether a command
worked, whether a dialog appeared, the
contents of anything
CAN read: $_OS .................. which OS (set at runtime)
lock-key LED state .... CAPS / NUM / SCROLL on/off
the button ............ pressed / not, timeout
its own state ......... current VID/PID/attackmode
The art of a smart payload is doing as much as possible
with that thin set of signals — and the lock-key channel
is the cleverest of them (and the basis of exfiltration —
Vol 6).
Everything in this volume is built on that table. Detection (§§3-6) reads those signals; evasion (§§7-8) shapes the output; discipline (§9) is how you combine them into a payload that works on a machine you have never seen.
3. $_OS — OS detection
$_OS is the internal variable holding the detected operating system of the host. It lets a single payload branch its entire behaviour on the target:
IF ($_OS == WINDOWS) THEN
REM Windows path — GUI r, cmd, etc.
ELSE
IF ($_OS == MAC) THEN
REM macOS path — GUI SPACE, terminal, etc.
ELSE
REM Linux / other path
END_IF
END_IF
This is the single most valuable thing 3.0 detection gives you, because the same payload line means different things on different OSes (Vol 3 §7) — GUI r is the Run dialog on Windows and useless on macOS. Before $_OS, a payload was either OS-specific by construction or unreliable. With it, a payload is genuinely cross-platform.
The honest caveat: OS detection is inference, not a system call — the device cannot ask the OS “what are you.” It is derived from observable USB/HID behaviour, and like all inference it can be wrong on an unusual host. A robust payload treats $_OS as a strong hint, still uses defensive DELAYs, and where the cost of a wrong branch is high, confirms with a lock-key check (§5) before committing.
4. Lock-key state — the host’s only back-channel
Here is the clever part. A USB keyboard is mostly a one-way device — it sends keystrokes to the host. But there is one channel that flows the other way: the host tells the keyboard the state of the lock-key LEDs — Caps Lock, Num Lock, Scroll Lock. That is how your keyboard’s Caps Lock light knows to turn on when any keyboard (or the OS) toggles it.
A keystroke-injection device is a keyboard, so it receives those LED-state updates too. That makes the lock keys a genuine, if narrow, host → device back-channel:
The lock-key back-channel
════════════════════════════════════════════════════════
device ──── keystrokes ──────────────► host
◄─── CAPS/NUM/SCROLL LED state ─ host
The host BROADCASTS lock-key LED state to every keyboard.
A keystroke-injection device is a keyboard. Therefore it
can READ that state. Three bits of information from the
host — and you can make the host SET them on purpose.
This is:
• a synchronisation signal (§5 — WAIT_FOR_*)
• the basis of exfiltration (Vol 6 — Keystroke Reflection)
The internal variables exposing it: $_CAPSLOCK_ON, $_NUMLOCK_ON, $_SCROLLLOCK_ON (current state), and $_SAVED_CAPSLOCK_ON / etc. (a state you have stashed). Plus SAVE_HOST_KEYBOARD_LOCK_STATE and RESTORE_HOST_KEYBOARD_LOCK_STATE to snapshot and put back the host’s lock-key state — courtesy, so your payload doesn’t leave the target’s Caps Lock on.
Three bits is not much. But it is something, and a payload that uses it is no longer fully blind.
5. WAIT_FOR_* — synchronising on host state
The WAIT_FOR_* family halts the payload until a lock-key event happens — turning the back-channel from §4 into a synchronisation primitive:
WAIT_FOR_CAPS_ON WAIT_FOR_CAPS_OFF WAIT_FOR_CAPS_CHANGE
WAIT_FOR_NUM_ON WAIT_FOR_NUM_OFF WAIT_FOR_NUM_CHANGE
WAIT_FOR_SCROLL_ON WAIT_FOR_SCROLL_OFF WAIT_FOR_SCROLL_CHANGE
The power move: a payload can make the host change a lock key as a “ready” signal, then wait for it. The classic pattern — instead of guessing how long a window takes to open with a DELAY, you type a command that toggles caps lock when it finishes, and WAIT_FOR_CAPS_CHANGE blocks until it actually does:
Guessed DELAY vs. Confirmed wait
───────────── ──────────────
GUI r GUI r
DELAY 500 ◄── hope it's STRINGLN powershell -Command "$x=1; ...
enough ... <work> ...; (Get-Host) | Out-Null"
STRINGLN cmd REM the command toggles a lock key when done
WAIT_FOR_CAPS_CHANGE ◄── blocks until it
STRINGLN next-thing actually finished
This is the closest Ducky Script gets to feedback. It does not work for everything — it needs the typed command to be able to toggle a lock key, which is platform- and shell-specific — but where it works, it replaces a guessed DELAY with a confirmed one, and that is the difference between a payload that works on your test machine and a payload that works on the target.
SAVE_HOST_KEYBOARD_LOCK_STATE / RESTORE_HOST_KEYBOARD_LOCK_STATE bracket this: snapshot the host’s real lock-key state, abuse the lock keys for signalling, put them back. Leaving a target’s Num Lock flipped is a small tell, and small tells add up (Vol 15).
6. Button waits and BUTTON_DEF
Every USB Rubber Ducky has a physical button, and 3.0 makes it programmable — a second input channel, this one from the operator rather than the host:
| Command | Effect |
|---|---|
WAIT_FOR_BUTTON_PRESS | halt the payload until the operator presses the button |
BUTTON_DEF … END_BUTTON | define a block that runs whenever the button is pressed (an interrupt handler) |
DISABLE_BUTTON / ENABLE_BUTTON | gate whether the button triggers BUTTON_DEF |
$_BUTTON_TIMEOUT, $_BUTTON_PUSH_RECEIVED, $_BUTTON_USER_DEFINED | internal variables for button state/timeout logic |
Why it matters operationally:
- Operator-gated execution.
WAIT_FOR_BUTTON_PRESSnear the top means the payload does nothing until you decide — plug the device in calmly, navigate the target to the right state by hand, then press the button. No more racing a guessed openingDELAY. - Multiple payloads / re-runs. A
BUTTON_DEFblock plusRESTART_PAYLOAD(Vol 4 §9) lets one device hold a payload you can fire repeatedly, or pick between behaviours by press pattern. - Abort / safe-state. A
BUTTON_DEFthatRESETs andSTOP_PAYLOADs is a panic button.
The button and the lock-key channel together are the two things that let a payload wait for the right moment instead of hoping it guessed the moment — and that is the whole reliability story.
7. Jitter — defeating the timing signature
Here is a detection fact (expanded in Vol 15): no human types at perfectly even intervals. A 1.0 payload with a fixed DELAY 5 between keystrokes produces a metronomic, machine-perfect timing pattern — and a behavioural detector can flag “this ‘keyboard’ types 200 cps with zero variance” as obviously not human.
Jitter introduces deliberate, random variation into inter-keystroke timing:
$_JITTER_ENABLED = TRUE
$_JITTER_MAX = 40 REM up to 40 ms of random delay between keystrokes
$_JITTER_ENABLED—TRUE/FALSE, master switch.$_JITTER_MAX— the maximum random delay added between keystrokes, in milliseconds (0-65535; the documented default is 20).
With jitter on, each keystroke is followed by a random 0–$_JITTER_MAX ms pause, so the timing profile looks irregular — more human, less “obviously a script.” It serves two purposes at once:
- Detection evasion — defeats the “impossibly even timing” heuristic.
- Reliability — irregular, slightly-slower typing is also less likely to outrun a slow target and drop keystrokes. Jitter is sometimes the fix for a payload that works on fast machines and fails on slow ones.
The cost is speed: jitter makes the payload slower, and on a shoulder-surf-risk target every extra second of “the screen is doing things by itself” is exposure. It is a tunable trade — Vol 15 (defense) and Vol 16 (posture) both weigh in.
8. Randomization
3.0 can generate random content, not just random timing. The use cases: defeating naive signature matching (a payload whose typed content is identical every run is trivially signatured), generating unpredictable filenames/identifiers, and fuzz-ish behaviour.
Random-character commands (each injects one random keystroke of its class):
RANDOM_LOWERCASE_LETTER RANDOM_UPPERCASE_LETTER RANDOM_LETTER
RANDOM_NUMBER RANDOM_SPECIAL RANDOM_CHAR
Random-integer machinery (numbers you can compute with):
$_RANDOM_MIN = 1000
$_RANDOM_MAX = 9999
VAR $port = $_RANDOM_INT REM a random integer in [MIN, MAX]
$_RANDOM_INT— yields a random integer in the current[$_RANDOM_MIN, $_RANDOM_MAX]range (default 0–9).$_RANDOM_SEED— a seed value (read from aseed.binon the device) so randomness is reproducible when you need it to be, and varied when you don’t.$_RANDOM_*_KEYCODEvariants — return the scan code rather than injecting, when you need the value programmatically.
REM Build an unpredictable temp filename
STRING loot_
RANDOM_LETTER
RANDOM_LETTER
RANDOM_NUMBER
RANDOM_NUMBER
STRINGLN .tmp
Randomization is a light evasion tool — it breaks exact-match signatures but does nothing against behavioural or content-semantic detection. Treat it as one layer, not a cloak (Vol 15).
9. Timing discipline — the synthesis
Everything in Vols 3-5 about timing — DELAY, WAIT_FOR_*, the button, jitter — comes down to one engineering discipline. State it as a hierarchy, best to worst:
The timing-reliability hierarchy
════════════════════════════════════════════════════════
BEST Operator-gated WAIT_FOR_BUTTON_PRESS — a human
confirms the moment. Can't be wrong.
Host-confirmed WAIT_FOR_CAPS_CHANGE — the host
itself signals "done". Robust where
it's achievable.
OS-branched IF ($_OS ...) — at least you're
guessing guessing the right kind of guess.
WORST Blind guessed DELAY 500 — works on the machine you
DELAY tested, maybe not the next one.
The discipline is: climb that hierarchy as far as the payload’s situation allows. Most payloads end up a mix — a WAIT_FOR_BUTTON_PRESS to start, $_OS to branch, WAIT_FOR_CAPS_CHANGE where a command can signal completion, and guessed DELAYs (with jitter) for the rest. A payload that is all blind DELAYs is a 1.0 payload that happens to be saved as 3.0; a payload that uses the detection features is using the language you actually paid for.
| Symptom | Likely cause | Fix |
|---|---|---|
| Works on your laptop, fails on the target | guessed DELAYs too short for a slower host | climb the hierarchy: button gate, host-confirmed waits, or longer + jittered delays |
| Keystrokes land in the wrong window | typed before focus settled | DELAY after every focus change; WAIT_FOR_* if a signal is available |
| Dropped/garbled keystrokes on some hosts | typing faster than the host processes | enable jitter; raise $_JITTER_MAX; add inter-key DELAY |
| Flagged by endpoint software | machine-perfect timing signature | jitter on; randomize content; and read Vol 15 |
| Payload fired at the wrong time | ran on plug-in before the operator was ready | WAIT_FOR_BUTTON_PRESS near the top |
10. Resources
- Ducky Script quick reference (jitter, randomization,
WAIT_FOR_*, internal variables): https://docs.hak5.org/hak5-usb-rubber-ducky/duckyscript-quick-reference/ - Vol 3 —
DELAYand the blind-typing problem this volume mitigates - Vol 4 — the
IF/WHILE/VARmachinery these signals feed - Vol 6 — Keystroke Reflection: the lock-key channel turned into exfiltration
- Vol 15 — detection: the defensive side of jitter and randomization
- Vol 13 — payload patterns that apply this discipline
This is Volume 5 of an 18-volume series. Next: Vol 6 covers exfiltration and ATTACKMODE — using the device as more than a typist: presenting as storage, spoofing its USB identity, and the Keystroke Reflection exfiltration channel that turns three lock-key bits into a way to read data back off the host.