UnoDucky: a USB Rubber Ducky imitation using an Arduino Uno

The aim of this project was to build a USB Rubber Ducky using an Arduino Uno and the components present in the “Arduino Starter Kit”.

The main parts of this project are:

  1. the arduino circuit
  2. the arduino code (sketch)
  3. The ducky script encoder, written in Python.

The resulting “device” works like this: you copy the encoded ducky scripts on a micro SD card that you insert in the reader. When the arduino is plugged in a computer, it will let you choose the script you want to run. Then, the arduino will behave like a Rubber Ducky (i.e. like an automatic keyboard).

Circuit

When assembled, it looks like this:

The LCD screen is used to show all the scripts found in the root of the SD card. The buttons allow to navigate through those files and select the script to run.

All the used components are in the official Arduino Starter Kit except for the SD card reader which I bought on amazon.

Sketch

The software running on the arduino is pretty simple. It reads the files from the SD card and let the user to select an encoded ducky script to run it.

An encoded ducky script contains the keys the arduino has to send on the host computer.

arduino/src/ducky_arduino/ducky_arduino.ino view raw
#include "SdFat.h"
#include <LiquidCrystal.h>
#include <string.h>



LiquidCrystal lcd(8, 7, 5, 4, 3, 2);

SdFat SD;
static const int SD_CS_PIN = 10;

static const int BTN_NEXT_PIN = 6;
static const int BTN_PLAY_PIN = 9;

int btnNextState = 0;
int btnPlayState = 0;

static const int DELAY_B_RELEASE = 10; // How many millis before release
static const int DELAY_A_RELEASE = 30; // How many millis after release

File root;
File currentFile;


void setup() {
  // Only for debug
  Serial.begin(9600);

  // SD init
  initializeSD();

  // buttons init
  pinMode(BTN_NEXT_PIN, INPUT);
  pinMode(BTN_PLAY_PIN, INPUT);

  // lcd init
  lcd.begin(16, 2);

  // show first file on lcd
  root = SD.open("/");
  currentFile = getNextFile();
  printFileOnLCD(currentFile);
  
}

void loop() {
  int btnPlayCurrentState = digitalRead(BTN_PLAY_PIN);

  if (btnPlayCurrentState != btnPlayState) {
    // Button "Play" pushed
    if (btnPlayCurrentState == HIGH) {
      playCurrentFile();
    }
    btnPlayState = btnPlayCurrentState;
  }

  
  int btnNextCurrentState = digitalRead(BTN_NEXT_PIN);
  if (btnNextCurrentState != btnNextState) {
    // Button "Next" pushed
    if (btnNextCurrentState == HIGH) {
      currentFile = getNextFile();
      printFileOnLCD(currentFile);
    }
    btnNextState = btnNextCurrentState;
  }
}



void initializeSD() {
  pinMode(SD_CS_PIN, OUTPUT);

  if (!SD.begin()) {
    lcd.clear();
    lcd.print("Error: SD init");
  }
  return;
}

File getNextFile() {
  File entry = root.openNextFile();
  if (!entry) {
    root.rewind();
    entry = root.openNextFile();
  } 
  while (entry.isDir()) {
    entry = root.openNextFile();
  } // TODO: what if there's no file? how to stop?
  return entry;
}

void printFileOnLCD(File f) {
  /**
   * Print filename and size of the specified file 
   * on the initialized lcd screen
   */
  char fileName[16];
  f.getName(fileName, 16);
  lcd.clear();
  lcd.print(fileName);
  lcd.setCursor(0,1);
  lcd.print(f.size());// + " b");
}


int read8Bytes(char* buf) {
  /**
   * Copy 8 bytes from the current file to the specified buffer.
   * Returns the number of bytes successfully read.
   */
  int i = 0;
  while (i != 8)
  {
    if (!currentFile.available()) {
      return i;
    }
    buf[i] = currentFile.read();
    i++;
  }
  return i;
}

void releaseKey(char* buf) 
{
  memset(buf, 0, 8);
  Serial.write(buf, 8); // Release key  
}

void playCurrentFile() {
  /**
   * Output the content of the currently selected 
   * file via the usb port.
   */
  char buf[8] = { 0 };
  while (read8Bytes(buf)) {
    if (buf[0] == 0x07) { // Delay
      uint8_t delay_time[7];
      memcpy(delay_time, &buf[1], 7 * sizeof(uint8_t));
      delay((int)delay_time);
    }
    else {
      Serial.write(buf, 8);
      delay(DELAY_B_RELEASE);
      releaseKey(buf);
      delay(DELAY_A_RELEASE);
    }
  }
  currentFile.seek(0);
    
}

The encoder

The encoder is a python script taking a ducky script as input and producing an encoded version of it. It also supports several keyboard layouts.

encoder/encoder.py view raw
import sys
import os
import argparse



current_line = 0
ducky_script = ""
ducky_lines = []

# =======================================================
# The following functions handle each a rubber ducky 
# command. They take the eventual parameter as argument.
# =======================================================

def ducky_STRING(text):
    rtn = b''
    for c in text:
        rtn += char_to_byte(c)

    return rtn

def ducky_REM(rem):
    return b''

def ducky_ENTER():
    return char_to_byte('\n')

def ducky_DELAY(delay):
    rtn = b''
    if delay.isdigit():
        rtn = b'\x07' + int(delay).to_bytes(7, byteorder='big')
    else:
        raise Exception("Invalid delay")
    return rtn

def ducky_ALT(control):
    return ducky_MODIFIERS(["ALT"], control)

def ducky_SHIFT(control):
    return ducky_MODIFIERS(["SHIFT"], control)

