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:
the arduino circuit
the arduino code (sketch)
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.
#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.
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.