def ducky_CTRL(control):
    return ducky_MODIFIERS(["CTRL"], control)

def ducky_COMMAND(control):
    return ducky_MODIFIERS(["COMMAND"], control)

def ducky_GUI(control):
    return ducky_MODIFIERS(["GUI"], control)
    
def ducky_ESC(control):
    return ducky_MODIFIERS(["ESC"], control)
    
def ducky_MODIFIERS(modifier_keys, control):
    """ 
        Returns the bytcode corespnding to the association of the 
        modifier key (ALT, CTRL, ...) and another key.

        Parameters
        -----------
        modifier_keys: str
            A string rpreenting a modifier key (defined in lang.modifiers)
            One of those values: ["CTRL", "SHIFT", "ALT", "COMMAND", "GUI"]
        control: str
            A string representing the key. May be empty string, otherwise, 
            must be defined in lang.keys.
    """
    rtn = b''
    modifier_key = bytearray(8)
    for k in modifier_keys:
        
        modifier_key = bytes_or(lang.modifiers[k], modifier_key)
    
    if control != "": # modifier + something
        if control.upper() in lang.keys:
            rtn = bytes_or(modifier_key, lang.keys[control.upper()])
        else:
            raise Exception("Unknown key : \""+control+"\"")

    else: # Just the modifier key
        rtn = lang.keys[modifier_key]

    return rtn



def ducky_REPEAT(nb):
    rtn = b''
    if nb.isdigit():
        print(int(nb))
        print((ducky_to_hex(ducky_lines[current_line-1]))*int(nb))
        if current_line != 0:
            rtn = (ducky_to_hex(ducky_lines[current_line-1]))*int(nb)
        else:
            raise Exception("REPEAT can't be the first instruction of the script.")
            
    else:
        raise Exception("Invalid repeatition")

    return rtn

def ducky_KEY(key):
    rtn = b''
    if key in lang.keys:
        rtn = lang.keys[key]
    else:
        raise Exception("Unknown key : \""+key+"\"")
    return rtn



# =======================================================
# =======================================================
# =======================================================

def bytes_or(a, b):
    """
        Returns
        --------
        bytes
            A bitwise OR operation on the two parameters.
    """
    rtn = bytearray(b'')
    a_b = bytearray(a)
    b_b = bytearray(b)

    if len(a_b) != len(b_b):
        return b''

    i = 0
    while i<len(a_b):
        rtn.append(int(a[i] | b[i]))
        i+=1

    return rtn

# Dictionnary that associates a ducky command with the 
# function that handles its translation to bytecode.
supported_command = {
    "STRING": ducky_STRING,
    "REM": ducky_REM,
    "ENTER": ducky_ENTER,
    "DELAY": ducky_DELAY,
    "ALT": ducky_ALT, 
    "SHIFT": ducky_SHIFT, 
    "CTRL": ducky_CTRL, 
    "CONTROL": ducky_CTRL, 
    "ESC": ducky_ESC, 
    "ESCAPE": ducky_ESC, 
    "COMMAND": ducky_COMMAND, 
    "REPEAT": ducky_REPEAT,
    "GUI": ducky_GUI, 
    "WINDOWS": ducky_GUI, 
    "CTRL-SHIFT": (lambda c: ducky_MODIFIERS(["CTRL", "SHIFT"], c)), 
}

def char_to_byte(c):
    """
        Converts a character to the key combinaison that produces it, expressed in a height bytes long bytecode.
        Parameters
        ----------
        c : str
            A single character, well defined in lang.chars.
    """
    rtn = b''
    if c in lang.chars:
        rtn = lang.chars[c]
    else:
        raise Exception("Unsupported character : '" + str(c) + "'")
    return rtn


def ducky_to_hex(ducky_line):
    """
        Takes a line of ducky script and returns an encoded, equivalent bytecode, for the arduino
    """
    command = ducky_line.split(maxsplit=1)
    rtn = b''
    if len(command) > 0:
        if command[0] in supported_command:
            if len(command) == 1:
                rtn = supported_command[command[0]]()
            else:
                rtn = supported_command[command[0]](command[1])
        elif command[0] in lang.keys:
            rtn = ducky_KEY(command[0])

        else:
            raise Exception("Invalid script: \"" + command[0] + "\" is not a valid command")

    return rtn


def encode_ducky():
    global ducky_lines, current_line

    ducky_lines = ducky_script.split('\n')
    rtn = b''
    while current_line < len(ducky_lines):
        l = ducky_lines[current_line]
        try:
            rtn += ducky_to_hex(l)
        except Exception as e:
            raise Exception("Syntax error at line " + str(current_line+1) + " in the script : \n" + str(e))

        current_line += 1

    return rtn



def generate_output_filename(input_filename):
    """ Returns the input_filename with a new extention """
    return os.path.splitext(input_filename)[0]+'.bin'

parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input_file', action='store', help="Input filename", required=True)
parser.add_argument('-o', '--output_file', action='store', help="Output filename", required=False)
parser.add_argument('-l', '--key_layout', action='store', choices=['en_us', 'fr_be'], help="Keyboard layout", required=False, default='en_us')
args = parser.parse_args()

lang = __import__(args.key_layout)

if args.output_file == None:
    args.output_file = generate_output_filename(args.input_file)

f = open(args.input_file, "r")
ducky_script = f.read()
f.close()

compiled = encode_ducky()


fo = open(args.output_file, "wb+")
fo.write(compiled)

Source code

The whole project (including source code, documentation and installation instructions) is on github.

Note: The project was made for fun and doesn’t intend to replace a Rubber Ducky. It’s not really usable, just functional.